您的位置:首页 > 编程语言 > Java开发

Java NIO与IO的区别和比较

2010-08-14 14:11 579 查看
Java NIO与IO的区别和比较

底层的IO有:IO的四种模式,分为阻塞IO,多路IO,非阻塞IO和异步IO,而Java的NIO是将多路IO与非阻塞IO这两种模式通过Selector和Channel进行了实现,同时支持了异步IO。

总体而言, IO与NIO的优势比较,IO对每一个socket要建立一个线程,线程与socket之间的关系是绑定的,不能使用线程池,而通过NIO可以结合线程池对需要的socket进行处理,不再需要将socket与线程进行绑定。对于多路非阻塞的核心是使用selector,对注册的事件通过selectionKey进行返回处理,而异步IO是在进行操作的时候就可以将监听器注册上或者在读取文件的时间返回Future共进一步的判断。其实对于阻塞IO也是可以使用线程池的,不过由于IO是阻塞的,没有完成之前,线程与Socket之间的关系不能解绑,故10000个同时的Socket请求需要10000个线程进行进行处理,而使用NIO即非阻塞的方式,可以将Socket和Thread解除绑定,通过Selector可以只有在Socket进行处理的读写的时候才用到线程,而阻塞将交给Selector进行完成了,所以对于10000个Socket请求可能不需要相同数目的Thread进行处理了,故在大数据量的Socket请求的时候,使用NIO可以提高吞吐量和性能。具体解释,可以参考:IO和NIO的简单比较

NIO开始于JDK1.4以上,NIO的特性是:非阻塞I/O,字符转换,缓冲以及通道。如上为了提高性能NIO引入了selector的概念,而selector监听注册到channel上的的selectorKey, 而channel用于进行数据双向交互,交互时用的不再是IO中用到的对象而是*Buffer,如ByteBuffer。

但是这样好吗?现在是OOD时代了,通过channel只能传送*Buffer,而增加一层是*Buffer与对象的转化!可以参看stackOverflow中 NIO
send Objects. 从此文章中,可以转化为对象的方法是ObjectInputStream(new ByteArrayInputStream(byteBuffer.Array())).readObject() 即可得到对象了,其中byteBuffer是从channel中读到的bytes。

NIO包(java.nio.*)引入了四个关键的抽象数据类型,它们共同解决传统的I/O类中的一些问题。

1. Buffer:它是包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。Buffer定义了一个存放线性 primitive type数据的容器接口,除boolean之外的其他primitive type都有一个相应的子类,其中ByteBuffer是最重要的子类。

2. Charset:它提供Unicode字符串影射到字节序列以及逆影射的操作。

3. Channels:包含socket,file和pipe三种管道,它实际上是双向交流的通道。

4. Selector:它将多元异步I/O操作集中到一个或多个线程中(它可以被看成是Unix中select()函数或Win32中WaitForSingleEvent()函数的面向对象版本)。

关于Selector机制有几篇文章介绍的不错,从中亦可以学习到一些遇到问题的解决办法

http://blog.csdn.net/haoel/article/details/2224055

http://blog.csdn.net/haoel/article/details/2224069

关于unix中的select(),select()是Linux/Unix 网络编程中的一个重要函数,通过调用select函数可以确定一个或者多个套接字(描述符)的状态,判断套接字上是否有数据需要读出或者写入。与select()比较密切的还有poll和epoll函数,详见http://www.cnblogs.com/biyeymyhjob/archive/2012/07/12/2588649.html

http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html

看看传统IO

在介绍NIO之前,有必要了解传统的I/O操作的方式。http://www.cnblogs.com/rollenholt/archive/2011/09/11/2173787.html

以网络应用为例,传统方式需要监听一个ServerSocket,接受请求的连接为其提供服务(服务通常包括了处理请求并发送响应)图一是服务器的生命周期图,其中标有粗黑线条的部分表明会发生I/O阻塞。



