Java NIO API详解

Java NIO API详解
Java NIO API详解

Java NIO API详解

NIO API 主要集中在java.nio 和它的subpackages 中:

java.nio

定义了Buffer 及其数据类型相关的子类。其中被java.nio.channels 中的类用来进行IO 操作的ByteBuffer 的作用非常重要。

java.nio.channels

定义了一系列处理IO 的Channel 接口以及这些接口在文件系统和网络通讯上的实现。通过Selector 这个类,还提供了进行非阻塞IO 操作的办法。这个包可以说是NIO API 的核心。

java.nio.channels.spi

定义了可用来实现channel 和selector API 的抽象类。

java.nio.charset

定义了处理字符编码和解码的类。

java.nio.charset.spi

定义了可用来实现charset API 的抽象类。

java.nio.channels.spi 和java.nio.charset.spi 这两个包主要被用来对现有NIO API 进行扩展,在实际的使用中,我们一般只和另外的3 个包打交道。下面将对这3 个包一一介绍。

Package java.nio

这个包主要定义了Buffer 及其子类。Buffer 定义了一个线性存放primitive type 数据的容器接口。对于除boolean 以外的其他primitive type ,都有一个相应的Buffer 子类,ByteBuffer 是其中最重要的一个子类。

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

Buffer

定义了一个可以线性存放primitive type 数据的容器接口。Buffer 主要包含了与类型(byte, char… )无关的功能。值得注意的是Buffer 及其子类都不是线程安全的。

每个Buffer 都有以下的属性:

capacity

这个Buffer 最多能放多少数据。capacity 一般在buffer 被创建的时候指定。

limit

在Buffer 上进行的读写操作都不能越过这个下标。当写数据到buffer 中时,limit 一般和capacity 相等,当读数据时,limit 代表buffer 中有效数据的长度。

position

读/ 写操作的当前下标。当使用buffer 的相对位置进行读/ 写操作时,读/ 写会从这个下标进行,并在操作完成后,buffer 会更新下标的值。

mark

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

这些属性总是满足以下条件:

0 <= mark <= position <= limit <= capacity

limit 和position 的值除了通过limit() 和position() 函数来设置,也可以通过下面这些函数来改变:

Buffer clear()

把position 设为0 ,把limit 设为capacity ,一般在把数据写入Buffer 前调用。

Buffer flip()

把limit 设为当前position ,把position 设为0 ,一般在从Buffer 读出数据前调用。Buffer rewind()

把position 设为0 ,limit 不变,一般在把数据重写入Buffer 前调用。

Buffer 对象有可能是只读的,这时,任何对该对象的写操作都会触发一个ReadOnlyBufferException 。isReadOnly() 方法可以用来判断一个Buffer 是否只读。

ByteBuffer

在Buffer 的子类中,ByteBuffer 是一个地位较为特殊的类,因为在java.io.channels 中定义的各种channel 的IO 操作基本上都是围绕ByteBuffer 展开的。

ByteBuffer 定义了4 个static 方法来做创建工作:

ByteBuffer allocate(int capacity)

创建一个指定capacity 的ByteBuffer 。

ByteBuffer allocateDirect(int capacity)

创建一个direct 的ByteBuffer ,这样的ByteBuffer 在参与IO 操作时性能会更好(很有可能是在底层的实现使用了DMA 技术),相应的,创建和回收direct 的ByteBuffer 的代价也会高一些。isDirect() 方法可以检查一个buffer 是否是direct 的。

ByteBuffer wrap(byte [] array)

ByteBuffer wrap(byte [] array, int offset, int length)

把一个byte 数组或byte 数组的一部分包装成ByteBuffer 。

ByteBuffer 定义了一系列get 和put 操作来从中读写byte 数据,如下面几个:

byte get()

ByteBuffer get(byte [] dst)

byte get(int index)

ByteBuffer put(byte b)

ByteBuffer put(byte [] src)

ByteBuffer put(int index, byte b)

这些操作可分为绝对定位和相对定为两种,相对定位的读写操作依靠position 来定位Buffer 中的位置,并在操作完成后会更新position 的值。

在其它类型的buffer 中,也定义了相同的函数来读写数据,唯一不同的就是一些参数和返回值的类型。

