2023-09-23  阅读(0)
原文作者:李林超 原文地址: https://www.lilinchao.com/archives/2107.html

[TOC]

前言

  • 多路复用

单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用

  • 多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用

  • 如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证

    • 有可连接事件时才去连接

    • 有可读事件才去读取

    • 有可写事件才去写入

      • 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件

一、处理accept事件

  • 服务器端代码
    import lombok.extern.slf4j.Slf4j;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    
    /**
     * @author lilinchao
     * @date 2022/6/2
     * @description Accept事件
     **/
    @Slf4j
    public class AcceptServer {
        public static void main(String[] args) throws IOException {
            //1.创建Selector,可以管理多个channel
            Selector selector = Selector.open();
    
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.configureBlocking(false);
    
            //2.建立channel和selector之间的联系(注册)
            //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. selector.select()方法,没有事件就阻塞,有事件发送就恢复运行继续向下处理
                selector.select();
    
                //4.处理事件,selectionKeys内部包含了所有发生的事件
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
    
                while (iterator.hasNext()){
                    //注意,如果事件不调用accept进行处理,那么不会阻塞,因为事件没被处理,就不能阻塞
                    //也就是说事件要么处理要么取消,不能不管
                    SelectionKey key = iterator.next();
                    log.debug("key:{}",key);
                    //拿到触发事件的channel
                    ServerSocketChannel channel = (ServerSocketChannel)key.channel();
                    SocketChannel sc = channel.accept();
                    log.debug("{}",sc);
                }
            }
        }
    }
  • 客户端代码
    import java.io.IOException;
    import java.net.Socket;
    
    /**
     * @author lilinchao
     * @date 2022/6/2
     * @description 1.0
     **/
    public class AcceptClient {
        public static void main(String[] args) {
            try (Socket socket = new Socket("localhost", 8080)) {
                System.out.println(socket);
                socket.getOutputStream().write("world".getBytes());
                System.in.read();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
  • 运行

(1)启动服务端程序

(2)通过Debug模式启动客户端程序

(3)通过Debug模式再启动一个客户端程序

  • 客户端启动项,选择【Edit Configurations】

202309232223411091.png

  • 选择【Allow parallel run】,再点击【OK】

202309232223415712.png

  • 再通过Debug模式启动一个客户端
  • 服务端程序输出结果
    17:05:25.304 [main] DEBUG com.lilinchao.nio.accept.AcceptServer - register key:sun.nio.ch.SelectionKeyImpl@3b764bce
    17:05:38.141 [main] DEBUG com.lilinchao.nio.accept.AcceptServer - key:sun.nio.ch.SelectionKeyImpl@3b764bce
    17:05:38.142 [main] DEBUG com.lilinchao.nio.accept.AcceptServer - java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:51904]
    17:05:55.911 [main] DEBUG com.lilinchao.nio.accept.AcceptServer - key:sun.nio.ch.SelectionKeyImpl@3b764bce
    17:05:55.912 [main] DEBUG com.lilinchao.nio.accept.AcceptServer - java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:51919]

从打印结果可以看出有两个客户端向服务端发送了连接请求。

问题:事件发生后能否不处理?

事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发

二、处理read事件

  • 服务端代码
    import lombok.extern.slf4j.Slf4j;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    
    import static com.lilinchao.nio.util.ByteBufferUtil.debugAll;
    
    /**
     * @author lilinchao
     * @date 2022/6/2
     * @description Read事件 服务端
     **/
    @Slf4j
    public class ReadServer {
        public static void main(String[] args) throws IOException {
    
            //1.创建selector,管理多个channel
            Selector selector = Selector.open();
    
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.configureBlocking(false);
    
            //2. 建立channel和selector之间的联系(注册)
            SelectionKey sscKey = ssc.register(selector, 0, null);
            sscKey.interestOps(SelectionKey.OP_ACCEPT);
            log.debug("register key:{}",sscKey);
            ssc.bind(new InetSocketAddress(8080));
    
            while (true){
                //3. selector.select()方法,没有事件就阻塞,有了事件发送了就恢复运行继续向下处理
                selector.select();
    
                //4. 处理事件,selectionKeys拿到所有发生的可读可写的事件
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
    
                //多个key的时候,accept和read方法都会触发事件,所以要区分事件类型
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    //处理key的时候要从selectKeys中删除,否则会报错
                    iterator.remove();
                    log.debug("key:{}",key);
    
                    //5.区分事件类型
                    if(key.isAcceptable()){
                        //拿到触发事件的channel
                        ServerSocketChannel channel = (ServerSocketChannel)key.channel();
                        SocketChannel sc = channel.accept();
                        //设置为非阻塞
                        sc.configureBlocking(false);
                        //scKey管sc的channel
                        SelectionKey scKey = sc.register(selector, 0, null);
                        //scKey关注读事件,也就是说客户端的通道关注可读事件
                        scKey.interestOps(SelectionKey.OP_READ);
                        log.debug("{}",sc);
                    }else if(key.isReadable()){
                        //客户端关闭之后也会引发read事件,这时需要从key中remove掉,否则拿不到channel,报错
                        try {
                            SocketChannel channel = (SocketChannel)key.channel();
                            ByteBuffer buffer1 = ByteBuffer.allocate(16);
                            //客户端正常断开,read返回值是-1
                            int read = channel.read(buffer1);
                            if(read == -1){
                                //正常断开
                                key.channel();
                            }
                            buffer1.flip();
                            debugAll(buffer1);
                        } catch (IOException e) {
                            e.printStackTrace();
                            key.cancel();//客户端断开,需要将key取消(从selector的key集合中真正删除)
                        }
                    }
    
                }
    
            }
    
        }
    }
  • 客户端代码
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.net.SocketAddress;
    import java.nio.channels.SocketChannel;
    
    /**
     * @author lilinchao
     * @date 2022/6/2
     * @description Read事件 客户端
     **/
    public class ReadClient {
        public static void main(String[] args) throws IOException {
            SocketChannel sc = SocketChannel.open();
            sc.connect(new InetSocketAddress("localhost", 8080));
    
            SocketAddress localAddress = sc.getLocalAddress();
            System.out.println("waiting...");
        }
    }
  • 运行