可以分析创建服务器的每个具体步骤。首先创建ServerSocket:ServerSocket server=new ServerSocket(10000);

然后接受新的连接请求:Socket newConnection=server.accept();

对于accept方法的调用将造成阻塞,直到ServerSocket接受到一个连接请求为止。一旦连接请求被接受,服务器可以读客户socket中的请求。

InputStream in = newConnection.getInputStream();
InputStreamReader reader = new InputStreamReader(in);
BufferedReader buffer = new BufferedReader(reader);
Request request = new Request();
while(!request.isComplete()) {
String line = buffer.readLine();
request.addLine(line);
}


这样的操作有两个问题,首先BufferedReader类的readLine()方法在其缓冲区未满时会造成线程阻塞,只有一定数据填满了缓冲区或者客户关闭了套接字,方法才会返回。其次,它回产生大量的垃圾,BufferedReader创建了缓冲区来从客户套接字读入数据,但是同样创建了一些字符串存储这些数据。虽然BufferedReader内部提供了StringBuffer处理这一问题,但是所有的String很快变成了垃圾需要回收。

同样的问题在发送响应代码中也存在

Response response = request.generateResponse();
OutputStream out = newConnection.getOutputStream();
InputStream in = response.getInputStream();
int ch;
while(-1 != (ch = in.read())) {
out.write(ch);
}
newConnection.close();


IO是以流方式一次一个字节的处理,而NIO是以块方式处理数据 . 类似的,读写操作被阻塞而且向流中一次写入一个字符会造成效率低下,所以应该使用缓冲区,但是一旦使用缓冲,流又会产生更多的垃圾。

传统的解决方法

通常在Java中处理阻塞I/O要用到线程(大量的线程)。一般是实现一个线程池用来处理请求,线程使得服务器可以处理多个连接,但是它们也同样引发了许多问题。每个线程拥有自己的栈空间并且占用一些CPU时间,耗费很大,而且很多时间是浪费在阻塞的I/O操作上,没有有效的利用CPU。对于此种情况在NIO中使用Selector将会有效提高线程的使用效率。



.I/O

1. Buffer
Buffer均非线性安全,而不在这个包中的StringBuffer是线性安全的,StringBuilder也非线性安全。每个Buffer子类都有以下属性:Capacity(容量)、limit(写时limit=capacity,读时limit=有效长度)、postition(当前读写的下标位置)和mark(一个临时存放的下标位置)。谈到的String,StringBuffer和StringBuilder,在这里插一杠子:http://www.cnblogs.com/A_ming/archive/2010/04/13/1711395.html 当我们在字符串缓冲去被多个线程使用是,JVM不能保证StringBuilder的操作是安全的,虽然他的速度最快,但是可以保证StringBuffer是可以正确操作的。当然大多数情况下就是我们是在单线程下进行的操作,所以大多数情况下是建议用StringBuilder而不用StringBuffer的,就是速度的原因。对于三者使用的总结
1.如果要操作少量的数据用 = String 2.单线程操作字符串缓冲区下操作大量数据 = StringBuilder 3.多线程操作字符串缓冲区下操作大量数据 = StringBuffer

传统的I/O不断的浪费对象资源(通常是String),新I/O通过使用Buffer读写数据避免了资源浪费。Buffer对象是线性的,有序的数据集合,它根据其类别只包含唯一的数据类型。

java.nio.Buffer类描述

java.nio.ByteBuffer包含字节类型。 可以从ReadableByteChannel中读在 WritableByteChannel中写

java.nio.MappedByteBuffer包含字节类型,直接在内存某一区域映射

java.nio.CharBuffer包含字符类型,不能写入通道

java.nio.DoubleBuffer包含double类型,不能写入通道

java.nio.FloatBuffer包含float类型

java.nio.IntBuffer包含int类型

java.nio.LongBuffer包含long类型

java.nio.ShortBuffer包含short类型



