开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情
要处理向客户端发送数据的行为,就要让 SocketChannel 把对写事件 SelectionKey.OP_WRITE 的关注也注册到 Selector 中,即
SelectionKey sKey = sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, byteBuffer);
对于服务器来说,向客户端发送数据就是调用 SocketChannel 的 write() 方法,传入待写入数据的 ByteBuffer,但要注意的是 write() 方法对 ByteBuffer 中数据的处理并不是一下子将其完全发送,即非阻塞的,而是和实时网络情况有关,因此需要判断 ByteBuffer 中的数据是否还有剩余,如果有那么需要将其作为附件绑定到 SelectionKey 上
ByteBuffer content = Charset.defaultCharset().encode(stringBuilder.toString());
sc.write(content);
if (content.hasRemaining()) {
sKey.attach(content);
}
这样当有数据没有发送完时,会触发可写事件 SelectionKey.OP_WRITE,服务器需要对 SelectionKey 进行监听,对于可写事件取出待发送的 ByteBuffer,继续使用 SocketChannel 处理,如果 Buffer 中的数据被处理完成,重新关联附件 null,释放掉原来的 ByteBuffer
while (true) {
// 阻塞方法,在没有事件发生时会阻塞,避免线程浪费
selector.select();
// selector作为观察者收到了主题发出的通知,开始处理所有发生的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 从集合中移除待处理的事件
iterator.remove();
// 区分事件类型
if (key.isAcceptable()) {
// ...
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
// 注册到selector上,监听read事件
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
// 传入byteBuffer作为附件
SelectionKey sKey = sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, byteBuffer);
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 3000000; i++) {
stringBuilder.append("a");
}
ByteBuffer content = Charset.defaultCharset().encode(stringBuilder.toString());
sc.write(content);
if (content.hasRemaining()) {
sKey.attach(content);
}
} else if (key.isReadable()) {
// ...
} else if (key.isWritable()) {
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel channel = (SocketChannel) key.channel();
channel.write(buffer);
if (!buffer.hasRemaining()) {
key.attach(null);
}
}
}
}
这样 Selector 就可以在单线程的情况下完成对多个 Channel 事件的监控,实现多路复用。多路复用仅能作用于网络IO,它保证了在有可连接事件时才去连接,有可写事件才去写入,有可读事件才会读取。