NIO笔记04-网络编程

news2024/11/14 22:05:45

文章目录

  • 1 非阻塞 vs 阻塞 ★★★
    • 阻塞
    • 非阻塞
    • 多路复用
  • 2 Selector
    • 创建
    • 绑定 Channel 事件
    • 监听 Channel 事件
      • 💡 select 何时不阻塞
  • 3 处理 accept 事件
    • 💡 事件发生后能否不处理
  • 4 处理 read 事件
    • 处理添加区分事件类型,客户端发送数据时会报空指针问题
      • 代码运行解析
      • 💡 为何要 iter.remove()
      • 💡 cancel 的作用
    • 处理客户端关闭连接导致服务器端报错问题
      • 1.客户端非正常断开(突然宕机)
      • 2.客户端正常断开(主动调用了SocketChannel.close()方法)
    • 消息边界问题(发送的消息长度大于ByteBuffer.allocate()设置的长度)
    • ByteBuffer 大小分配(ByteBuffer扩容)
  • 5 处理 write 事件
    • 一次无法写完例子
    • 💡 write 为何要取消
  • 6 多线程优化
    • 💡 如何拿到 cpu 个数
  • 7 UDP


1 非阻塞 vs 阻塞 ★★★

阻塞

  • 阻塞模式下,相关方法都会导致线程暂停
    • ServerSocketChannel.accept 会在没有连接建立时让线程暂停
    • SocketChannel.read 会在没有数据可读时让线程暂停
    • 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
  • 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
  • 但多线程下,有新的问题,体现在以下方面
    • 32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
    • 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接

代码示例:
服务器端

public static void main(String[] args) throws IOException {
    // 使用 nio 来理解阻塞模式, 单线程
    // 0. ByteBuffer
    ByteBuffer buffer = ByteBuffer.allocate(16);
    // 1. 创建了服务器
    ServerSocketChannel ssc = ServerSocketChannel.open();

    // 2. 绑定监听端口,便于客户端建立连接发送数据给服务器。
    ssc.bind(new InetSocketAddress(8080));

    // 3. 连接集合
    List<SocketChannel> channels = new ArrayList<>();
    while (true) {
        // 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信
        log.debug("connecting...");
        //Channel是数据读写的通道
        /**
         * 该函数用于在给定的channel上接受一个连接请求,返回一个新的SocketChannel对象,该对象表示与客户端的连接。具体来说:
         * channel.accept()是一个阻塞操作,它会一直等待直到有客户端发起连接请求。
         * 当有客户端连接时,服务器端会返回一个新的SocketChannel对象sc,该对象与客户端的连接相关联。
         * 通过SocketChannel对象,可以进行读写操作,与客户端进行数据传输。
         * SocketChannel还可以用于配置连接的参数,如设置非阻塞模式、选择器等。
         * 总之,SocketChannel sc = channel.accept()是服务器端用于接受客户端连接请求的关键代码,通过它建立与客户端的连接,并进行后续的数据传输操作
         */
        SocketChannel sc = ssc.accept(); // accept()会阻塞方法,线程进入堵塞状态
        log.debug("connected... {}", sc);
        channels.add(sc);
        for (SocketChannel channel : channels) {
            // 5. 接收客户端发送的数据
            log.debug("before read... {}", channel);
            channel.read(buffer); // 阻塞方法,线程进入堵塞状态,等待客户端发送数据
            buffer.flip();
            debugRead(buffer);
            buffer.clear();
            log.debug("after read...{}", channel);
        }
    }
}

以上代码还需要改进,因为如果在客户端长时间不发生数据时,还是会不断进行循环,造成cpu资源的浪费。

客户端

    public static void main(String[] args) throws IOException {
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost", 8080));
        SocketAddress address = sc.getLocalAddress();
        System.out.println("waiting...");
//        sc.close();
    }

非阻塞

  • 非阻塞模式下,相关方法都会不会让线程暂停
    • 在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行
    • SocketChannel.read 在没有数据可读时会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行ServerSocketChannel.accept
    • 写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去
  • 但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu
  • 数据复制过程中,线程实际还是阻塞的(AIO 改进的地方)

代码示例:

public static void main(String[] args) throws IOException {
       // 1. 创建 selector, 管理多个 channel
       Selector selector = Selector.open();
       ServerSocketChannel ssc = ServerSocketChannel.open();
       ssc.configureBlocking(false);
       // 2. 建立 selector 和 channel 的联系(注册)
       // SelectionKey:将来事件发生后,通过它可以知道是哪个channel的事件
       SelectionKey sscKey = ssc.register(selector, 0, null);
       // SelectionKey.OP_ACCEPT:key 只关注 accept 事件
       // ssc.register()中第二个入参为0表示不关注任何事件
       sscKey.interestOps(SelectionKey.OP_ACCEPT);
       log.debug("sscKey:{}", sscKey);
       ssc.bind(new InetSocketAddress(8080));
       while (true) {
           // 3. select()方法:没有事件发生,线程阻塞。有事件,线程才会恢复运行
           // select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
           // 如:不进行channel.accept()操作,则一直为未处理事件,线程会一直运行不阻塞
           selector.select();
           // 4. 处理事件, selectedKeys 内部包含了所有发生的事件
           // selector.selectedKeys()拿到所有发生的读写连接的事件集合
           Iterator<SelectionKey> iter = selector.selectedKeys().iterator();// accept, read
           while (iter.hasNext()) {
               //其实就是ServerSocketChannel.register()注册的SelectionKey
               SelectionKey key = iter.next();
               // 处理key 时,要从 selectedKeys 集合中删除,否则下次处理就会有问题
               iter.remove();
               log.debug("key: {}", key);
               ServerSocketChannel channel = (ServerSocketChannel) key.channel();
               SocketChannel sc = channel.accept();
//                key.cancel();//取消事件(将未处理的事件取消,不让事件未处理导致线程一直运行)
               sc.configureBlocking(false);//设置为非阻塞,配置Selector使用

               //将SocketChannel注册到Selector
               SelectionKey scKey = sc.register(selector, 0, null);
               scKey.interestOps(SelectionKey.OP_READ);
               log.debug("{}", sc);
               log.debug("scKey:{}", scKey);
           }
       }
   }

多路复用

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

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

2 Selector

在这里插入图片描述
使用selector的好处

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

创建

Selector selector = Selector.open();

绑定 Channel 事件

也称之为注册事件,绑定的事件 selector 才会关注

channel.configureBlocking(false);//将 channel 设置为非阻塞模式。这意味着后续对该通道的操作(如读写操作)不会阻塞当前线程。
SelectionKey key = channel.register(selector, 绑定事件);
  • channel 必须工作在非阻塞模式
  • FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用
  • 绑定的事件类型可以有
    • connect - 客户端连接成功时触发
    • accept - 服务器端成功接受连接时触发
    • read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
    • write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况

selector管理多个channel,并且它能管理监测这些 channel上有没有这些事件(连接事件、可读事件等),然后线程会获取并处理这些事件。

监听 Channel 事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件

方法1,阻塞直到绑定事件发生

int count = selector.select();

方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)

int count = selector.select(long timeout);

方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

int count = selector.selectNow();

💡 select 何时不阻塞

  1. 事件发生时
    • 客户端发起连接请求,会触发 accept 事件
    • 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件
    • channel 可写,会触发 write 事件
    • 在 linux 下 nio bug 发生时
  2. 调用 selector.wakeup()
  3. 调用 selector.close()
  4. selector 所在线程 interrupt

3 处理 accept 事件

代码示例:
服务器端

@Slf4j
public class ChannelDemo {
    public static void main(String[] args) {
        try (ServerSocketChannel channel = ServerSocketChannel.open()) {
            channel.bind(new InetSocketAddress(8080));
            System.out.println(channel);
            Selector selector = Selector.open();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                int count = selector.select();
//                int count = selector.selectNow();
                log.debug("select count: {}", count);
//                if(count <= 0) {
//                    continue;
//                }

                // 获取所有事件
                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();
                        log.debug("{}", sc);
                    }
                    // 处理完毕,必须将事件移除
                    iter.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端

public class Client {
    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();
        }
    }
}

💡 事件发生后能否不处理

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

4 处理 read 事件

处理添加区分事件类型,客户端发送数据时会报空指针问题

手动删除SelectionKey中的处理过的事件元素

