使用Selector进行网络通信
在这里主要使用的是IO模型中的多路复用,想详细了解io模型可以看我上一篇
多路复用
单线程可以配合Selector完成多个Channel可读写事件的监控,这称之为多路复用
- 多路复用仅针对网络IO、普通文件IO没法利用多路复用
Selector事件
- accept :会在有连接请求时触发
SelectionKey.*OP_ACCEPT= 16* - connect :是客户端,连接建立后触发
SelectionKey.OP_CONNECT= 8 - read : 可读事件
SelectionKey.*OP_READ= 1* - write : 可写事件
SelectionKey.OP_WRITE= 4
1、Selector常用方法
Selector.open() :获取Selector 选择器
ssc.configureBlocking(false):配置通道为非阻塞SelectionKey sscKey = ssc.register(selector, 0, null);- 把ssc这个Channel注册到selector 上,并且表示关注事件为0(也可以添加要关注的事件),附件为null
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);- // 将套接字通过到注册到选择器,关注 read和 write事件
scKey.interestOps(SelectionKey.*OP_READ* | SelectionKey.*OP_WRITE*)//使用的是位运算符- 表示关注这两个事件,也可以使用 + 号代替,
scKey.attach(buffer);- 把buffer挂载到scKey这个事件上;
2、具体实现代码
简单获取事件
public static void main(String[] args) throws IOException {
// 1、创建selector,用于管理多个channel
Selector selector = Selector.open();
ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 2、建立selector 和 channel 的联系(注册)
// SelectionKey 就是将来用于管理时间发生后,通过它可以知道事件和哪个Channel的事件
SelectionKey sscKey = ssc.register(selector, 0, null);
// key 只关注accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key:{}"+sscKey);
ssc.bind(new InetSocketAddress(8080)); //连接本机端口
while (true){
// 3、select 方法,没有事件就阻塞,如果发生事件,线程恢复运行
// (如果不处理这个事件时,不会进行阻塞;只有处理了这个事件或者取消了这个事件才会进入阻塞)
selector.select();
// 4、处理事件,SelectionKey 内部包含了所有发生的事件
// 获取迭代器,进行遍历事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
log.debug("key:{}",key);
/* ServerSocketChannel channel = (ServerSocketChannel) key.channel();
// 处理这个事件
SocketChannel sc = channel.accept();
log.debug("{}",sc);*/
// 取消这个事件
key.cancel();
}
}
}
流程图

3、accept和read事件
监听Channel事件
可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少Channel发生了事件
方法1,阻塞直到绑定事件发生
selector.select();
方法2.阻塞直到绑定事件发生,或是超时(时间单位为ms)
selector.select(long timeout);
方法3.不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件
selector.selectNow();
🔔select 何时不阻塞
- 事件发生时
- 客户端发起连接请求,会触发accept事件
- 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于buffer 缓冲区,会触发多次读取事件
- channel 可写,会触发write事件
- 在linux下nio bug发生时
- 调用selector.wakeup()
- 调用selector.closel()
- selector 所在线程interrupt
- 使用Selector读取客户端数据
- 处理完一个事件需要把事件从
SelectionKey集合中删除掉,因为SelectionKey处理完这个事件后不会删除,只会做一个处理后的标记,就会一直循环执行这个已经处理过的事件(就会报错);需要手动的把这个事件从集合中remove()掉 - 当客户端错误断开连接时,服务器会发生一个read事件,
selector集合就会往SelectionKey集合中丢这个事件,然而因为客户端已经断开连接了,这个事件就无法处理成功,会报错,需要try catch 包住;并且需要把这个事件从selector集合中取消,避免一直往SelectionKey集合中丢;正常断开也是,执行channel.read(buffer1)会返回-1,也需要手动去取消这个事件。 - 如果传输中文字符时,如果缓冲区大小不够,出现半包时,就会出现消息边界问题
- 处理完一个事件需要把事件从
public static void main(String[] args) throws IOException {
// 1、创建selector,用于管理多个channel
Selector selector = Selector.open();
ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 2、建立selector 和 channel 的联系(注册)
// SelectionKey 就是将来用于管理事件发生后,通过它可以知道事件和哪个Channel的事件
SelectionKey sscKey = ssc.register(selector, 0, null);
// key 只关注accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key:{}"+sscKey);
ssc.bind(new InetSocketAddress(8080));
while (true){
// 3、select 方法,没有事件就阻塞,如果发生事件,线程恢复运行
// (如果不处理这个事件时,不会进行阻塞;只有处理了这个事件或者取消了这个事件才会进入阻塞)
selector.select();
// 4、处理事件,SelectionKey 内部包含了所有发生的事件
// 获取迭代器,进行遍历事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
// 当拿到key之后需要把key从sekectedkeys集合中删除掉,
// 否则下次还会继续处理这个被处理过的key,就会报错
iterator.remove();
log.debug("key:{}",key);
// 5、区分事件类型
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
// 处理这个事件
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, 0, null);
scKey.interestOps(SelectionKey.OP_READ);
log.debug("{}",sc);
log.debug("key{}",scKey);
}else if (key.isReadable()){ //如果是read
// 读取channel中的数据
try {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer1 = ByteBuffer.allocate(4);
int read = channel.read(buffer1); //如果客户端正常断开,read的返回值是-1;
if (read==-1){
key.cancel(); //取消这个事件,避免一直往集合中放
}
buffer1.flip();
System.out.println(Charset.defaultCharset().decode(buffer1)); //直接打印会出现消息边界的问题
} catch (IOException e) {
e.printStackTrace();
key.cancel(); //当客户端异常断开时,需要把这个key取消,(从selector集合中删除,避免一直往执行集合写入)
}
}
// 取消这个事件
// key.cancel();
}
}
}
}
4、处理消息边界
消息边界出现的原因
- ByteBufeer较小,但是消息比较大
- ByteBufeer较大,消息比较小。会出现半包现象
- ButeBuffer较小,但是容纳了多个消息。此时会出现黏包现象

