[ Java | NIO详解 | 4/5 ] 网络编程

通用Client

     public static void main(String[] args) throws IOException {
         SocketChannel channel = SocketChannel.open(new InetSocketAddress(25565));
         // 注:Buffer大小最好为有效数据大小 -> 防止服务端解码异常
         channel.write(ByteBuffer.wrap("传输的信息".getBytes(StandardCharsets.UTF_8)));
         channel.close();
     }

阻塞模式

Server

     private static final ByteBuffer BUFFER = ByteBuffer.allocate(1024);
 ​
     public static void main(String[] args) throws IOException {
         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
         serverSocketChannel.bind(new InetSocketAddress(25565));
         while (true) {
             SocketChannel socketChannel = serverSocketChannel.accept(); // 阻塞在此处
             socketChannel.read(BUFFER);
             BUFFER.flip();
             System.out.println(StandardCharsets.UTF_8.decode(BUFFER));
             BUFFER.clear();
         }
     }

非阻塞模式

Server - 轮询

     public static void main(String[] args) throws IOException {
         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
         serverSocketChannel.configureBlocking(false); // 非阻塞模式
         serverSocketChannel.bind(new InetSocketAddress(25565));
         List<SocketChannel> channels = new ArrayList<>();
         while (true) {
             SocketChannel socketChannel = serverSocketChannel.accept();
             if (socketChannel != null) { // 连接建立
                 socketChannel.configureBlocking(false); // 非阻塞模式
                 channels.add(socketChannel);
             }
             for (SocketChannel channel : channels) {
                 ByteBuffer BUFFER = ByteBuffer.allocate(1024);
                 int read = channel.read(BUFFER); // 非阻塞模式
                 if (read > 0) {
                     BUFFER.flip();
                     System.out.println(StandardCharsets.UTF_8.decode(BUFFER));
                     BUFFER.clear();
                 }
             }
         }
     }

Server - Selector

注意 每注册一个 SocketChannel 都会多出一个 SelectionKey。我们调用 remove() 仅是从 selectedKeys 集合中移除本次调用到的 key ( Selector 自己不会移除),防止下次 select() 时集合中仍存有先前处理过的 key。 key 的 OP_ACCEPT 状态处理完成后就被迭代器 remove(),而 OP_READ 状态在套接字连接的过程中一直存在,故每次 select() 时 selectedKeys 中都会加入正在进行传输的 SocketChanne l ! ! ! 在保证数据读完且客户端连接断开的情况下( read() == -1 )可以通过 cancel() 永久移除该 key,否则默认数据传输未完成,在每个下一次轮询时重新加入该key,耗时耗力!

     public static void main(String[] args) throws IOException {
         // 1. 创建 Selector 管理多个 Channel
         Selector selector = Selector.open();
         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
         serverSocketChannel.configureBlocking(false); // 非阻塞模式
         // 2. 建立 Selector 和 Channel 的联系(注册)
         serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
         serverSocketChannel.bind(new InetSocketAddress(25565));
         while (true) {
             // 3. 查询事件,若存在则加入到 selectedKeys
             selector.select();
             // 4. 处理事件,selectedKeys 内包含所有发生的事件
             Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
             while (iterator.hasNext()) {
                 SelectionKey key = iterator.next();
                 // 处理 key 时要手动从 selectedKeys 集合中删除,否则下次获取时 key 上无对应 Channel 调用会空指针
                 iterator.remove();
                 if (key.isAcceptable()) { // 是否是 Accept 事件
                     // 向对应端口的 ServerSocketChannel 注册 OP_READ 监听
                     ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                     SocketChannel socketChannel = serverChannel.accept();
                     socketChannel.configureBlocking(false);
                     socketChannel.register(selector, SelectionKey.OP_READ);
                 } else if (key.isReadable()) {
                     try {
                         System.out.println(key);
                         SocketChannel channel = (SocketChannel) key.channel();
                         ByteBuffer buffer = ByteBuffer.allocate(32);
                         int length;
                         while ((length = channel.read(buffer)) > 0) {
                             buffer.flip();
                             System.out.println(new String(buffer.array(), 0, length));
                             buffer.clear();
                         }
                         // -1 : 客户端断开
                         if (length == -1) key.cancel();
                     } catch (IOException e) {
                         log.warn("客户端断开连接: ", e);
                         key.cancel();
                     }
                 }
             }
         }
     }

