文章目录
- 回顾一下
- 一、Selector 是什么?
- 二、代码示例
- 1.打开一个ServerSocketChannel
- 2.选择select
- 3.处理事件集合
- 3.NIO 编程步骤总结
- 补充
- 小结
回顾一下
之前总结过NIO组件之一 Channel,我们可以通过它来与客户端建立连接,并且设置为非阻塞模式,这样虽然解决了使用BIO可能内存爆掉的问题,但是当客户端只是建立连接,没有数据收发时,如果去轮询每个客户端,可能会造成性能的浪费,这样的客户端占用越多,很有可能导致服务端cpu会飙升。
那我们如何优化这一问题呢?聪明的同学就会想到,让channel只遍历那些有数据收发的客户端不就行了吗?对,NIO另一大重要组件Selector多路复用器由此出现。
一、Selector 是什么?
我们先来看一下Selector处理Channe图示:
Selector 一般被称为选择器 ,也可以翻译为多路复用器 。它是 Java NIO 核心组件中的一个,可以同时监听多个服务器端口,用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。由此可以实现单线程管理多个 channels,也就是可以管理多个网络连接。
二、代码示例
1.打开一个ServerSocketChannel
让其绑定9090端口,设置阻塞方式为非阻塞。
创建一个Selector对象,并且将ServerSocketChannel注册到Selector上,并且监听连接事件。
这里的SelectionKey.OP_ACCEPT是一个IO事件,表示服务器监听到了客户连接,那么服务器可以接收这个连接了,还有一些其他事件:
- 读就绪: SelectionKey.OP_READ
- 写就绪: SelectionKey.OP_WRITE
- 连接就绪: SelectionKey.OP_CONNECT(表示客户与服务器的连接已经建立成功)
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9090));
serverSocketChannel.configureBlocking(false);
//打开Selector处理Channel
Selector selector = Selector.open();
//把ServerSocketChannel注册到Selector上,并且selector监听accept连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
2.选择select
selector.select()使第一个尚未返回的选择操作立即返回,如果当前没有事件发生,则会阻塞。其源码是在linux系统上使用epoll系统调用来实现的。所以,Java语言的NIO组件所使用的就是IO多路复用模型。本质上select/epoll系统调用是阻塞式的,这也是为什么他会阻塞等待的原因。
while(true){
//它会阻塞等待需要处理的事件发生
selector.select();
//获取selector感知到的事件集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
3.处理事件集合
对应上面的四个事件,分别有isAcceptable(),isReadable(),isWriteable()和isConnectable(),事件发生即为true,然后创建SocketChannel,就可执行具体业务操作了,下面示例为通过ByteBuffer进行读数据。
//遍历SelectionKey,对事件进行处理
while(iterator.hasNext()){
SelectionKey key = iterator.next();
//如果是OP_ACCEPT连接事件。则进行连接获取和事件注册
if(key.isAcceptable()){
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
//注册读事件
socketChannel.register(selector, SelectionKey.OP_READ);
}else if(key.isReadable()){
//如果是OP_READ读事件。则进行读取
SocketChannel server = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(32);
int read = server.read(byteBuffer);
if(read > 0){
System.out.println("接收到消息:" + new String( byteBuffer.array() ));
} else if(read == -1){//如果客户端断开连接,关闭socket
iterator.remove();
server.close();
}
//从事件集合中删除本次处理的key,防止下次重复处理
iterator.remove();
}
}
3.NIO 编程步骤总结
先创建 ServerSocketChannel 通道,然后创建 Selector 选择器,并绑定监听端口;设置Channel 通道是非阻塞模式,把 Channel 注册到 Socketor 选择器上,监听连接事件;调用Selector 的 select 方法(循环调用),监测通道的就绪状况,然后调用 selectKeys 方法获取就绪 channel 集合;遍历就绪 channel 集合,判断就绪事件类型,实现具体的业务操作最后根据业务,决定是否需要再次注册监听事件,重复执行上面操作。
补充
为了避免每次循环结束后手动关闭通道,我们可以使用java7的try-with-resource进行自动关闭资源。
ByteBuffer buffer = ByteBuffer.allocate(32);
// 通过try-with-resource
try(ServerSocketChannel server = ServerSocketChannel.open()) {
// 为服务器通道绑定端口
server.bind(new InetSocketAddress(9090));
//...
} catch (IOException e) {
e.printStackTrace();
}
小结
高性能的Java通信离不开Java NIO组件,现在主流的技术框架或中间件服务器都使用了Java NIO组件,譬如Tomcat、Jetty、Netty。不管是面试还是实际开发,作为Java工程师,都必须掌握NIO的原理和开发实战技能。