Java NIO之Selector多路复用器


回顾一下

之前总结过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事件,表示服务器监听到了客户连接,那么服务器可以接收这个连接了,还有一些其他事件:

  1. 读就绪: SelectionKey.OP_READ
  2. 写就绪: SelectionKey.OP_WRITE
  3. 连接就绪: 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的原理和开发实战技能。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值