可以通过调用allocate(int capacity)方法或者allocateDirect(int capacity)方法分配一个Buffer。特别的,你可以创建MappedBytesBuffer通过调用FileChannel.map(int mode,long
position,int size)。直接(direct)buffer在内存中分配一段连续的块并使用本地访问方法读写数据,这里的直接内存不是在JVM中分配的,而是直接在真正的内存中。非直接(nondirect)buffer通过使用Java中的数组访问代码读写数据。有时候必须使用非直接缓冲例如使用任何的wrap方法(如ByteBuffer.wrap(byte[]))在Java数组基础上创建buffer。使用直接内存的好处是,减少了JVM对内存的读写,因为一般而言,JVM对内存的对象管理是放到Heap中的,即如果没有直接内存,则JVM会多一层处理,并且直接内存会减少对分配给Java内存的占用,同时又不进行垃圾回收管理,但此分配直接内存的方法一般是在使用本地IO时,如读写文件时进行使用。官方解释如下:

A byte buffer is either direct or non-direct.
Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it
. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after)
each invocation of one of the underlying operating system's native I/O operations.

A direct byte buffer may be created by invoking the
allocateDirect
factory method of this class. The buffers returned by this method typically have somewhat higher allocation and deallocation costs than non-direct buffers. The contents of direct buffers may reside
outside of the normal garbage-collected heap, and so their impact upon the memory footprint of an application might not be obvious. It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the
underlying system's native I/O operations. In general it is best to allocate direct buffers only when they yield a measureable gain in program performance. 即最好使用直接内存当会有性能的很大提升时。

2. 字符编码

向ByteBuffer中存放数据涉及到两个问题:字节的顺序和字符转换。ByteBuffer内部通过ByteOrder类处理了字节顺序问题,但是并没有处理字符转换。事实上,ByteBuffer没有提供方法读写String。

Java.nio.charset.Charset处理了字符转换问题。它通过构造CharsetEncoder和CharsetDecoder将字符序列转换成字节和逆转换。

3. 通道(Channel)

你可能注意到现有的java.io类中没有一个能够读写Buffer类型,所以NIO中提供了Channel类来读写Buffer。通道可以认为是一种连接,可以是到特定设备,程序或者是网络的连接。通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是
InputStream
或者
OutputStream
的子类),
通道
可以用于读、写或者同时用于读写。因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在
UNIX 模型中,底层操作系统通道是双向的。

通道的类等级结构图如下



图中ReadableByteChannel和WritableByteChannel分别用于读写。

GatheringByteChannel可以从使用一次将多个Buffer中的数据写入通道,相反的,ScatteringByteChannel则可以一次将数据从通道读入多个Buffer中。你还可以设置通道使其为阻塞或非阻塞I/O操作服务。

为了使通道能够同传统I/O类相容,Channel类提供了静态方法创建Stream或Reader

4. Selector

在过去的阻塞I/O中,我们一般知道什么时候可以向stream中读或写,因为方法调用直到stream准备好时返回。但是使用非阻塞通道,我们需要一些方法来知道什么时候通道准备好了。在NIO包中,设计Selector就是为了这个目的。SelectableChannel可以注册特定的事件,而不是在事件发生时通知应用,通道跟踪事件。然后,当应用调用Selector上的任意一个selection方法时,它查看注册了的通道看是否有任何感兴趣的事件发生。下图是selector和两个已注册的通道的例子
http://www.cnblogs.com/rollenholt/archive/2011/09/29/2195730.html

接下来的这张 UML 类图描述了 java.nio.channels 中类的关系:





并不是所有的通道都支持所有的操作。SelectionKey类定义了所有可能的操作位,将要用两次。首先,当应用调用SelectableChannel.register(Selector sel,int op)方法注册通道时,它将所需操作作为第二个参数传递到方法中。然后,一旦SelectionKey被选中了,SelectionKey的readyOps()方法返回所有通道支持操作的数位的和。SelectableChannel的validOps方法返回每个通道允许的操作。注册通道不支持的操作将引发IllegalArgumentException异常。下表列出了SelectableChannel子类所支持的操作。

