概述
- Selector是NIO中实现I/O多路复用的关键类。Selector 实现了通过一个线程管理多个Channel,从而管理多个网络连接的目的。
- 要使用Selector,得向Selector注册Channel,然后调用它的select() 方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。
流程
Selector
- Selector 能够在单个线程中处理多个通道,这样可以减少多个线程造成上下文切换问题。Channel 需要注册到Selectors 上,一个Selectors 可以注册多个 Channel
- Channel 注册到Selector 后会返回一个SelectionKey 对象,因为可注册多个Channel,返回的是一个 SelectorKey 集合(select()方法返回)
- SelectionKey 将注册测Channel 与 Selectors 建立关系,通过 SelectorKey 可返现获取通道和事件信息。SelectionKey 维护着两个很重要的属性:
interestOps、readyOps
。
interestOps 记录Selectors 监听通道的哪些事件,感兴趣的事件设置到该字段,这样在selection 操作时,当发现该Channel 有感兴趣的事件发生时,就会将感兴趣的事件再设置到
readyOps
中,这样 Selectors 就能得知是哪些事件发生了以做相应处理。
事件
为了实现Selector 管理多个SocketChannel,必须将具体的SocketChannel 对象注册到Selector,并声明需要监听的事件(这样Selector 才知道需要记录什么数据),一共有4种事件:
- connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8)
- accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16)
- read:读事件,对应值为SelectionKey.OP_READ(1)
- write:写事件,对应值为SelectionKey.OP_WRITE(4)
每次请求到达服务器,都是从connect开始,connect 成功后,服务端开始准备accept,准备就绪,开始读数据,并处理,最后写回数据返回。
Selector 服务器端原理
- 创建ServerSocketChannel 实例,并绑定制定端口
- 创建Selector 实例
- 将serverSocketChannel 注册到Selector,并指定事件OP_ACCEPT, 最底层的socket 通过channel 和selector 建立关联
- 如果没有准备号的socket,select 方法会被阻塞一段时间并返回0
- 如果底层有socket 已经准备好,selecto r的select 方法会返回socket 的个数,而且selectedKeys 方法会返回socket 对应的事件(connect、accept、read or write)
- 根据不同的事件类型,进行不同的处理逻辑
- 服务器端实现
public static void main(String[] args) throws Exception {
//创建serversocketchannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//selector
Selector selector = Selector.open();
//绑定端口,在服务器监听
serverSocketChannel.socket().bind(new InetSocketAddress(9527));
//设置非阻塞
serverSocketChannel.configureBlocking(false);
//serverSocketChannel 注册到selector,关心事件为 连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//循环等客户端连接
while (true) {
//等待1秒,如果没有事件发生,返回
int select = selector.select(1000);
if (select == 0) {
System.out.println("服务器等待1秒,无连接");
continue;
}
/**
如果返回非0,获取关注的 selectionKeys 集合
* 1.果果kes>0 获取到了练级的key
* 2.通过 selectionKeys 反向获取通道
*/
Set<SelectionKey> selectionKeys = selector.selectedKeys();
if (selectionKeys.size() > 0) {
Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
while (selectionKeyIterator.hasNext()) {
//获取selectKey
SelectionKey key = selectionKeyIterator.next();
//根据key 获取响应的通道,判断发生的事件
if (key.isAcceptable()) {
System.out.println("有新的客户端连接...");
//为该链接生成socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//设置非阻塞
socketChannel.configureBlocking(false);
//当前的socketChannel 注册到selector, 关注事件为read, socketChannel 关联一个buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
//读操作
if (key.isReadable()) {
//通过key 反向获取SocketChannel
SocketChannel socketChannel = (SocketChannel) key.channel();
//获取socketChannel 关联的 bytebuff
ByteBuffer buffer = (ByteBuffer) key.attachment();
//读取buffer 到channel
int read = socketChannel.read(buffer);
System.out.println("客户端读取数据: "+new String(buffer.array()));
}
//删除当前key ,方式重读读写
selectionKeyIterator.remove();
}
}
}
}
Selector 客户端原理
- 客户端通过 SocketChannel 创建一个连接,客户端未与服务器建立连接时,不会阻塞,仍可以做其他事情
- 如果客户端发送数据,会触发read 事件,这样下次轮询调用select 方法时,就能通过socketChannel 读取数据,同时在selector上注册该socketChannel 的OP_WRITE事件,实现服务器往客户端写数据
public class NIOClient {
public static void main(String[] args) throws Exception {
//创建一个客户端连接
SocketChannel socketChannel = SocketChannel.open();
//建立与服务连接,与服务器监听端口一致
InetSocketAddress inetSocketAddress = new InetSocketAddress("1270.0.0.1", 9527);
//未连接到服务器
while (!socketChannel.connect(inetSocketAddress)){
if(!socketChannel.finishConnect()){
System.out.println("未连接值服务器,不阻塞,可以去做其它事情...");
}
}
//连接到服务器,发送消息
String str = "nio client test...";
socketChannel.write(ByteBuffer.wrap(str.getBytes()));
//关闭连接
socketChannel.close();
}
}
NIO中实现非阻塞IO的核心设计Selector,Selector就是注册各种IO事件的地方,而且当那些事件发生时,就是这个对象告诉我们所发生的事件。