代码示例:

   public static void main(String[] args) throws IOException {
        // 1. 创建 selector, 管理多个 channel
        Selector selector = Selector.open();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        // 2. 建立 selector 和 channel 的联系(注册)
        // SelectionKey:将来事件发生后,通过它可以知道是哪个channel的事件
        SelectionKey sscKey = ssc.register(selector, 0, null);
        // SelectionKey.OP_ACCEPT:key 只关注 accept 事件
        // ssc.register()中第二个入参为0表示不关注任何事件
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("sscKey:{}", sscKey);
        ssc.bind(new InetSocketAddress(8080));
        while (true) {
            // 3. select 方法:没有事件发生,线程阻塞。有事件,线程才会恢复运行
            // select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
            // 如:不进行channel.accept()操作,则一直为未处理事件,线程会一直运行不阻塞
            selector.select();
            // 4. 处理事件, selectedKeys 内部包含了所有发生的事件
            // selector.selectedKeys()拿到所有发生的读写连接的事件集合
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
            while (iter.hasNext()) {
                //其实就是ServerSocketChannel.register()注册的SelectionKey
                SelectionKey key = iter.next();
                // 处理key 时,要从 selectedKeys 集合中删除,否则下次处理就会有问题
//                iter.remove();
                log.debug("key: {}", key);
                // 5. 区分事件类型
                if (key.isAcceptable()) { // 如果是 accept (新的客户端连接)
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();//执行后并不会删除该SelectionKey,而是修改状态为已处理
                    sc.configureBlocking(false);

                    SelectionKey scKey = sc.register(selector, 0, null);
                    scKey.interestOps(SelectionKey.OP_READ);
                    log.debug("{}", sc);
                    log.debug("scKey:{}", scKey);
                } else if (key.isReadable()) { // 如果是 read(客户端发送数据)
                    SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel
                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    channel.read(buffer); // 如果是正常断开,read 的方法的返回值是 -1
                    buffer.flip();
                    debugAll(buffer);
                }
            }
        }
    }

代码运行解析

在这里插入图片描述

  1. 首先当调用ServerSocketChannel.register()时,会创建SelectionKey注册到selector集合中。
  2. 在SelectionKey.interestOps(SelectionKey.OP_ACCEPT)中让key关注accept事件,并且管理的是该ServerSocketChannel上的事件
  3. 向下执行到selector.select()因为没有事件所以进行阻塞。
  4. 当发现新的事件时会创建一个新的集合selectedKeys(发生事件的key会被填充到selectedKeys中)
    注意:selectedKeys只会向其中添加元素,并不会主动的删除元素
  5. 当事件被处理(如使用了ServerSocketChannel.accept()),执行后并不会删除该SelectionKey,而是修改状态为已处理
  6. 当调用SocketChannel.register()时,注册到selector集合中。
  7. 当调用SelectionKey.interestOps(SelectionKey.OP_READ)关注read事件,并且管理的是该SocketChannel上的事件
  8. 此时客户端发送数据时会将事件加入到selectedKeys中,并且会在selectedKeys中拿去所以的元素
  9. 此时因为刚刚客户端的连接,存储在selectedKeys的元素没有被删除,所以就被取出来了。
  10. 但是又因为被处理过了,所以此时调用SelectionKey.channel().accept()返回的为空,所以会报错空指针
  11. 所以要手动删除key(Selector.selectedKeys().iterator().remove())

💡 为何要 iter.remove()

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

  • 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
  • 第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常

💡 cancel 的作用

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

处理客户端关闭连接导致服务器端报错问题

1.客户端非正常断开(突然宕机)

使用try catch抓住因为客户端断开导致的服务器端的报错异常,使用SelectionKey.cancel();进行删除。

try {
    SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel
    ByteBuffer buffer = ByteBuffer.allocate(4);
    int read = channel.read(buffer);
    // 如果客户端是正常断开,read 的方法的返回值是 -1
    if(read == -1) {
        key.cancel();
    } else {
        buffer.flip();
        System.out.println(Charset.defaultCharset().decode(buffer));
    }
} catch (IOException e) {
    e.printStackTrace();
    // 处理客户端断开了,因此需要将 key 取消(从 selector 的 keys 集合中真正删除 key)
    key.cancel();
}