ServerSocketChannel OP_ACCEPT

SocketChannel OP_CONNECT, OP_READ, OP_WRITE

DatagramChannel OP_READ, OP_WRITE

Pipe.SourceChannel OP_READ

Pipe.SinkChannel OP_WRITE


如果使用NIO进行Server端和Client端进行通信,请参看本博中的:NIO 网络通信


使用NIO读写文件

关于IO中的BufferedWriter, BufferedReader:

Writes text to a character-output stream, buffering characters so as to provide for the efficient writing of single characters, arrays, and strings: flush()

Reads text from a character-input stream, buffering characters so as to provide for the efficient reading of characters, arrays, and lines: readLine()

在我们第一个练习中,我们将从一个文件中读取一些数据。如果使用原来的 I/O,那么我们只需创建一个
FileInputStream
并从它那里读取。而在
NIO 中,情况稍有不同:我们首先从
FileInputStream
获取一个
FileInputStream
对象,然后使用这个通道来读取数据。

在 NIO 系统中,任何时候执行一个读操作,您都是从通道中读取,但是您不是 直接 从通道读取。因为所有数据最终都驻留在缓冲区中,所以您是从通道读到缓冲区中。

因此读取文件涉及三个步骤:(1) 从
FileInputStream
获取
Channel
,(2)
创建
Buffer
,(3)
将数据从
Channel
读到
Buffer
中。

现在,让我们看一下这个过程。

三个容易的步骤

第一步是获取通道。我们从
FileInputStream
获取通道:

?
下一步是创建缓冲区:

?

最后,需要将数据从通道读到缓冲区中,如下所示:

?
您会注意到,我们不需要告诉通道要读 多少数据 到缓冲区中。每一个缓冲区都有复杂的内部统计机制,它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据

写入文件

在 NIO 中写入文件类似于从文件中读取。首先从
FileOutputStream
获取一个通道:

?
下一步是创建一个缓冲区并在其中放入一些数据 - 在这里,数据将从一个名为
message
的数组中取出,这个数组包含字符串
"Some bytes" 的 ASCII 字节(本教程后面将会解释
buffer.flip()
buffer.put()
调用)。

?
最后一步是写入缓冲区中

?
注意在这里同样不需要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。

读写结合

下面我们将看一下在结合读和写时会有什么情况。我们以一个名为 CopyFile.java 的简单程序作为这个练习的基础,它将一个文件的所有内容拷贝到另一个文件中。CopyFile.java 执行三个基本操作:首先创建一个
Buffer
,然后从源文件中将数据读到这个缓冲区中,然后将缓冲区写入目标文件。这个程序不断重复
― 读、写、读、写 ― 直到源文件结束。

CopyFile 程序让您看到我们如何检查操作的状态,以及如何使用
clear()
flip()
方法重设缓冲区,并准备缓冲区以便将新读取的数据写到另一个通道中。

运行
CopyFile 例子

因为缓冲区会跟踪它自己的数据,所以 CopyFile 程序的内部循环 (inner loop) 非常简单,如下所示:

?
第一行将数据从输入通道
fcin
中读入缓冲区,第二行将这些数据写到输出通道
fcout


检查状态

下一步是检查拷贝何时完成。当没有更多的数据时,拷贝就算完成,并且可以在
read()
方法返回
-1 是判断这一点,如下所示:

?
重设缓冲区

最后,在从输入通道读入缓冲区之前,我们调用
clear()
方法。同样,在将缓冲区写入输出通道之前,我们调用
flip()
方法,如下所示

?
clear()
方法重设缓冲区,使它可以接受读入的数据。
flip()
方法让缓冲区可以将新读入的数据写入另一个通道。
http://www.cnblogs.com/rollenholt/archive/2011/09/29/2195730.html 关于为什么要使用flip和clear方法在本link中有详尽说明。