除了读写byte 类型数据的函数,ByteBuffer 的一个特别之处是它还定义了读写其它primitive 数据的方法,如:

int getInt()

从ByteBuffer 中读出一个int 值。

ByteBuffer putInt(int value)

写入一个int 值到ByteBuffer 中。

读写其它类型的数据牵涉到字节序问题,ByteBuffer 会按其字节序(大字节序或小字节序)写入或读出一个其它类型的数据(int,long… )。字节序可以用order 方法来取得和设置:

ByteOrder order()

返回ByteBuffer 的字节序。

ByteBuffer order(ByteOrder bo)

设置ByteBuffer 的字节序。

ByteBuffer 另一个特别的地方是可以在它的基础上得到其它类型的buffer 。如:

CharBuffer asCharBuffer()

为当前的ByteBuffer 创建一个CharBuffer 的视图。在该视图buffer 中的读写操作会按照

ByteBuffer 的字节序作用到ByteBuffer 中的数据上。

用这类方法创建出来的buffer 会从ByteBuffer 的position 位置开始到limit 位置结束,可以看作是这段数据的视图。视图buffer 的readOnly 属性和direct 属性与ByteBuffer 的一致,而且也只有通过这种方法,才可以得到其他数据类型的direct buffer 。

ByteOrder

用来表示ByteBuffer 字节序的类,可将其看成java 中的enum 类型。主要定义了下面几个static 方法和属性:

ByteOrder BIG_ENDIAN

代表大字节序的ByteOrder 。

ByteOrder LITTLE_ENDIAN

代表小字节序的ByteOrder 。

ByteOrder nativeOrder()

返回当前硬件平台的字节序。

MappedByteBuffer

ByteBuffer 的子类,是文件内容在内存中的映射。这个类的实例需要通过FileChannel 的map() 方法来创建。

接下来看看一个使用ByteBuffer 的例子,这个例子从标准输入不停地读入字符,当读满一行后,将收集的字符写到标准输出:

public static void main(String [] args)

throws IOException

{

// 创建一个capacity为256的ByteBuffer

ByteBuffer buf = ByteBuffer.allocate(256);

while ( true ) {

// 从标准输入流读入一个字符

int c = System.in.read();

// 当读到输入流结束时,退出循环

if (c == -1)

break ;

// 把读入的字符写入ByteBuffer中

buf.put(( byte ) c);

// 当读完一行时,输出收集的字符

if (c == '\n' ) {

// 调用flip()使limit变为当前的position的值,position变为0,

// 为接下来从ByteBuffer读取做准备

buf.flip();

// 构建一个byte数组

byte [] content = new byte [buf.limit()];

// 从ByteBuffer中读取数据到byte数组中

buf.get(content);

// 把byte数组的内容写到标准输出

System.out.print( new String(content));

// 调用clear()使position变为0,limit变为capacity的值,

// 为接下来写入数据到ByteBuffer中做准备

buf.clear();

}

}

}

Package java.nio.channels

这个包定义了Channel 的概念,Channel 表现了一个可以进行IO 操作的通道(比如,通过FileChannel ,我们可以对文件进行读写操作)。java.nio.channels 包含了文件系统和网络通讯相关的channel 类。这个包通过Selector 和SelectableChannel 这两个类,还定义了一个进行非阻塞(non-blocking )IO 操作的API ,这对需要高性能IO 的应用非常重要。

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

Channel

Channel 表现了一个可以进行IO 操作的通道,该interface 定义了以下方法:

boolean isOpen()

该Channel 是否是打开的。

void close()

关闭这个Channel ,相关的资源会被释放。

ReadableByteChannel

定义了一个可从中读取byte 数据的channel interface 。

int read(ByteBuffer dst)

从channel 中读取byte 数据并写到ByteBuffer 中。返回读取的byte 数。

WritableByteChannel

定义了一个可向其写byte 数据的channel interface 。

int write(ByteBuffer src)

从ByteBuffer 中读取byte 数据并写到channel 中。返回写出的byte 数。

ByteChannel

ByteChannel 并没有定义新的方法,它的作用只是把ReadableByteChannel 和WritableByteChannel 合并在一起。

ScatteringByteChannel

