共计 3324 个字符,预计需要花费 9 分钟才能阅读完成。
NIO Server 端多路复用开发的一般步骤是:
// 打开选择器
Selector selector = Selector.open();
// 打开通到
ServerSocketChannel socketChannel = ServerSocketChannel.open();
// 配置非阻塞模型
socketChannel.configureBlocking(false);
// 绑定端口
socketChannel.bind(new InetSocketAddress(8080));
// 注册事件,OP_ACCEPT 只适用于 ServerSocketChannel
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while(iter.hasNext()) {
SelectionKey key = iter.next();
if(key.isAcceptable()) {
SocketChannel channel = ((ServerSocketChannel)key.channel()).accept();
channel.configureBlocking(false);
channel.register(selector,SelectionKey.OP_READ);
}
if(key.isWritable()) {
}
if(key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(512);
channel.read(readBuffer);
readBuffer.flip();
//handler Buffer
// 一般是响应客户端的数据
// 直接是 write 写不就完事了嘛,为啥需要 write 事件?
//channel.write(…)
}
iter.remove();
}
}
刚开始对 NIO 的写操作理解的不深,不知道为什么要注册写事件,何时注册写事件,为什么写完之后要取消注册写事件。
如果有 channel 在 Selector 上注册了 SelectionKey.OP_WRITE 事件,在调用 selector.select(); 时,系统会检查内核写缓冲区是否可写(什么时候是不可写的呢,比如缓冲区已满,channel 调用了 shutdownOutPut 等等),如果可写,selector.select(); 立即返回,随后进入 key.isWritable() 分支。
当然你在 channel 上可以直接调用 write(…),也可以将数据发送出去,但这样不够灵活,而且可能浪费 CPU。
看一个场景,服务端需要发送一个 200M 的 Buffer, 看看使用 OP_WRITE 事件和不使用的区别。
// 不使用事件,缺点是,程序运行到这会等到 200M 文件发送完成后才继续往下执行,不符合异步事件模型
// 的编程思想,如果缓冲区一直处于不可写状态,那么该过程一直在这里死循环,浪费了 CPU 资源。
ByteBuffer buffer = …. //200M 的 Buffer
while(buffer.hasRemaining()) {
// 该方法只会写入小于 socket’s output buffer 空闲区域的任何字节数
// 并返回写入的字节数,可能是 0 字节。
channel.write(buffer);
}
// 使用事件的方式,谁好谁坏,一看便知
if(key.isReadable()) {
ByteBuffer buffer = …. //200M 的 Buffer
// 注册写事件
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
// 绑定 Buffer
key.attach(buffer);
}
//isWritable 分支
if(key.isWritable()) {
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel channel = (SocketChannel) key.channel();
if (buffer.hasRemaining()) {
channel.write(buffer)
} else {
// 发送完了就取消写事件,否则下次还会进入该分支
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}
}
客户端开发的一般步骤:
// 打开选择器
Selector selector = Selector.open();
// 打开通道
SocketChannel socketChannel = SocketChannel.open();
// 配置非阻塞模型
socketChannel.configureBlocking(false);
// 连接远程主机
socketChannel.connect(new InetSocketAddress(“127.0.0.1”,8080));
// 注册事件
socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
// 循环处理
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while(iter.hasNext()) {
SelectionKey key = iter.next();
if(key.isConnectable()) {
// 连接建立或者连接建立不成功
SocketChannel channel = (SocketChannel) key.channel();
// 完成连接的建立
if(channel.finishConnect()) {
}
}
if(key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(500 * 1024 * 1024);
buffer.clear();
channel.read(buffer);
//buffer Handler
}
iter.remove();
}
}
起初对 OP_CONNECT 事件还有 finishConnect 不理解,OP_CONNECT 事件何时触发,特别是为什么要在 key.isConnectable() 分支里调用 finishConnect 方法后才能进行读写操作。
首先,在 non-blocking 模式下调用 socketChannel.connect(new InetSocketAddress(“127.0.0.1”,8080)); 连接远程主机,如果连接能立即建立就像本地连接一样,该方法会立即返回 true,否则该方法会立即返回 false, 然后系统底层进行三次握手建立连接。连接有两种结果,一种是成功连接,第二种是异常,但是 connect 方法已经返回,无法通过该方法的返回值或者是异常来通知用户程序建立连接的情况,所以由 OP_CONNECT 事件和 finishConnect 方法来通知用户程序。不管系统底层三次连接是否成功,selector 都会被唤醒继而触发 OP_CONNECT 事件,如果握手成功,并且该连接未被其他线程关闭,finishConnect 会返回 true,然后就可以顺利的进行 channle 读写。如果网络故障,或者远程主机故障,握手不成功,用户程序可以通过 finishConnect 方法获得底层的异常通知,进而处理异常。