缓冲区的使用:一个内部循环

下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。其中fcin和fcout分别为使用FileInputStream和FileOutputStream得到的channel. Buffer为新建的ByteBuffer,ByteBuffer.allocate(1024)

?
read()
write()
调用得到了极大的简化,因为许多工作细节都由缓冲区完成了。
clear()
flip()
方法用于让缓冲区在读和写之间切换

缓冲区分配和包装

在能够读和写之前,必须有一个缓冲区。要创建缓冲区,您必须 分配 它。我们使用静态方法
allocate()
来分配缓冲区:

?
allocate()
方法分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中
― 在本例中是一个
ByteBuffer


您还可以将一个现有的数组转换为缓冲区,如下所示:

?
本例使用了
wrap()
方法将一个数组包装为缓冲区。必须非常小心地进行这类操作。一旦完成包装,底层数据就可以通过缓冲区或者直接访问。

缓冲区分片

slice()
方法根据现有的缓冲区创建一种 子缓冲区。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。

使用例子可以最好地说明这点。让我们首先创建一个长度为 10 的
ByteBuffer


?
然后使用数据来填充这个缓冲区,在第 n 个槽中放入数字 n:

?
现在我们对这个缓冲区分片,以创建一个包含槽 3 到槽 6 的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个 窗口 。

窗口的起始和结束位置通过设置
position
limit
值来指定,然后调用
Buffer
slice()
方法:

?
是缓冲区的
子缓冲区
。不过,
片段
缓冲区
共享同一个底层数据数组,我们在下一节将会看到这一点。

直接和间接缓冲区

另一种有用的
ByteBuffer
是直接缓冲区。 直接缓冲区是为加快
I/O 速度,而以一种特殊的方式分配其内存的缓冲区。

实际上,直接缓冲区的准确定义是与实现相关的。Sun 的文档是这样描述直接缓冲区的:

给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。

您可以在例子程序 FastCopyFile.java 中看到直接缓冲区的实际应用,这个程序是 CopyFile.java 的另一个版本,它使用了直接缓冲区以提高速度。

还可以用内存映射文件创建直接缓冲区。

内存映射文件
I/O

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

内存映射文件 I/O 是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会送入(或者 映射 )到内存中。

内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。

尽管创建内存映射文件相当简单,但是向它写入可能是危险的。仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

连网和异步 I/O

概述

连网是学习异步 I/O 的很好基础,而异步 I/O 对于在 Java 语言中执行任何输入/输出过程的人来说,无疑都是必须具备的知识。NIO 中的连网与 NIO 中的其他任何操作没有什么不同 ― 它依赖通道和缓冲区,而您通常使用
InputStream
OutputStream
来获得通道。

本节首先介绍异步 I/O 的基础 ― 它是什么以及它不是什么,然后转向更实用的、程序性的例子。

异步
I/O

异步 I/O 是一种 没有阻塞地 读写数据的方法。通常,在代码进行
read()
调用时,代码会阻塞直至有可供读取的数据。同样,
write()
调用将会阻塞直至数据能够写入。

另一方面,异步 I/O 调用不会阻塞。相反,您将注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接,等等,而在发生这样的事件时,系统将会告诉您。

异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。

我们将通过研究一个名为 MultiPortEcho.java 的例子程序来查看异步 I/O 的实际应用。这个程序就像传统的 echo server,它接受网络连接并向它们回响它们可能发送的数据。不过它有一个附加的特性,就是它能同时监听多个端口,并处理来自所有这些端口的连接。并且它只在单个线程中完成所有这些工作。

步骤:1. Selector 2 ServerSocketChannel(set blocking false,bind port to the ServerSocket) 3. channel 上注册Selector,和selectorKey, 4.selector.select() 及selectedKeys 得到所有的事件List,每个事件都是一个selectionKey, 5. 通过key可以得到channel,channel.accept得到SocketChannel. 然后可以 2,3,4,5