继承了ReadableByteChannel 并提供了同时往几个ByteBuffer 中写数据的能力。

GatheringByteChannel

继承了WritableByteChannel 并提供了同时从几个ByteBuffer 中读数据的能力。

InterruptibleChannel

用来表现一个可以被异步关闭的Channel 。这表现在两方面:

1.当一个InterruptibleChannel 的close() 方法被调用时,其它block 在这个InterruptibleChannel 的IO 操作上的线程会接收到一个AsynchronousCloseException 。2.当一个线程block 在InterruptibleChannel 的IO 操作上时,另一个线程调用该线程的interrupt() 方法会导致channel 被关闭,该线程收到一个ClosedByInterruptException ,同时线程的interrupt 状态会被设置。

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

非阻塞IO

非阻塞IO 的支持可以算是NIO API 中最重要的功能,非阻塞IO 允许应用程序同时监控多个channel 以提高性能,这一功能是通过Selector ,SelectableChannel 和SelectionKey 这3 个类来实现的。

SelectableChannel 代表了可以支持非阻塞IO 操作的channel ,可以将其注册在Selector 上,这种注册的关系由SelectionKey 这个类来表现(见UML 图)。Selector 这个类通过select() 函数,给应用程序提供了一个可以同时监控多个IO channel 的方法:

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

下面是JDK 1.4 中非阻塞IO 的一个例子,这段code 使用了非阻塞IO 实现了一个time

// 给ServerSocketChannel对应的socket绑定IP和端口

InetAddress lh = InetAddress.getLocalHost();

InetSocketAddress isa = new InetSocketAddress(lh, port);

ssc.socket().bind(isa);

// 将ServerSocketChannel注册到Selector上,返回对应的SelectionKey

SelectionKey acceptKey =

ssc.register(acceptSelector, SelectionKey.OP_ACCEPT);

int keysAdded = 0;

// 用select()函数来监控注册在Selector上的SelectableChannel

// 返回值代表了有多少channel可以进行IO操作(ready for IO)

while ((keysAdded = acceptSelector.select()) > 0) {

// selectedKeys()返回一个SelectionKey的集合,

// 其中每个SelectionKey代表了一个可以进行IO操作的channel。

// 一个ServerSocketChannel可以进行IO操作意味着有新的TCP连接连入了

Set readyKeys = acceptSelector.selectedKeys();

Iterator i = readyKeys.iterator();

while (i.hasNext()) {

SelectionKey sk = (SelectionKey) i.next();

// 需要将处理过的key从selectedKeys这个集合中删除

i.remove();

// 从SelectionKey得到对应的channel

ServerSocketChannel nextReady =

(ServerSocketChannel) sk.channel();

// 接受新的TCP连接

Socket s = nextReady.accept().socket();

// 把当前的时间写到这个新的TCP连接中

PrintWriter out =

new PrintWriter(s.getOutputStream(), true );

Date now = new Date();

out.println(now);

// 关闭连接

out.close();

}

}

}

这是个纯粹用于演示的例子,因为只有一个ServerSocketChannel 需要监控,所以其实并不真的需要使用到非阻塞IO 。不过正因为它的简单,可以很容易地看清楚非阻塞IO 是如何工作的。

SelectableChannel

这个抽象类是所有支持非阻塞IO 操作的channel (如DatagramChannel 、SocketChannel )的父类。SelectableChannel 可以注册到一个或多个Selector 上以进行非阻塞IO 操作。

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

SelectableChannel configureBlocking(boolean block)

设置blocking 模式。

boolean isBlocking()

返回blocking 模式。

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

int validOps()

返回一个bit mask ,表示这个channel 上支持的IO 操作。当前在SelectionKey 中,用静态常量定义了 4 种IO 操作的bit 值:OP_ACCEPT ,OP_CONNECT ,OP_READ 和OP_WRITE 。

SelectionKey register(Selector sel, int ops)

将当前channel 注册到一个Selector 上并返回对应的SelectionKey 。在这以后,通过调用Selector 的select() 函数就可以监控这个channel 。ops 这个参数是一个bit mask ,代表了需要监控的IO 操作。

SelectionKey register(Selector sel, int ops, Object att)

这个函数和上一个的意义一样,多出来的att 参数会作为attachment 被存放在返回的SelectionKey 中,这在需要存放一些session state 的时候非常有用。