2.客户端正常断开(主动调用了SocketChannel.close()方法)

    public static void main(String[] args) throws IOException {
        // 1. 创建 selector, 管理多个 channel
        Selector selector = Selector.open();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        // 2. 建立 selector 和 channel 的联系(注册)
        // SelectionKey:将来事件发生后,通过它可以知道是哪个channel的事件
        SelectionKey sscKey = ssc.register(selector, 0, null);
        // SelectionKey.OP_ACCEPT:key 只关注 accept 事件
        // ssc.register()中第二个入参为0表示不关注任何事件
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("sscKey:{}", sscKey);
        ssc.bind(new InetSocketAddress(8080));
        while (true) {
            // 3. select 方法:没有事件发生,线程阻塞。有事件,线程才会恢复运行
            // select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
            // 如:不进行channel.accept()操作,则一直为未处理事件,线程会一直运行不阻塞
            selector.select();
            // 4. 处理事件, selectedKeys 内部包含了所有发生的事件
            // selector.selectedKeys()拿到所有发生的读写连接的事件集合
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();// accept, read
            while (iter.hasNext()) {
                //其实就是ServerSocketChannel.register()注册的SelectionKey
                SelectionKey key = iter.next();
                // 处理key 时,要从 selectedKeys 集合中删除,否则下次处理就会有问题
                iter.remove();
                log.debug("key: {}", key);
                // 5. 区分事件类型
                if (key.isAcceptable()) { // 如果是 accept (新的客户端连接)
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();//执行后并不会删除该SelectionKey,而是修改状态为已处理
                    sc.configureBlocking(false);

                    SelectionKey scKey = sc.register(selector, 0, null);
                    scKey.interestOps(SelectionKey.OP_READ);
                    log.debug("{}", sc);
                    log.debug("scKey:{}", scKey);
                } else if (key.isReadable()) { // 如果是 read(客户端发送数据)
                    try {
                        SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel
                        ByteBuffer buffer = ByteBuffer.allocate(4);
                        int read = channel.read(buffer);
                        // 如果客户端是正常断开,read 的方法的返回值是 -1
                        if(read == -1) {
                            key.cancel();
                        } else {
                            buffer.flip();
//                            debugAll(buffer);
                            System.out.println(Charset.defaultCharset().decode(buffer));
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        // 处理客户端断开了,因此需要将 key 取消(从 selector 的 keys 集合中真正删除 key)
                        key.cancel();
                    }
                }
            }
        }
    }

消息边界问题(发送的消息长度大于ByteBuffer.allocate()设置的长度)

在这里插入图片描述

  1. 一种思路是固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽
  2. 另一种思路是按分隔符拆分,缺点是效率低
  3. 使用TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量
    • Http 1.1 是 TLV 格式
    • Http 2.0 是 LTV 格式

在这里插入图片描述
因为发送的消息长度大于ByteBuffer.allocate()设置的长度,所以会分多次读取。代码如下:

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket ss=new ServerSocket(9000);
        while (true) {
            Socket s = ss.accept();
            InputStream in = s.getInputStream();
            byte[] arr = new byte[4];
            while(true) {
                int read = in.read(arr);
                if(read == -1) {
                    break;
                }
                System.out.println(new String(arr, 0, read));
            }
        }
    }
}
public class Client {
    public static void main(String[] args) throws IOException {
        Socket max = new Socket("localhost", 9000);
        OutputStream out = max.getOutputStream();
        out.write("hello".getBytes());
        out.write("world".getBytes());
        out.write("你好".getBytes());
        max.close();
    }
}

ByteBuffer 大小分配(ByteBuffer扩容)

  • 每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为每个 channel 维护一个独立的 ByteBuffer
  • ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer
    • 一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能。
      参考实现 http://tutorials.jenkov.com/java-performance/resizable-array.html
    • 另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗。

因为一个ByteBuffer最好对应一个SocketChannel方便使用,而一个SelectionKey对应一个SocketChannel,所以可以利用这点进行关联
netty中会根据实际情况增大缩小ByteBuffer