Selectors

本节的阐述对应于
MultiPortEcho
的源代码中的
go()
方法的实现,因此应该看一下源代码,以便对所发生的事情有个更全面的了解。

异步 I/O 中的核心对象名为
Selector
Selector
就是您注册对各种
I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。

所以,我们需要做的第一件事就是创建一个
Selector


?
然后,我们将对不同的通道对象调用
register()
方法,以便注册我们对这些对象中发生的
I/O 事件的兴趣。
register()
的第一个参数总是这个
Selector


打开一个
ServerSocketChannel

为了接收连接,我们需要一个
ServerSocketChannel
。事实上,我们要监听的每一个端口都需要有一个
ServerSocketChannel
。对于每一个端口,我们打开一个
ServerSocketChannel
,如下所示:

?
第一行创建一个新的
ServerSocketChannel
,最后三行将它绑定到给定的端口。第二行将
ServerSocketChannel
设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法,否则异步
I/O 就不能工作。

选择键

下一步是将新打开的
ServerSocketChannels
注册到
Selector
上。为此我们使用
ServerSocketChannel.register() 方法,如下所示

?
register()
的第一个参数总是这个
Selector
。第二个参数是
OP_ACCEPT
,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。这是适用于
ServerSocketChannel
的唯一事件类型。

请注意对
register()
的调用的返回值。
SelectionKey
代表这个通道在此
Selector
上的这个注册。当某个
Selector
通知您某个传入事件时,它是通过提供对应于该事件的
SelectionKey
来进行的。
SelectionKey
还可以用于取消通道的注册。

内部循环

现在已经注册了我们对一些 I/O 事件的兴趣,下面将进入主循环。使用
Selectors
的几乎每个程序都像下面这样使用内部循环:

?
首先,我们调用
Selector
select()
方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时,
select()
方法将返回所发生的事件的数量。

接下来,我们调用
Selector
selectedKeys()
方法,它返回发生了事件的
SelectionKey
对象的一个
集合


我们通过迭代
SelectionKeys
并依次处理每个
SelectionKey
来处理事件。对于每一个
SelectionKey
,您必须确定发生的是什么
I/O 事件,以及这个事件影响哪些 I/O 对象。

监听新连接

程序执行到这里,我们仅注册了
ServerSocketChannel
,并且仅注册它们“接收”事件。为确认这一点,我们对
SelectionKey
调用
readyOps()
方法,并检查发生了什么类型的事件:

?
可以肯定地说,
readOps()
方法告诉我们该事件是新的连接。

接受新的连接

因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心
accept()
操作会阻塞:

?
下一步是将新连接的
SocketChannel
配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将
SocketChannel
注册到
Selector
上,如下所示:

?
注意我们使用
register()
OP_READ
参数,将
SocketChannel
注册用于 读取 而不是 接受 新连接。

删除处理过的
SelectionKey

在处理
SelectionKey
之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的
SelectionKey
从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的
remove()
方法来删除处理过的
SelectionKey


?
现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的 I/O 事件)了。

传入的
I/O

当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用
Selector.select()
,并返回一个或者多个
I/O 事件。这一次,
SelectionKey
将被标记为
OP_READ
事件,如下所示:

?
与以前一样,我们取得发生 I/O 事件的通道并处理它。在本例中,由于这是一个 echo server,我们只希望从套接字中读取数据并马上将它发送回去。

每次返回主循环,我们都要调用
select
Selector()
方法,并取得一组
SelectionKey
。每个键代表一个
I/O 事件。我们处理事件,从选定的键集中删除
SelectionKey
,然后返回主循环的顶部。

这个程序有点过于简单,因为它的目的只是展示异步 I/O 所涉及的技术。在现实的应用程序中,您需要通过将通道从
Selector
中删除来处理关闭的通道。而且您可能要使用多个线程。这个程序可以仅使用一个线程,因为它只是一个演示,但是在现实场景中,创建一个线程池来负责
I/O 事件处理中的耗时部分会更有意义。