boolean isRegistered()

该channel 是否已注册在一个或多个Selector 上。

SelectableChannel 还提供了得到对应SelectionKey 的方法:

SelectionKey keyFor(Selector sel)

返回该channe 在Selector 上的注册关系所对应的SelectionKey 。若无注册关系,返回null 。

Selector

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

Selector open()

Selector 的一个静态方法,用于创建实例。

在一个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 中移走。这个集合无法直接访问。

以下是select() 相关方法的说明:

int select()

监控所有注册的channel ,当其中有注册的IO 操作可以进行时,该函数返回,并将对应的SelectionKey 加入selected-key set 。

int select(long timeout)

可以设置超时的select() 操作。

int selectNow()

进行一个立即返回的select() 操作。

Selector wakeup()

使一个还未返回的select() 操作立刻返回。

SelectionKey

代表了Selector 和SelectableChannel 的注册关系。

Selector 定义了4 个静态常量来表示4 种IO 操作,这些常量可以进行位操作组合成一个bit mask 。

int OP_ACCEPT

有新的网络连接可以accept ,ServerSocketChannel 支持这一非阻塞IO 。

int OP_CONNECT

代表连接已经建立(或出错),SocketChannel 支持这一非阻塞IO 。

int OP_READ

int OP_WRITE

代表了读、写操作。

以下是其主要方法:

Object attachment()

返回SelectionKey 的attachment ,attachment 可以在注册channel 的时候指定。Object attach(Object ob)

设置SelectionKey 的attachment 。

SelectableChannel channel()

返回该SelectionKey 对应的channel 。

Selector selector()

返回该SelectionKey 对应的Selector 。

void cancel()

cancel 这个SelectionKey 所对应的注册关系。

int interestOps()

返回代表需要Selector 监控的IO 操作的bit mask 。

SelectionKey interestOps(int ops)

设置interestOps 。

int readyOps()

返回一个bit mask ,代表在相应channel 上可以进行的IO 操作。

ServerSocketChannel

支持非阻塞操作,对应于https://www.360docs.net/doc/7211821821.html,.ServerSocket 这个类,提供了TCP 协议IO 接口,支持OP_ACCEPT 操作。

ServerSocket socket()

返回对应的ServerSocket 对象。

SocketChannel accept()

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

SocketChannel

支持非阻塞操作,对应于https://www.360docs.net/doc/7211821821.html,.Socket 这个类,提供了TCP 协议IO 接口,支持OP_CONNECT ,OP_READ 和OP_WRITE 操作。这个类还实现了ByteChannel ,ScatteringByteChannel 和GatheringByteChannel 接口。

DatagramChannel 和这个类比较相似,其对应于https://www.360docs.net/doc/7211821821.html,.DatagramSocket ,提供了UDP 协议IO 接口。

Socket socket()

返回对应的Socket 对象。

boolean connect(SocketAddress remote)

boolean finishConnect()

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

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 。

Package java.nio.charset

这个包定义了Charset 及相应的encoder 和decoder 。下面这张UML 类图描述了这个包中类的关系,可以将其中Charset ,CharsetDecoder 和CharsetEncoder 理解成一个Abstract Factory 模式的实现:

Charset

代表了一个字符集,同时提供了factory method 来构建相应的CharsetDecoder 和CharsetEncoder 。

Charset 提供了以下static 的方法:

SortedMap availableCharsets()

返回当前系统支持的所有Charset 对象,用charset 的名字作为set 的key 。boolean isSupported(String charsetName)

判断该名字对应的字符集是否被当前系统支持。

Charset forName(String charsetName)

返回该名字对应的Charset 对象。

Charset 中比较重要的方法有:

String name()

返回该字符集的规范名。

Set aliases()

返回该字符集的所有别名。

CharsetDecoder newDecoder()

创建一个对应于这个Charset 的decoder 。

CharsetEncoder newEncoder()

创建一个对应于这个Charset 的encoder 。

CharsetDecoder

将按某种字符集编码的字节流解码为unicode 字符数据的引擎。

CharsetDecoder 的输入是ByteBuffer ,输出是CharBuffer 。进行decode 操作时一般按如下步骤进行:

1.调用CharsetDecoder 的reset() 方法。(第一次使用时可不调用)

2.调用decode() 方法0 到n 次,将endOfInput 参数设为false ,告诉decoder 有可能还有新的数据送入。

3.调用decode() 方法最后一次,将endOfInput 参数设为true ,告诉decoder 所有数据都已经送入。

4.调用decoder 的flush() 方法。让decoder 有机会把一些内部状态写到输出的CharBuffer 中。

CharsetDecoder reset()

重置decoder ,并清除decoder 中的一些内部状态。

CoderResult decode(ByteBuffer in, CharBuffer out, boolean endOfInput)

从ByteBuffer 类型的输入中decode 尽可能多的字节,并将结果写到CharBuffer 类型的输出中。根据decode 的结果,可能返回3 种CoderResult :CoderResult.UNDERFLOW 表示已经没有输入可以decode ;CoderResult.OVERFLOW 表示输出已满;其它的CoderResult 表示decode 过程中有错误发生。根据返回的结果,应用程序可以采取相应的措施,比如,增加输入,清除输出等等,然后再次调用decode() 方法。

CoderResult flush(CharBuffer out)

有些decoder 会在decode 的过程中保留一些内部状态,调用这个方法让这些decoder 有机会将这些内部状态写到输出的CharBuffer 中。调用成功返回CoderResult.UNDERFLOW 。如果输出的空间不够,该函数返回CoderResult.OVERFLOW ,这时应用程序应该扩大输出CharBuffer 的空间,然后再次调用该方法。

CharBuffer decode(ByteBuffer in)

一个便捷的方法把ByteBuffer 中的内容decode 到一个新创建的CharBuffer 中。在这个方法中包括了前面提到的4 个步骤,所以不能和前3 个函数一起使用。

decode 过程中的错误有两种:malformed-input CoderResult 表示输入中数据有误;unmappable-character CoderResult 表示输入中有数据无法被解码成unicode 的字符。如何处理decode 过程中的错误取决于decoder 的设置。对于这两种错误,decoder 可以通过CodingErrorAction 设置成:

1.忽略错误

2.报告错误。(这会导致错误发生时,decode() 方法返回一个表示该错误的CoderResult 。)

3.替换错误,用decoder 中的替换字串替换掉有错误的部分。

CodingErrorAction malformedInputAction()

返回malformed-input 的出错处理。

CharsetDecoder onMalformedInput(CodingErrorAction newAction)

设置malformed-input 的出错处理。

CodingErrorAction unmappableCharacterAction()

返回unmappable-character 的出错处理。

CharsetDecoder onUnmappableCharacter(CodingErrorAction newAction)

设置unmappable-character 的出错处理。

String replacement()

返回decoder 的替换字串。

CharsetDecoder replaceWith(String newReplacement)

设置decoder 的替换字串。

CharsetEncoder

将unicode 字符数据编码为特定字符集的字节流的引擎。其接口和CharsetDecoder 相类似。

CoderResult

描述encode/decode 操作结果的类。

CodeResult 包含两个static 成员:

CoderResult OVERFLOW

表示输出已满

CoderResult UNDERFLOW

表示输入已无数据可用。

其主要的成员函数有:

boolean isError()

boolean isMalformed()

boolean isUnmappable()

boolean isOverflow()

boolean isUnderflow()

用于判断该CoderResult 描述的错误。

int length()

返回错误的长度,比如,无法被转换成unicode 的字节长度。

void throwException()

抛出一个和这个CoderResult 相对应的exception 。

CodingErrorAction

表示encoder/decoder 中错误处理方法的类。可将其看成一个enum 类型。有以下static 属性:

CodingErrorAction IGNORE

忽略错误。

CodingErrorAction REPLACE

用替换字串替换有错误的部分。

CodingErrorAction REPORT

报告错误,对于不同的函数,有可能是返回一个和错误有关的CoderResult ,也有可能是抛出一个CharacterCodingException

Java NIO Socket通信

一套接字通道

1. 阻塞式套接字通道

与Socket和ServerSocket对应,NIO提供了SocketChannel和ServerSocketChannel 对应,这两种通道同时支持一般的阻塞模式和更高效的非阻塞模式。