服务器端

    /**
     * 解决客户端发送的数据大于ByteBuffer的容量的问题
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        // 1. 创建 selector, 管理多个 channel
        Selector selector = Selector.open();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        // 2. 建立 selector 和 channel 的联系(注册)
        // SelectionKey:将来事件发生后,通过它可以知道是哪个channel的事件
        SelectionKey sscKey = ssc.register(selector, 0, null);
        // SelectionKey.OP_ACCEPT:key 只关注 accept 事件
        // ssc.register()中第二个入参为0表示不关注任何事件
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("sscKey:{}", sscKey);
        ssc.bind(new InetSocketAddress(8080));
        while (true) {
            // 3. select 方法:没有事件发生,线程阻塞。有事件,线程才会恢复运行
            // select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
            // 如:不进行channel.accept()操作,则一直为未处理事件,线程会一直运行不阻塞
            selector.select();
            // 4. 处理事件, selectedKeys 内部包含了所有发生的事件
            // selector.selectedKeys()拿到所有发生的读写连接的事件集合
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();// accept, read
            while (iter.hasNext()) {
                //其实就是ServerSocketChannel.register()注册的SelectionKey
                SelectionKey key = iter.next();
                // 处理key 时,要从 selectedKeys 集合中删除,否则下次处理就会有问题
                iter.remove();
                log.debug("key: {}", key);
                // 5. 区分事件类型
                if (key.isAcceptable()) { // 如果是 accept (新的客户端连接)
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();//执行后并不会删除该SelectionKey,而是修改状态为已处理
                    sc.configureBlocking(false);
                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    //将-个byteBuffer作为附件关联到selectionKey上
                    SelectionKey scKey = sc.register(selector, 0, buffer);
                    scKey.interestOps(SelectionKey.OP_READ);
                    log.debug("{}", sc);
                    log.debug("scKey:{}", scKey);
                } else if (key.isReadable()) { // 如果是 read(客户端发送数据)
                    try {
                        SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel
                        //获版 SelectionKey 上关联的附件
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        int read = channel.read(buffer);
                        // 如果客户端是正常断开,read 的方法的返回值是 -1
                        if(read == -1) {
                            key.cancel();
                        } else {
//                            buffer.flip();
//                            debugAll(buffer);
                            split(buffer);
                            // 如果方法中进行了compact()操作,buffer还是满了,则需要扩容
                            if(buffer.position() == buffer.limit()){
                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
                                buffer.flip();
                                newBuffer.put(buffer);//将旧的buffer的数据复制到新的buffer中
                                key.attach(newBuffer);
                            }
//                            System.out.println(Charset.defaultCharset().decode(buffer));
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        // 处理客户端断开了,因此需要将 key 取消(从 selector 的 keys 集合中真正删除 key)
                        key.cancel();
                    }
                }
            }
        }
    }

    private static void split(ByteBuffer source) {
        source.flip();
        for (int i = 0; i < source.limit(); i++) {
            // 找到一条完整消息
            if (source.get(i) == '\n') {
                int length = i + 1 - source.position();
                // 把这条完整消息存入新的 ByteBuffer
                ByteBuffer target = ByteBuffer.allocate(length);
                // 从 source 读,向 target 写
                for (int j = 0; j < length; j++) {
                    target.put(source.get());
                }
                debugAll(target);
            }
        }
        source.compact();
    }

客户端

    public static void main(String[] args) throws IOException {
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost", 8080));
        SocketAddress address = sc.getLocalAddress();

//        ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
        //发送数据,将 buffer 数据写入 channel
//        sc.write(buffer);

//        sc.write(Charset.defaultCharset().encode("hello\nworld\n"));
        sc.write(Charset.defaultCharset().encode("0123456789abcdefzzzz\n"));
        System.out.println("waiting...");
//        sc.close();
    }

5 处理 write 事件

一次无法写完例子

  • 非阻塞模式下,无法保证把 buffer 中所有数据都写入 channel,因此需要追踪 write 方法的返回值(代表实际写入字节数)
  • 用 selector 监听所有 channel 的可写事件,每个 channel 都需要一个 key 来跟踪 buffer,但这样又会导致占用内存过多,就有两阶段策略
    • 当消息处理器第一次写入消息时,才将 channel 注册到 selector 上
    • selector 检查 channel 上的可写事件,如果所有的数据写完了,就取消 channel 的注册
    • 如果不取消,会每次可写均会触发 write 事件

服务器端向客户端发送超大量数据,导致服务器发送数据时缓冲区是满的,导致无法发送数据。使得int write = sc.write(buffer);中返回为0
改进代码:让服务器端发送数据缓冲区满了时,进行读操作

服务器端

/**
 * 写入内容过多问题
 */
public class WriteServer {

    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        ssc.bind(new InetSocketAddress(8080));
        while (true) {
            selector.select();
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                iter.remove();
                if (key.isAcceptable()) {
                    //省略ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    //SocketChannel不能直接获得,因为ServerSocketChannel只有一个,而SocketChannel有多个
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    SelectionKey sckey = sc.register(selector, 0, null);
                    sckey.interestOps(SelectionKey.OP_READ);
                    // 1. 向客户端发送大量数据
                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < 30000000; i++) {
                        sb.append("a");
                    }
                    ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());

                    // 2. 返回值代表实际写入的字节数
//                    while (buffer.hasRemaining()){
//                        int write = sc.write(buffer);
//                        System.out.println(write);
                    // 会出现很多次write为0的情况,原因是缓冲区已经满了无法再进行写入
//                    }
                    //向客户端发送一次数据
                    int write = sc.write(buffer);
                    System.out.println(write);

                    // 3. 判断是否有剩余内容(实现服务器端在发送大量数据时阻塞,进行其他的事件)
                    if (buffer.hasRemaining()) {
                        // 4. 关注可写事件
                        //拿到原来和关注事件和读事件一起进行关注,只加入读事件会覆盖之前的关注的事件
                        sckey.interestOps(sckey.interestOps() + SelectionKey.OP_WRITE);
                        //sckey.interestOps(sckey.interestOps() | SelectionKey.OP_WRITE);//也可以使用位运算符,添加需要关注的多个事件
                        // 5. 把服务器端向客户端未写完的数据挂到 sckey 上
                        sckey.attach(buffer);
                    }
                } else if (key.isWritable()) {
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    SocketChannel sc = (SocketChannel) key.channel();
                    int write = sc.write(buffer);
                    System.out.println(write);
                    //然后本次也没有发送完所有数据给客户端,因为SelectionKey已经关注了可写事件,下次可写时会再次触发
                    // 6. 清理SelectionKey上附件操作
                    if (!buffer.hasRemaining()) {
                        key.attach(null); // 需要清除buffer
                        key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);//不需关注可写事件
                    }
                }
            }
        }
    }
}