(1)启动服务端程序

(2)Debug启动客户端程序

  • 选择【Evalute Expression】

202309232223420203.png

  • 输入如下内容
    sc.write(Charset.defaultCharset().encode("hello!"));

202309232223425584.png

  • 点击【Evaluate】提交
  • 服务端输出结果
    17:20:50.713 [main] DEBUG com.lilinchao.nio.read.ReadServer - register key:sun.nio.ch.SelectionKeyImpl@3b764bce
    17:21:11.373 [main] DEBUG com.lilinchao.nio.read.ReadServer - key:sun.nio.ch.SelectionKeyImpl@3b764bce
    17:21:11.374 [main] DEBUG com.lilinchao.nio.read.ReadServer - java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:52466]
    17:23:48.603 [main] DEBUG com.lilinchao.nio.read.ReadServer - key:sun.nio.ch.SelectionKeyImpl@368102c8
    17:23:48.653 [main] DEBUG io.netty.util.internal.logging.InternalLoggerFactory - Using SLF4J as the default logging framework
    +--------+-------------------- all ------------------------+----------------+
    position: [0], limit: [6]
             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
    +--------+-------------------------------------------------+----------------+
    |00000000| 68 65 6c 6c 6f 21 00 00 00 00 00 00 00 00 00 00 |hello!..........|
    +--------+-------------------------------------------------+----------------+

服务端监听到客户端的连接,并读取打印客户端发送过来的数据。

问题:为何要 iter.remove()

因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如 > > * 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey > * 第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常

问题:cancel 的作用

cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文