字符集

根据 Sun 的文档,一个
Charset
是“十六位 Unicode 字符序列与字节序列之间的一个命名的映射”。实际上,一个
Charset
允许您以尽可能最具可移植性的方式读写字符序列。

Java 语言被定义为基于 Unicode。然而在实际上,许多人编写代码时都假设一个字符在磁盘上或者在网络流中用一个字节表示。这种假设在许多情况下成立,但是并不是在所有情况下都成立,而且随着计算机变得对 Unicode 越来越友好,这个假设就日益变得不能成立了。

在本节中,我们将看一下如何使用
Charsets
以适合现代文本格式的方式处理文本数据。这里将使用的示例程序相当简单,不过,它触及了使用
Charset
的所有关键方面:为给定的字符编码创建
Charset
,以及使用该
Charset
解码和编码文本数据。

编码/解码

要读和写文本,我们要分别使用
CharsetDecoder
CharsetEncoder
。将它们称为 编码器 和 解码器 是有道理的。一个 字符 不再表示一个特定的位模式,而是表示字符系统中的一个实体。因此,由某个实际的位模式表示的字符必须以某种特定的 编码 来表示。

CharsetDecoder
用于将逐位表示的一串字符转换为具体的
char
值。同样,一个
CharsetEncoder
用于将字符转换回位。

在下一个小节中,我们将考察一个使用这些对象来读写数据的程序。

处理文本的正确方式

现在我们将分析这个例子程序 UseCharsets.java。这个程序非常简单 ― 它从一个文件中读取一些文本,并将该文本写入另一个文件。但是它把该数据当作文本数据,并使用
CharBuffer
来将该数句读入一个
CharsetDecoder
中。同样,它使用
CharsetEncoder
来写回该数据。

我们将假设字符以 ISO-8859-1(Latin1) 字符集(这是 ASCII 的标准扩展)的形式储存在磁盘上。尽管我们必须为使用 Unicode 做好准备,但是也必须认识到不同的文件是以不同的格式储存的,而 ASCII 无疑是非常普遍的一种格式。事实上,每种 Java 实现都要求对以下字符编码提供完全的支持:

US-ASCII

ISO-8859-1

UTF-8

UTF-16BE

UTF-16LE

UTF-16

示例程序

在打开相应的文件、将输入数据读入名为
inputData
ByteBuffer
之后,我们的程序必须创建
ISO-8859-1 (Latin1) 字符集的一个实例:

?
然后,创建一个解码器(用于读取)和一个编码器 (用于写入):

?
 为了将字节数据解码为一组字符,我们把
ByteBuffer
传递给
CharsetDecoder
,结果得到一个
CharBuffer


?
 如果想要处理字符,我们可以在程序的此处进行。但是我们只想无改变地将它写回,所以没有什么要做的。

要写回数据,我们必须使用
CharsetEncoder
将它转换回字节:

?
 在转换完成之后,我们就可以将数据写到文件中了。

结束语和参考资料

结束语

正如您所看到的, NIO 库有大量的特性。在一些新特性(例如文件锁定和字符集)提供新功能的同时,许多特性在优化方面也非常优秀。

在基础层次上,通道和缓冲区可以做的事情几乎都可以用原来的面向流的类来完成。但是通道和缓冲区允许以 快得多 的方式完成这些相同的旧操作 ― 事实上接近系统所允许的最大速度。

不过 NIO 最强大的长度之一在于,它提供了一种在 Java 语言中执行进行输入/输出的新的(也是迫切需要的)结构化方式。随诸如缓冲区、通道和异步 I/O 这些概念性(且可实现的)实体而来的,是我们重新思考 Java 程序中的 I/O过程的机会。这样,NIO 甚至为我们最熟悉的 I/O 过程也带来了新的活力,同时赋予我们通过和以前不同并且更好的方式执行它们的机会。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息