客户端

public class WriteClient {
    public static void main(String[] args) throws IOException {
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost", 8080));

        // 3. 接收数据
        int count = 0;
        while (true) {
            ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
            count += sc.read(buffer);
            System.out.println(count);
            buffer.clear();
        }
    }
}

💡 write 为何要取消

只要向 channel 发送数据时,socket 缓冲可写,这个事件会频繁触发,因此应当只在 socket 缓冲区写不下时再关注可写事件,数据写完之后再取消关注

6 多线程优化

💡 利用多线程优化
现在服务器都是多核 cpu,设计时要充分考虑别让 cpu 的力量被白白浪费

前面的代码只有一个选择器,没有充分利用多核 cpu,如何改进呢?
分两组选择器

  • 单线程配一个选择器,专门处理 accept 事件
  • 创建 cpu 核心数的线程,每个线程配一个选择器,轮流处理 read 事件
    模拟netty的设计思路
/**
 * boss负责接受连接
 * worker负责连接读写操作
 */
@Slf4j
public class MultiThreadServer {

    /**
     *  多worker
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        Thread.currentThread().setName("boss");//修改主线程的名称为boss
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        Selector boss = Selector.open();
        SelectionKey bossKey = ssc.register(boss, 0, null);
        bossKey.interestOps(SelectionKey.OP_ACCEPT);
        ssc.bind(new InetSocketAddress(8080));
        // 1. 创建固定数量的 worker 并初始化
        Worker[] workers = new Worker[2];
        // 最大发挥多核cpu优势
        // 注意在docker中,使用Runtime.getRuntime().availableProcessors()拿到的是物理cpu个数,并不是docker容器申请时的个数
        // 所以建议还是直接填写个数
//        Worker[] workers = new Worker[Runtime.getRuntime().availableProcessors()];
        for (int i = 0; i < workers.length; i++) {
            workers[i] = new Worker("worker-" + i);
        }
        AtomicInteger index = new AtomicInteger();
        while(true) {
            boss.select();
            Iterator<SelectionKey> iter = boss.selectedKeys().iterator();
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                iter.remove();
                if (key.isAcceptable()) {
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    log.debug("connected...{}", sc.getRemoteAddress());
                    // 2. 关联 selector
                    log.debug("before register...{}", sc.getRemoteAddress());
                    // 使用round robin 轮询,平均的使用到每一个worker
                    workers[index.getAndIncrement() % workers.length].register(sc); // boss 调用 初始化 selector , 启动 worker-0
                    log.debug("after register...{}", sc.getRemoteAddress());
                }
            }
        }
    }

    /**
     * Worker专门负责读写事件
     * 每个Worker拥有独立的线程和selector
     */
    static class Worker implements Runnable{
        private Thread thread;
        private Selector selector;
        private String name;
        private volatile boolean start = false; // 还未初始化
        //使用ConcurrentLinkedQueue在两个线程之间传递数据
        private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
        public Worker(String name) {
            this.name = name;
        }

        /**
         *  使用selector.wakeup()解决worker线程的selector.select()阻塞问题
         *  将selector.wakeup();放到sc.register()之前,也可以解决worker线程的selector.select()阻塞问题
         * @param sc
         * @throws IOException
         */
        public void register(SocketChannel sc) throws IOException {
            if (!start) {
                selector = Selector.open();
                thread = new Thread(this, name);
                thread.start();
                start = true;
            }
            //wakeup()方法有点像消息队列中的消息,不管worker线程的selector.select()代码先wakeup()运行还是后wakeup()运行
            // 都会唤醒select()方法
            selector.wakeup(); // 唤醒 select 方法 boss
            sc.register(selector, SelectionKey.OP_READ, null); // boss
        }