消息边界

当 数据长度 > 缓存区大小 时,数据会分开发送导致服务端解码异常,称为消息边界问题

可能出现的问题

  • 黏包 - 多个消息合并在同一 Buffer 中发出

  • 半包 - 由于 Buffer 大小限制,消息被分割发出

解决方案

  • 固定 Buffer 长度

    多用于传输固定范围内长度的数据,服务端固定一个 Buffer 大小 >= 消息长度,客户端发送消息时未达到最大长度则补齐进行发送。(浪费带宽)

  • 分隔符

    在两消息间加入特定分隔符,服务端接受时逐行比对进行消息分割接收,可能存在 Buffer 扩容问题。(效率低)

  • TLV 格式

    TLV - Type、Length、Value。规定数个字节的头文件长度记录消息长度,服务端基于此可有效进行消息接收。类似 Http 式。( Buffer 需要提前分配,若过大影响服务端吞吐量)

ByteBuffer大小分配

  • 每个 Channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 Channel 共同使用,因此需要为每个 Channel 维护一个独立的 ByteBuffer

  • ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer

    • 一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能,参考实现

    • 另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗

多线程优化

多核 Worker 分配 SocketChannel

     public static void main(String[] args) throws IOException {
         Thread.currentThread().setName("Boss");
         ServerSocketChannel ssc = ServerSocketChannel.open();
         ssc.bind(new InetSocketAddress(25565));
         ssc.configureBlocking(false);
 ​
         Selector selector = Selector.open();
         ssc.register(selector, SelectionKey.OP_ACCEPT);
 ​
         Worker[] workers = new Worker[Runtime.getRuntime().availableProcessors()];
         for (int i = 0; i < workers.length; ++ i) {
             workers[i] = new Worker("worker-" + i);
         }
         AtomicInteger integer = new AtomicInteger();
         while (true) {
             selector.select();
             Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
             while (iterator.hasNext()) {
                 SelectionKey key = iterator.next();
                 iterator.remove();
                 if (key.isAcceptable()) {
                     SocketChannel sc = ssc.accept();
                     log.debug("connect {}", sc);
                     sc.configureBlocking(false);
 ​
                     workers[integer.getAndIncrement() % workers.length].register(sc);
                 }
             }
         }
     }
 ​
     /**
      * Worker 的实际意义就是
      *      对于每个工作线程都新建一个 Selector 对 SocketChannel
      *    进行拆分管理,每个连接被均匀分配到每个工作线程上。
      * */
     private static class Worker implements Runnable {
         private final String name;
         private Thread thread;
         private Selector selector;
 ​
         public Worker(String name) {
             this.name = name;
         }
         
         public void register(SocketChannel sc) throws IOException {
             if (thread == null) {
                 thread = new Thread(this, name);
                 selector = Selector.open();
                 thread.start();
             }
             // 唤醒 Worker 线程执行注册操作,在下一次循环中进行 select() 筛选事件监听
             selector.wakeup();
             SelectionKey register = sc.register(selector, SelectionKey.OP_READ);
             log.debug("register {}", register);
         }
         
         @Override
         public void run() {
             while (true) {
                 try {
                     // Selector#select() 会阻塞,故使用队列延迟执行 SocketChannel 注册
                     selector.select();
                     Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                     while (iterator.hasNext()) {
                         SelectionKey key = iterator.next();
                         iterator.remove();
                         if (key.isReadable()) {
                             log.debug("read...");
                             ByteBuffer buffer = ByteBuffer.allocate(16);
                             SocketChannel sc = (SocketChannel) key.channel();
                             sc.read(buffer);
                             buffer.flip();
                             System.out.println(Charset.defaultCharset().decode(buffer));
                         }
                     }
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
     }

如何拿到 cpu 个数

  • Runtime.getRuntime().availableProcessors() 如果工作在 docker 容器下,因为容器不是物理隔离的,会拿到物理 cpu 个数,而不是容器申请时的个数

  • 这个问题直到 jdk 10 才修复,使用 jvm 参数 UseContainerSupport 配置,默认开启

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值