处理方式
- 一种思路是固定消息长度,数据包一样,服务器按预定长度读取,缺点是浪费宽带
- 另一种是按分隔符拆分,缺点是效率低
- TLV格式,即Type类型、Length长度、Value数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的buffer,缺点是buffer需要提前分配,如果内容过大,则影响server吞吐量
- Http1.1是TLV格式:先传出类型,再传出长度,最后传出数据
- Http2.0是LTV格式:先传出长度,再传出类型,最后传出数据

如果一次传输的数据过长,第一次分隔符的长度就大于了缓冲区长度,则会出现下面的情况

代码举例
按固定分隔符解决消息边界
- 因为缓冲区不能够扩容,所以需要把缓冲区作为附件放入
SelectionKey中,当容量不够时,进行一次扩容,更新掉附件中的缓冲区
public class Server2 {
public static void main(String[] args) throws IOException {
// 1、创建selector,用于管理多个channel
Selector selector = Selector.open();
ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 2、建立selector 和 channel 的联系(注册)
// SelectionKey 就是将来用于管理事件发生后,通过它可以知道事件和哪个Channel的事件
SelectionKey sscKey = ssc.register(selector, 0, null);
// key 只关注accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
log.debug("register key:{}"+sscKey);
ssc.bind(new InetSocketAddress(8080));
while (true){
// 3、select 方法,没有事件就阻塞,如果发生事件,线程恢复运行
// (如果不处理这个事件时,不会进行阻塞;只有处理了这个事件或者取消了这个事件才会进入阻塞)
selector.select();
// 4、处理事件,SelectionKey 内部包含了所有发生的事件
// 获取迭代器,进行遍历事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
// 当拿到key之后需要把key从sekectedkeys集合中删除掉,
// 否则下次还会继续处理这个被处理过的key,就会报错
iterator.remove();
log.debug("key:{}",key);
// 5、区分事件类型
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
// 处理这个事件
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
ByteBuffer buffer2 = ByteBuffer.allocate(5);
// register(注册的selector,事件数量,附件)
SelectionKey scKey = sc.register(selector, 0, buffer2); //把buffer当作attachment(附件)放入SelectionKey中
scKey.interestOps(SelectionKey.OP_READ);
log.debug("{}",sc);
log.debug("key{}",scKey);
}else if (key.isReadable()){ //如果是read
// 读取channel中的数据
try {
SocketChannel channel = (SocketChannel) key.channel();
// 获取放入SelectionKey中关联的附件
ByteBuffer buffer1 = (ByteBuffer) key.attachment();
int read = channel.read(buffer1); //如果客户端正常断开,read的返回值是-1;
if (read==-1){
key.cancel(); //取消这个事件,避免一直往集合中放
}else {
System.out.println(buffer1);
split(buffer1);
if (buffer1.position()==buffer1.limit()){ //如果缓冲区的position和limit相等,则代表数据长度大于缓冲区,数据未读
ByteBuffer newBuffer = ByteBuffer.allocate(buffer1.capacity() * 2); //则把原来的容量进行扩容
newBuffer.flip();
newBuffer.put(buffer1); //把原来缓冲区的数据放入新缓冲区
key.attach(newBuffer); //再更新SelectionKey中的附件buffer
}
}
buffer1.flip();
System.out.println(Charset.defaultCharset().decode(buffer1)); //直接打印会出现消息边界的问题
} catch (IOException e) {
e.printStackTrace();
key.cancel(); //当客户端异常断开时,需要把这个key取消,(从selector集合中删除,避免一直往执行集合写入)
}
}
// 取消这个事件
// key.cancel();
}
}
}
private static void split(ByteBuffer buffer) {
buffer.flip();
for (int i = 0; i < buffer.limit(); i++) {
// 通过\n找到一条完整消息
if (buffer.get(i)=='\n'){
int length = i+1-buffer.position();
// 创建指定大小的缓冲区
ByteBuffer target = ByteBuffer.allocate(length);
// 把完整数据写入新的ByteBuffer中
for (int j = 0; j < length; j++) {
target.put(buffer.get());
}
target.flip();
// String s = StandardCharsets.UTF_8.decode(target).toString();
// System.out.println(s);
System.out.println(buffer);
}
}
buffer.compact();
}
}
本文介绍了JavaNIO中的Selector如何用于实现多路复用网络通信,包括Selector的基本操作、事件类型、关注事件的方法,以及处理accept和read事件的具体步骤。文章还讨论了消息边界问题及其解决方案,如固定消息长度、分隔符拆分和TLV格式。

1021

被折叠的 条评论
为什么被折叠?