客户端通过SocketChannel.open()方法打开一个Socket通道,如果此时提供了SocketAddress参数,则会自动开始连接,否则需要主动调用connect()方法连接,创建连接后,可以像一般的Channel一样的用Buffer进行读写,这都是阻塞模式的。

服务器端通过ServerSocketChannel.open()创建,并使用bind()方法绑定到一个监听地址上,最后调用accept()方法阻塞等待客户端连接。当客户端连接后会返回一个SocketChannel以实现与客户端的读写交互。

总的来说,阻塞模式即是net包I/O的翻版,只是采用Channel和Buffer实现而已。

2.多路复用套接字通道(Selector实现的非阻塞式IO)

套接字通道多路复用的思想是创建一个Selector,将多个通道对它进行注册,当套接字有关注的事件发生时,可以选出这个通道进行操作。

服务器端的代码如下,相关说明就带在注释里了:

查看源码

打印?

01// 创建一个选择器,可用close()关闭,isOpen()表示是否处于打开状态,他不隶属于当前线程

02Selector selector = Selector.open();

03// 创建ServerSocketChannel,并把它绑定到指定端口上

04ServerSocketChannel server = ServerSocketChannel.open(); 05server.bind(new InetSocketAddress("127.0.0.1", 7777)); 06// 设置为非阻塞模式, 这个非常重要

07server.configureBlocking(false);

08// 在选择器里面注册关注这个服务器套接字通道的accept事件

09// ServerSocketChannel只有OP_ACCEPT可用,OP_CONNECT,OP_READ,OP_WRITE用于SocketChannel

10server.register(selector, SelectionKey.OP_ACCEPT); 11while(true) {

12 // 测试等待事件发生,分为直接返回的selectNow()和阻塞等待的select(),另外也可加一个参数表示阻塞超时

13 // 停止阻塞的方法有两种: 中断线程和selector.wakeup(),有事件发生时,会自动的wakeup()

14 // 方法返回为select出的事件数(参见后面的注释有说明这个值为什么可能为0).

15 // 另外务必注意一个问题是,当selector被select()阻塞时,其他的线程调用同一个selector的register也会被阻塞到select返回为止

16 // select操作会把发生关注事件的Key加入到selectionKeys中(只管加不管减)

17 if(selector.select() == 0) { //

18 continue;

19 }

20

21 // 获取发生了关注时间的Key集合,每个SelectionKey对应了注册的一个通道

22 Set keys = selector.selectedKeys();

23 // 多说一句selector.keys()返回所有的SelectionKey(包括没有发生事件的)

24 for(SelectionKey key : keys) {

25 // OP_ACCEPT 这个只有ServerSocketChannel才有可能触发

26 if(key.isAcceptable()) {

27 // 得到与客户端的套接字通道

28 SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();

29 // 同样设置为非阻塞模式

30 channel.configureBlocking(false);

31 // 同样将于客户端的通道在selector上注册,OP_READ对应可读事件(对方有写入数据),可以通过key获取关联的选择器

32 channel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(1024));

33 }

34 // OP_READ 有数据可读

35 if(key.isReadable()) {

36 SocketChannel channel = (SocketChannel) key.channel();

37 // 得到附件,就是上面SocketChannel进行register的时候的第三个参数,可为随意Object

38 ByteBuffer buffer = (ByteBuffer) key.attachment();

39 // 读数据这里就简单写一下,实际上应该还是循环读取到读不出来为止的

40 channel.read(buffer);

41 // 改变自身关注事件,可以用位或操作|组合时间

42 key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

43 }

44 // OP_WRITE 可写状态这个状态通常总是触发的,所以只在需要写操作时才进行关注

45 if(key.isWritable()) {

46 // 写数据掠过,可以自建buffer,也可用附件对象(看情况),注意buffer写入后需要flip

47 // ......

48 // 写完就吧写状态关注去掉,否则会一直触发写事件

49 key.interestOps(SelectionKey.OP_READ);

50 }

51

52 // 由于select操作只管对selectedKeys进行添加,所以key处理后我们需要从里面把key去掉

53 keys.remove(key);

54 }

55}

