一 套接字通道
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,将多个通道对它进行注册,当套接字有关注的事件发生时,可以选出这个通道进行操作。
服务器端的代码如下,相关说明就带在注释里了:
02 |
Selector
selector = Selector.open(); |
04 |
ServerSocketChannel
server = ServerSocketChannel.open(); |
05 |
server.bind( new InetSocketAddress( "127.0.0.1" , 7777 )); |
07 |
server.configureBlocking( false ); |
10 |
server.register(selector,
SelectionKey.OP_ACCEPT); |
17 |
if (selector.select()
== 0 )
{ |
22 |
Set<SelectionKey>
keys = selector.selectedKeys(); |
24 |
for (SelectionKey
key : keys) { |
26 |
if (key.isAcceptable())
{ |
28 |
SocketChannel
channel = ((ServerSocketChannel) key.channel()).accept(); |
30 |
channel.configureBlocking( false ); |
32 |
channel.register(key.selector(),
SelectionKey.OP_READ, ByteBuffer.allocate( 1024 )); |
35 |
if (key.isReadable())
{ |
36 |
SocketChannel
channel = (SocketChannel) key.channel(); |
38 |
ByteBuffer
buffer = (ByteBuffer) key.attachment(); |
42 |
key.interestOps(SelectionKey.OP_READ
| SelectionKey.OP_WRITE); |
45 |
if (key.isWritable())
{ |
49 |
key.interestOps(SelectionKey.OP_READ); |
这里需要着重说明一下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()表示已经创建好了连接。
非阻塞实现的客户端如下:
01 |
Selector
selector = Selector.open(); |
03 |
SocketChannel
channel = SocketChannel.open(); |
05 |
channel.configureBlocking( false ); |
07 |
channel.connect( new InetSocketAddress( "127.0.0.1" , 7777 )); |
09 |
channel.register(selector,
SelectionKey.OP_CONNECT); |
14 |
Set<SelectionKey>
keys = selector.selectedKeys(); |
15 |
for (SelectionKey
key : keys) { |
17 |
if (key.isConnectable())
{ |
20 |
if (channel.finishConnect())
{ |
24 |
key.interestOps(SelectionKey.OP_READ); |
虽然例子是这样的,不过服务器和客户端可以自己单方面选择是否采用非阻塞模式,用阻塞模式的客户端连接非阻塞模式的服务器端是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(),这方面和线程池的关闭是类似的。