IO系列文章之二:Reactor与Java NIO学习

本系列的上一篇文章已经介绍过,IO的阻塞与非阻塞看的是发起IO请求时是否会被阻塞。

一、使用阻塞IO

在原来进行Java Socket网络编程时,每打开一个I/O通道,read()就一直等待读取字节内容,如果内容没有准备好,read()会阻塞直到数据到来,但此时线程不能做其它事情,所以解决方法就是开辟一个线程池,把每个请求分发到一个线程中去,让线程去等待。

存在的问题:

一个客户端一个线程的方式去处理,则由于创建、维护和切换线程需要的系统开销导致系统扩展性方面受到了很大限制。对于连接生存期比较长的协议来说,线程池的大小仍然限制了系统可以同时处理的客户端数量。如果增加线程池的大小,将带来更多的线程处理开销,而不能提升系统的性能,因为在大部分的时间里客户端是处于空闲状态的。

二、对阻塞IO的改进

根据上文中的情况,由于要一直等待数据准备好,所以进程会一直阻塞直到有数据进来。因此能不能在有数据进来时自动通知,这样就不必开启多个线程死等,从而也就不堵塞了。

其实这就是NIO中就使用的一种多线程模式reactor(有些文章翻译成反应器或反应堆模式)。

三、关于Reactor模式


 

Reactor:负责响应IO事件,当检测到一个新的事件,将其发送给相应的Handler去处理。

Reactor在Java NIO中由JDK类库提供的Selector类实现,循环监听所有注册到Selector上Channel的事件,在没有感兴趣的事件时阻塞。

事件类型及对应常量值
读事件:SelectionKey.OP_READ(1)
写事件:SelectionKey.OP_WRITE(4)
客户端连接服务端事件:SelectionKey.OP_CONNECT(8)
服务端接收客户端连接事件:SelectionKey.OP_ACCEPT(16)

Acceptor:负责接受客户端连接,并实现分派任务操作处理。
Handler:负责处理请求(read...send),同时将handler与事件绑定。

关于Reactor模式,这里有一个关于服务员处理顾客点餐的比喻,我觉得很生动形象,看过后应该更容易理解,分享给大家:http://daimojingdeyu.iteye.com/blog/828696  

四、Java NIO

Java BIO中,一直使用流的方式完成I/O。所有I/O都被视为单个的字节移动,通过Stream对象一次移动一个字节。

Java NIO使用不同的方式--块I/O,块I/O的效率可以比流I/O高许多。

面向流的I/O一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。面向流的I/O通常相当慢。

面向块的I/O以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按流式的字节处理数据要快得多。但是面向块的I/O缺少一些面向流的 I/O 所具有的优雅性和简单性。

1、缓冲区:

Java NIO 中,所有数据都是用缓冲区处理的。

在读取数据时,从通道中读取的任何数据是直接读到缓冲区中的。

在写入数据时,发送给通道的所有对象都必须首先放到缓冲区中。

缓冲区实质上是一个数组,但缓冲区不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程

(1)缓冲区分配和包装:

在能读写之前,必须有一个缓冲区。要创建缓冲区,您必须分配它。我们使用静态方法allocate()来分配缓冲区,值得注意的是 Buffer 及其子类都不是线程安全的。

ByteBuffer buffer = ByteBuffer.allocate(1024);

将一个现有的数组转换为缓冲区:

byte array[] = new byte[1024];

ByteBuffer buffer = ByteBuffer.wrap(array);

slice()方法根据现有的缓冲区创建子缓冲区

buffer.position(3);

buffer.limit(7);

ByteBuffer slice = buffer.slice();

新缓冲区与原来的缓冲区共享一部分数据,如果修改子缓冲区中的数据,原缓冲区内对应数据也会被修改。

(2)只读缓冲区

可以读取它们,但是不能向它们写入。通过调用缓冲区的asReadOnlyBuffer()方法,将任何常规缓冲区转换为只读缓冲区。

(3)直接缓冲区

为加快I/O速度,而以一种特殊的方式分配其内存的缓冲区。

Sun文档:

给定一个直接字节缓冲区,Java虚拟机将尽最大努力直接对它执行本机I/O操作。

也就是说,它会在每一次调用底层操作系统的本机I/O操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。