这里需要着重说明一下select操作做了什么(根据现象推的,具体好像没有找到这个的文档说明),他每次检查keys里面每个Key对应的通道的状态,如果有关注状态时,就决定返回,这时会同时将Key对象加入到selectedKeys中,并返回selectedKeys本次变化的对

象数(原本就在selectedKeys中的对象是不计的),由于一个Key对应一个通道(可能同时处于多个状态,所以注意上面的if语句我都没有写else),所以select返回0也是有可能的。另外OP_WRITE和OP_CONNET这两个状态是不能长期关注的,只在有需要的时候监听,处理完必须马上去掉。如果没有发现有任何关注状态,select会一直阻塞到有状态变化或者超时什么的。

SelectionKey的其他几个方法,attach(Object)为key设置附件,并返回之前的附件;interestOps()和readyOps()返回关注状态和当前状态;cancel()为取消注册;isValid()表示key是否有效(在key取消注册,通道关闭,选择器关闭这三个事情发生之前,key均为有效的,但不包括对方关闭通道,所以读写应注意异常)。

还有一个状态上面没有使用,OP_CONNECT这个主要是用于客户端,对应的key的方法是isConnectable()表示已经创建好了连接。

非阻塞实现的客户端如下:

查看源码

打印?

01Selector selector = Selector.open();

02// 创建一个套接字通道,注意这里必须使用无参形式

03SocketChannel channel = SocketChannel.open();

04// 设置为非阻塞模式,这个方法必须在实际连接之前调用(所以open的时候不能提供服务器地址,否则会自动连接)

05channel.configureBlocking(false);

06// 连接服务器,由于是非阻塞模式,这个方法会发起连接请求,并直接返回false(阻塞模式是一直等到链接成功并返回是否成功)

07channel.connect(new InetSocketAddress("127.0.0.1", 7777)); 08// 注册关联链接状态

09channel.register(selector, SelectionKey.OP_CONNECT);

10while(true) {

11 // 前略和服务器端的类似

12 // ...

13 // 获取发生了关注时间的Key集合,每个SelectionKey对应了注册的一个通道

14 Set keys = selector.selectedKeys();

15 for(SelectionKey key : keys) {

16 // OP_CONNECT 两种情况,链接成功或失败这个方法都会返回true

17 if(key.isConnectable()) {

18 // 由于非阻塞模式,connect只管发起连接请求,finishConnect()方法会阻塞到链接结束并返回是否成功

19 // 另外还有一个isConnectionPending()返回的是是否处于正在连接状态(还在三次握手中)

20 if(channel.finishConnect()) {

21 // 链接成功了可以做一些自己的处理,略

22 // ...

23 // 处理完后必须吧OP_CONNECT关注去掉,改为关注OP_READ

24 key.interestOps(SelectionKey.OP_READ);

25 }

26 }

27 // 后略和服务器端的类似

28 // ...

29 }

30}

虽然例子是这样的,不过服务器和客户端可以自己单方面选择是否采用非阻塞模式,用阻塞模式的客户端连接非阻塞模式的服务器端是OK的。

二NIO2的异步IO通道

以下API是由Java7提供。老版本无法使用。

异步IO通道的实现有两种实现方式,一是在阻塞模式的原方法(主要指的是read和write,具体可以查看API文档)上传于一个CompletionHandler实例以实现回调,另外也可以令其返回一个Future实例(Java5新增同步工具包java.util.concurrent中的API),然后再适当的时候通过其get方法来获取返回的结果。异步文件I/O通道为AsynchronousFileChannel,而异步套接字通道为AsynchronousServerSocketChannel,分别对应其各自的原始通道。

异步I/O需要与一个AsynchronousChannelGroup对象关联,他实质上就是一个用于I/O 的线程池。AsynchronousChannelGroup对象可以通过其自身静态方法的withThreadPool(),withCachedThreadPool(),withFixedThreadPool()提供一个线程池来创建(线程池也是Java5新增同步工具包java.util.concurrent中的API)。在异步通道创建open()时,可将这个对象传入进行关联。如果没有提供这个对象的话,就默认使用系统分组。但是需要注意的是系统分组的线程池是个守护线程池,JVM是可能在没有读写完成前正常结束的。AsynchronousChannelGroup在使用完后需要shutdowm(),这方面和线程池的关闭是类似的。

相关主题
相关文档
最新文档