多路复用
单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用。
-
多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用
-
如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
-
有可连接事件时才去连接
-
有可读事件才去读取
-
有可写事件才去写入
-
限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件
-
-
Selector
创建
Selector selector = Selector.open();
绑定 Channel 事件
也称之为注册事件,绑定的事件 selector 才会关心
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, 绑定事件);
-
channel 必须工作在非阻塞模式
-
FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用
-
绑定的事件类型可以有
-
connect - 客户端连接成功时触发
-
accept - 服务器端成功接受连接时触发
-
read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
-
write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
-
监听 Channel 事件
//方法1,阻塞直到绑定事件发生
int count = selector.select();
//方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)
int count = selector.select(long timeout);
//方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件
int count = selector.selectNow();
select 何时不阻塞
事件发生时
客户端发起连接请求,会触发 accept 事件
客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件
channel 可写,会触发 write 事件
在 linux 下 nio bug 发生时
调用 selector.wakeup()
调用 selector.close()
selector 所在线程 interrupt
处理 accept 事件
服务器端代码
public class Server {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
//非阻塞(accept方法非阻塞)
ssc.configureBlocking(false);
//将 channel 注册到 selector 里
SelectionKey sscKey = ssc.register(selector, 0, null);
System.out.println("ssc register key:"+sscKey);
//设置为accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
while (true){
//select方法无事件阻塞,有事件不阻塞
//事件要么处理,要么取消,不能置之不理,否则select不会阻塞
selector.select();
//处理事件,selectedKeys 里包含所有发生的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
if (iterator.hasNext()) {
SelectionKey key = iterator.next();
//处理key时,要从selectedKeys中删除,否则下次处理会出问题
iterator.remove();
//处理accept事件
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
System.out.println("sc register key:"+scKey);
}
}
}
}
}
事件发生后能否不处理
事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发
ByteBuffer 大小分配
-
每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为每个 channel 维护一个独立的 ByteBuffer
-
ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer
-
一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能。
-
另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗
-
处理 read 事件
public class Server {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));
//非阻塞(accept方法非阻塞)
ssc.configureBlocking(false);
//将 channel 注册到 selector 里
SelectionKey sscKey = ssc.register(selector, 0, null);
System.out.println("ssc register key:"+sscKey);
//设置为accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
while (true){
//select方法无事件阻塞,有事件不阻塞
//事件要么处理,要么取消,不能置之不理,否则select不会阻塞
selector.select();
//处理事件,selectedKeys 里包含所有发生的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
if (iterator.hasNext()) {
SelectionKey key = iterator.next();
//处理key时,要从selectedKeys中删除,否则下次处理会出问题
iterator.remove();
//处理accept事件
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
//非阻塞(read方法非阻塞)
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16);
//将 channel 注册到 selector 里,添加附件buffer
SelectionKey scKey = sc.register(selector, 0, buffer);
System.out.println("sc register key:"+scKey);
//设置为read事件
scKey.interestOps(SelectionKey.OP_READ);
//处理read事件
}else if (key.isReadable()){
try {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
int read = channel.read(buffer);
if (read == -1) {
//客户端关闭会发送read事件,处理客户端正常关闭,取消该事件
key.cancel();
} else {
split(buffer);
//容量到达上限
if (buffer.position() == buffer.limit()){
//拷贝扩容
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
buffer.flip();
newBuffer.put(buffer);
//为key添加新的附件newBuffer
key.attach(newBuffer);
}
System.out.println(Charset.defaultCharset().decode(buffer));
}
}catch (Exception e){
e.printStackTrace();
//客户端关闭会发送read事件,处理客户端异常关闭,取消该事件
key.cancel();
}
}
}
}
}
public static void split(ByteBuffer source){
source.flip();
for (int i = 0; i < source.limit(); i++) {
//以\n为每个词的结束符
if (source.get(i) == '\n'){
int length = i + 1 - source.position();
for (int j = 0; j < length; j++) {
source.get();
}
}
}
source.compact();
}
}
为何要 iterator.remove()
因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如
第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常
cancel 的作用
cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件
处理 write 事件
-
非阻塞模式下,无法保证把 buffer 中所有数据都写入 channel,因此需要追踪 write 方法的返回值(代表实际写入字节数)
-
用 selector 监听所有 channel 的可写事件,每个 channel 都需要一个 key 来跟踪 buffer,但这样又会导致占用内存过多,就有两阶段策略
-
当消息处理器第一次写入消息时,才将 channel 注册到 selector 上
-
selector 检查 channel 上的可写事件,如果所有的数据写完了,就取消 channel 的注册
-
如果不取消,会每次可写均会触发 write 事件
-
public class WriteServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true){
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
if (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, SelectionKey.OP_READ);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5000000; i++) {
sb.append("a");
}
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
//写一次
int write = sc.write(buffer);
System.out.println(write);
//如果没写完,则设置write事件,等待缓冲区空闲再写
if (buffer.hasRemaining()) {
scKey.interestOps(scKey.interestOps() + SelectionKey.OP_WRITE);
//添加附件buffer
scKey.attach(buffer);
}
}else if (key.isWritable()){
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel channel = (SocketChannel) key.channel();
int write = channel.write(buffer);
System.out.println(write);
//清理操作
if (!buffer.hasRemaining()) {
//清除附件
key.attach(null);
//取消write事件
key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
}
}
}
}
}
}
write 为何要取消
只要向 channel 发送数据时,socket 缓冲可写,这个事件会频繁触发,因此应当只在 socket 缓冲区写不下时再关注可写事件,数据写完之后再取消关注