(4)内存映射文件I/O:

只有文件中实际读取或者写入的部分才会送入或者映射到内存中。只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,0,1024);

将文件的前1024个字节映射到内存。

2、Channel

Channel是一个对象,可以通过它读取和写入数据。通道就像是流。

将数据写入包含一个或者多个字节的缓冲区,不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

通道与流的不同之处在于通道是双向的,而流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者同时用于读写。

3、状态变量

(1)、三个值指定缓冲区在任意时刻的状态:

position:跟踪已经写了或者读了多少数据。

limit:表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。position总是小于或者等于limit。

capacity:表明可以储存在缓冲区中的最大数据容量.limit决不能大于capacity。

另外,关于mark:

一个临时存放的位置下标。调用 mark() 会将mark设为当前的position的值,以后调用reset()会将position属性设置为mark的值。 mark的值总是小于等于position的值,如果将position的值设的比mark小,当前的mark值会被抛弃掉。

(2)、访问方法:

ByteBuffer.get() 获取字节

ByteBuffer.put() 写入字节

Buffer.clear()

重设缓冲区,使它可以接受读入的数据。它将limit设置为与capacity相同。设置position为0。

Buffer.flip()

让缓冲区可以将新读入的数据写入另一个通道。它limit设置为当前position,将position设置为0。

4、Selector、SelectableChannel和SelectionKey

SelectableChannel

代表了可以支持非阻塞IO操作的channel,可以将其注册在Selector上,这种注册的关系由SelectionKey这个类来表现。

SelectableChannel可以是blocking和non-blocking模式,所有channel创建的时候都是blocking模式,只有 non-blocking的SelectableChannel才可以参与非阻塞IO操作。

通过register()方法,SelectableChannel可以注册到Selector上。

ServerSocketChannel支持非阻塞操作,对应于java.net.ServerSocket这个类,提供了TCP协议IO接口,支持OP_ACCEPT操作。

socket():返回对应的ServerSocket对象。

accept():接受一个连接,返回代表这个连接的SocketChannel对象。

SocketChannel支持非阻塞操作,对应于java.net.Socket这个类,提供了TCP协议IO接口,支持OP_CONNECT,OP_READ和OP_WRITE操作。

socket():返回对应的Socket对象。

finishConnect():connect()进行一个连接操作。如果当前SocketChannel是blocking模式,这个函数会等到连接操作完成或错误发生才返回。如果当前SocketChannel是non-blocking模式,函数在连接能立刻被建立时返回true,否则函数返回false,应用程序需要在以后用finishConnect()方法来完成连接操作。

Selector

这个类通过select() 函数,给应用程序提供了一个可以同时监控多个IO channel的方法。

应用程序通过调用select() 函数,让Selector监控注册在其上的多个SelectableChannel ,当有channel的IO操作可以进行时,select()方法就会返回以让应用程序检查channel的状态,并作相应的处理。

Selector可以同时监控多个SelectableChannel的IO状况,是非阻塞IO的核心:

在一个Selector 中,有3个SelectionKey的集合:

(1)key set代表了所有注册在这个Selector上的channel ,这个集合可以通过keys()方法拿到。

(2)Selected-key set代表了所有通过select()方法监测到可以进行IO操作的channel ,这个集合可以通过 selectedKeys()拿到。

(3)Cancelled-key set代表了已经cancel了注册关系的channel ,在下一个select()操作中,这些channel对应的SelectionKey会从key set和cancelled-key set中移走,这个集合无法直接访问。

5、其它API

Pipe

包含了一个读和一个写的channel(Pipe.SourceChannel 和 Pipe.SinkChannel) ,这对channel可以用于进程中的通讯。

FileChannel

用于对文件的读、写、映射、锁定等操作,和映射操作相关的类有FileChannel.MapMode,和锁定操作相关的类有FileLock。值得注意的是FileChannel并不支持非阻塞操作。

Channels

这个类提供了一系列static方法来支持stream类和channel类之间的互操作。这些方法可以将channel类包装为 stream类,比如,将ReadableByteChannel包装为InputStream或Reader;也可以将stream类包装为channel 类,将OutputStream包装为WritableByteChannel。

猜你喜欢

转载自shensy.iteye.com/blog/1867572