上节使用通道channel模拟了客户端给服务器端发送消息。代码是基于同步阻塞式IO。这样效率很低,而且一个服务端进程只能处理一个客户端连接。这节学习Selector选择器。可以让一个服务端处理多个客户端的连接,性能有了较大的提升。
一、Selector选择器简介
Selector是NIO中另外一个重要的实现。选择器是Java对五种IO模型之一的多路复用模型的一中实现。可以同时监控多个非阻塞套接字通道。
1、选择键SelectKey
选择键包含四种事件,通过选择键,可以选择器发现我们要选择的事件,分别是:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
事件之间也可以通过或运算进行组合。
通过以下几个方法可以判断事件是否就绪。我们通过Selector进行轮询,查看我们需要的事件是否准备好。
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
2、通道注册
将我们感兴趣的事情告知Selector,待事件发生时,Seletor就可返回就绪的事件,然后执行后续的事情。
// 切换成非阻塞模式
serverSocketChannel.configureBlocking(false);
// 将通道注册到选择器上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
3、选择过程
Selector包含3中不同功能的选择方法
- int select() 这是一个阻塞方法,至少有一个通道处于就绪状态时才返回
- int select(long timeout) 同样也是一个阻塞方法,不过可对该方法设置超时时间,使得线程不会一致被阻塞
- int selectNow() 非阻塞方法,调用后立刻返回
二、实例代理以及演示
服务端的代码比较固定,参考下面注释,可以很好的理解。这里需要注意当每次结束内循环后,需要将本次轮询获得到的所有选择键移除掉,以便重新开始新的轮询。
/**
* @author Time
* @created 2019/8/28
*/
public class TcpSelectServer {
public static void main(String[] args) throws IOException {
// 1. 获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2. 切换成非阻塞模式
serverSocketChannel.configureBlocking(false);
// 3.绑定连接
serverSocketChannel.bind(new InetSocketAddress(8080));
// 4.获取选择器
Selector selector = Selector.open();
// 5.将通道注册到选择器上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6. 轮询式获取选择器上已经准备就绪的通道
while(selector.select() > 0 ){
// 7.获取当前选择器中所有的注册事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
// 获取准备就绪的事件
SelectionKey sk = iterator.next();
// 判断具体是什么事件
if(sk.isAcceptable()){
// 获取客户端非阻塞
SocketChannel s = serverSocketChannel.accept();
// 切换非阻塞模式
s.configureBlocking(false);
// 将该通道注册到选择器上
s.register(selector,SelectionKey.OP_READ);
}else if (sk.isReadable()){
// 获取当前选择器上读就绪通道
SocketChannel socketChannel = (SocketChannel) sk.channel();
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(128);
int len = 0;
while((len = socketChannel.read(buffer)) > 0){
buffer.flip();
System.out.println(new String(buffer.array(),0,len));
buffer.clear();
}
}
// 取消选择键
iterator.remove();
}
}
}
}
客户端代码
/**
* @author Time
* @created 2019/8/28
* 非阻塞式客户端
*/
public class TcpNonBlockingClient01 {
public static void main(String[] args) throws IOException {
// 1. 获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost",8080));
// 2.切换成非阻塞模式
socketChannel.configureBlocking(false);
// 3.分配指定大小的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(128);
// 4.发送数据给服务器
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String str = scanner.nextLine();
buffer.put((new Date().toString() + "\n" + str).getBytes());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
// 5. 关闭通道
socketChannel.close();
}
}
效果演示:可以发现现在一个服务端进程可以处理多个连接