        @Override
        public void run() {
            while(true) {
                try {
                    selector.select(); // worker-0  select()方法:没有事件发生,线程阻塞。有事件,线程才会恢复运行
                    Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        if (key.isReadable()) {
                            ByteBuffer buffer = ByteBuffer.allocate(16);
                            SocketChannel channel = (SocketChannel) key.channel();
                            log.debug("read...{}", channel.getRemoteAddress());
                            channel.read(buffer);
                            buffer.flip();
                            debugAll(buffer);
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客户端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel sc = SocketChannel.open();
        sc.connect(new InetSocketAddress("localhost", 8080));
        SocketAddress address = sc.getLocalAddress();

//        ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
        //发送数据,将 buffer 数据写入 channel
//        sc.write(buffer);

//        sc.write(Charset.defaultCharset().encode("hello\nworld\n"));
        sc.write(Charset.defaultCharset().encode("0123456789abcdefzzzz\n"));
        System.out.println("waiting...");
//        sc.close();
    }
}

💡 如何拿到 cpu 个数

Runtime.getRuntime().availableProcessors() 如果工作在 docker 容器下,因为容器不是物理隔离的,会拿到物理 cpu 个数,而不是容器申请时的个数。
这个问题直到 jdk 10 才修复,使用 jvm 参数 UseContainerSupport 配置, 默认开启

7 UDP

  • UDP 是无连接的,client 发送数据不会管 server 是否开启
  • server 这边的 receive 方法会将接收到的数据存入 byte buffer,但如果数据报文超过 buffer 大小,多出来的数据会被默默抛弃

服务器端


public class UdpServer {
    public static void main(String[] args) {
        try (DatagramChannel channel = DatagramChannel.open()) {
            channel.socket().bind(new InetSocketAddress(9999));
            System.out.println("waiting...");
            ByteBuffer buffer = ByteBuffer.allocate(32);
            channel.receive(buffer);
            buffer.flip();
            debug(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端

public class UdpClient {
    public static void main(String[] args) {
        try (DatagramChannel channel = DatagramChannel.open()) {
            ByteBuffer buffer = StandardCharsets.UTF_8.encode("hello");
            InetSocketAddress address = new InetSocketAddress("localhost", 9999);
            channel.send(buffer, address);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2105049.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于TCP的web端服务器数据库查询商品

背景描述 该系统为创建网络并发服务器&#xff0c;搭建HTML网络页面框架&#xff0c;通过HTTP超文本网络传输与用户建立连接&#xff08;TCP建立连接&#xff09;&#xff0c;从自己建立的数据库中查询用户所需信息&#xff0c;使用户能在网页中直接查询相关内容。本系统包括…

Elasticsearch设置密码报错:ERROR: X-Pack Security is disabled by configuration.

elasticsearch@6ef6c3f5ee45:~$ bin/elasticsearch-setup-passwords auto Unexpected response code [405] from calling GET http://172.17.0.2:9200/_security/_authenticate?pretty It doesn’t look like the X-Pack security feature is enabled on this Elasticsearch n…

Meshy-4:AI驱动3D建模的革命性工具,解锁虚拟创作新高度

Meshy发布了最新的AI驱动3D建模工具——Meshy-4&#xff0c;这是虚拟环境创作领域的一大进步。对设计师和开发者来说&#xff0c;Meshy-4的出现不仅是技术上的飞跃&#xff0c;更是创作效率的极大提升。 Meshy-4的亮点与功能 1. 更清晰、更专业的AI生成3D模型&#xff1a; Mes…

沉浸式体验:ARM 工控机携手 HT for Web 打造智能建筑监控

工业领域技术的进步不断推动着生产和管理方式的革新。随着物联网、大数据、云计算等技术的发展&#xff0c;工业自动化和信息化融合的趋势日益明显。在这样的背景下&#xff0c;HT for Web 和 ARM 工业计算机成为了工业智能化道路上的重要工具。 HT for Web 是一款基于WebGL的高…

2024国赛数学建模备赛|30种常用的算法模型之最优算法,线性规划

1.最优化理论基础 1.1 最优化问题的数学模型 通俗地说&#xff0c;所谓最优化问题&#xff0c;就是求一个多元函数在某个给定集合上的极 值. 几乎所有类型的最优化问题都可以用下面的数学模型来描述: 这里&#xff0c;&#x1d43e; 是某个给定的集合 (称为可行集或可行域)&a…

ffmpeg 视频编码及基本知识

理论 H264编码原理&#xff08;简略&#xff09; 1. 视频为什么需要进行编码压缩 降低视频数据大小&#xff0c;方便存储和传输 2. 为什么压缩的原始数据采用YUV格式 彩色图像的格式是 RGB 的&#xff0c;但RGB 三个颜色是有相关性的。 采用YUV格式&#xff0c;利用人对图像的…

LoRa芯片在RX时产生的中断顺序QA

目录 1 前言2 问题集锦及解答2.1 radio芯片在接包时&#xff0c;preamble、header和Rx done三个中断产生顺序是怎么样的&#xff1f;谁先谁后&#xff1f;2.2 产生了Header error中断后&#xff0c;radio芯片会继续接收本包还是立马丢弃本包&#xff1f;2.3 产生了CRC error中断…

语言中的类型转换

编程语言中必然有很多情况需要转换类型。比如引入const的概念就为了提高安全性&#xff0c;编译器提前检查&#xff0c;避免一些意外修改。当然&#xff0c;有时&#xff0c;我们希望手动转换一个变量的类型&#xff0c;让其变成常量&#xff0c;可以利用编译器提供的cast方法。…

保隆科技半年报:净利同比下滑近两成,ADAS/空悬业务仍亏损

2024年上半年&#xff0c;在全球产业链调整、局部战争仍未平息等事件长期影响下&#xff0c;叠加主要经济体货币政策调整、债务风险上升等周期性因素&#xff0c;全球经济复苏面临较大不确定性&#xff0c;汽车市场尚处在缓慢恢复阶段。 这也导致不少汽车零部件上市公司的半年报…

SQL语言的规则和规范

规则 是什么呢&#xff0c;规则就是我们最基本&#xff0c;每时每刻都要遵守的比如人行道靠右&#xff0c;不能逆行&#xff0c; 规范 呢就是锦上添花&#xff0c;如果你不这么做&#xff0c;是不那么道德&#xff0c;不那么好的&#xff0c;就像小学生见到老师要问好&#…

SAP HCM 如何追踪Z表的日志修改记录

导读 INTRODUCTION 日志记录&#xff1a;这几天遇到一个问题&#xff0c;就是查谁修改Z表的数据&#xff0c;因为HCM系统大部分都是信息类型&#xff0c;信息类型修改是有专门一套的处理机制&#xff0c;那么Z开头的表是不是也有追踪的一套机制。今天我们分析下如何开启Z表追…

新生自我介绍ppt怎么做?用这款在线PPT软件一键自动生成!

新学期伊始&#xff0c;初一新生除了适应新的学习环境&#xff0c;还要制作新生自我介绍ppt&#xff0c;让同学们更好地相互了解彼此&#xff0c;自我介绍成为了一项重要的流程。制作一份精美的自我介绍PPT&#xff0c;无疑能够让你在新班级中脱颖而出&#xff0c;给同学们留下…

Mysql之存储引擎概述

文章目录 存储引擎MySQL体系结构存储引擎特点InnoDBMyISAMMemory总结 存储引擎选择 存储引擎 MySQL体系结构 连接层&#xff1a;最上层是一些客户端和链接服务&#xff0c;主要完成一些类似于连接处理、授权认证、及相关的安全方案。服务器也会为安全接入的每个客户端验证它所…

基于python学生信息成绩的管理系统设计与实现,很详细!

需求分析 1.1数据操纵 &#xff08;1&#xff09;录入并保存学生的基本信息及选课信息&#xff08;如学号、姓名、性别、专业、课程名称、课程成绩&#xff09;&#xff1b; &#xff08;2&#xff09;可以对已经保存的学生基本信息及选课信息进行修改&#xff1b; &#x…

字符串(4题)

目录 1.最长公共前缀 2.最长回文串 3.二进制求和 4.字符串相乘 1.最长公共前缀 . - 力扣&#xff08;LeetCode&#xff09; class Solution { public:string longestCommonPrefix(vector<string>& strs) {string ret;int cur 0;while(1){if(strs[0].size() cu…

YOLOv8改进 | 模块缝合 | C2f 融合REPVGGOREPA提升检测性能【详细步骤 完整代码】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录 &#xff1a;《YOLOv8改进有效…

记一种常用的实时数据同步方案:Canal+Kafka+Flume

记一种常用的实时数据同步方案&#xff1a;CanalKafkaFlume 在当今数据驱动的业务环境中&#xff0c;数据同步是确保系统间数据一致性的关键环节。一种高效、稳定且可扩展的数据同步方案对于支撑企业的数据处理和分析需求至关重要。本文将介绍一种结合了Canal、Kafka和Flume的…

【unity游戏开发】Blender导出到Unity,带texture

【背景】 上一篇完成了将Mixamo的动画应用到blender的fbx模型中。但是默认配置导出fbx又导入Unity后发现Texture都没了(mesh和rig都在)。如何将Texture也一并导入呢? 【要点】 Blender导出后的FBX展开Mesh的名称不是文件名称,而是同Blender中的Mesh名称。可以根据这一点…

【案例66】支付指令客户端崩溃分析全过程

问题现象 月底&#xff0c;需要给人员开工资&#xff0c;但是财务人员在点击【支付状态指令】节点&#xff0c;点击状态确认后&#xff0c;系统直接崩溃&#xff0c;页面都卡掉。人员已经2天未发工资&#xff0c;情况比较紧急。 更改Uclient模式从分离模式改为嵌入模式&#x…