• NIO Netty(三)


    1、网络编程

    1、阻塞
    • 阻塞模式下,相关方法都会导致线程暂停
      • ServerSocketChannel.accept 会在没有连接建立时让线程暂停
      • SocketChannel.read 会在没有数据可读时让线程暂停
      • 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
    • 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
    • 但多线程下,有新的问题,体现在以下方面
      • 32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM(OutOfMemory),并且线程太多,反而会因为频繁上下文切换导致性能降低
      • 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接
    2、非阻塞
    • 非阻塞模式下,相关方法都会不会让线程暂停
      • 在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行
      • SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept
      • 写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去
    • 但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu
    • 数据复制过程中,线程实际还是阻塞的(AIO 改进的地方)
    3、多路复用

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

    • 多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用
    • 如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
      • 有可连接事件时才去连接
      • 有可读事件才去读取
      • 有可写事件才去写入
        • 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件

    4、selector

    selector 版
    selector
    thread
    channel
    channel
    channel

    好处

    • 一个线程配合 selector 就可以监控多个 channel 的事件,事件发生线程才去处理。避免非阻塞模式下所做无用功
    • 让这个线程能够被充分利用
    • 节约了线程的数量
    • 减少了线程上下文切换
    客户端代码

    开并行,多个客户端测试服务端

    package com.jtc.test;
    
    import java.io.IOException;
    import java.net.Socket;
    
    public class Client1 {
        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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    服务端代码

    监听accept与read事件并处理

    package com.jtc.test;
    
    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 java.util.Set;
    
    import static com.jtc.Utils.ByteBufferUtil.debugAll;
    
    @Slf4j
    public class Server1 {
        public static void main(String[] args) {
            try (ServerSocketChannel channel = ServerSocketChannel.open()) {
                channel.bind(new InetSocketAddress(8080));
                System.out.println(channel);
                log.debug("sssssss");
    
                Selector selector = Selector.open();
                //注册事件,绑定的事件 selector 才会关心
                /*channel 必须工作在非阻塞模式
                        * FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用
                        * 绑定的事件类型可以有
                            * connect - 客户端连接成功时触发
                            * accept - 服务器端成功接受连接时触发
                            * read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
                            * write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况*/
                channel.configureBlocking(false);
                channel.register(selector, SelectionKey.OP_ACCEPT);
    
                while (true) {
                    //监听 Channel 事件
                    //方法1,阻塞直到绑定事件发生
                    int count = selector.select();
                    //方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)
                    //int count = selector.select(long timeout);
                    //方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件
                    //int count = selector.selectNow();
                    log.debug("select count: {}", count);
    
                    // 获取所有事件
                    Set<SelectionKey> keys = selector.selectedKeys();
    
                    // 遍历所有事件,逐一处理
                    Iterator<SelectionKey> iter = keys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        // 判断事件类型
                        if (key.isAcceptable()) {
                            ServerSocketChannel c = (ServerSocketChannel) key.channel();
                            // 必须处理
                            SocketChannel sc = c.accept();
                            sc.configureBlocking(false);
                            sc.register(selector, SelectionKey.OP_READ);
                            log.debug("连接已建立: {}", sc);
                        } else if (key.isReadable()) {
                            SocketChannel sc = (SocketChannel) key.channel();
                            ByteBuffer buffer = ByteBuffer.allocate(128);
                            int read = sc.read(buffer);
                            if(read == -1) {
                                //cancel 会取消注册在 selector 上的 channel,
                                //并从 keys 集合中删除 key 后续不会再监听事件
                                key.cancel();
                                sc.close();
                            } else {
                                buffer.flip();
                                debugAll(buffer);
                            }
                        }
                        // 处理完毕,必须将事件移除
                        iter.remove();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    为什么要iter.remove():

    因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如

    • 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
    • 第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常
  • 相关阅读:
    Java一次返回中国所有省市区三级树形级联+前端vue展示【200ms内】
    STM32F103 CAN通讯实操
    IaC实战指南:DevOps的自动化基石
    Notepad++ 和正则表达式 只保留自己想要的内容
    佛山复星禅诚医院黄汉森:云边协同,打造线上线下一体化智慧医疗
    行人属性识别二:添加新网络训练和自定义数据集训练
    运动排行榜日行9万步,背后原来是模拟器作弊
    OPPOWatch3什么时候发布 OPPOWatch3配置如何
    某百亿头部量化私募企业在招岗位:AI算法工程师年base60-200万+bonus+cut965制度“岗位职责:base 北京 上海 杭州 深圳
    彻底搞懂JavaScript原型和原型链
  • 原文地址:https://blog.csdn.net/qq_42900213/article/details/126533065