NIO 处理消息边界问题

43 阅读1分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

数据包从 channel 写入 ByteBuffer 后,在某一时刻 Buffer 中的消息并非是完整的,即 粘包/拆包 问题,对此处理消息边界有三种实现方式

  1. 固定的消息长度:数据包的大小相同,服务器按照固定长度读取,缺点是浪费带宽
  2. 按照分隔符拆分:指定分隔符,但效率会低
  3. TLV(Type、Length、Value)格式:即先指定消息的类型和长度,根据长度分配合适大小的缓冲区

因此需要在读事件的 Channel 上绑定一个 ByteBuffer

// 分配ByteBuffer缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
// 传入byteBuffer作为附件传入
sc.register(selector, SelectionKey.OP_READ, byteBuffer);


// 从SelectionKey中取出ByteBuffer读数据
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();

在读数据时从这个 ByteBuffer 中取出并根据分隔符进行切分,将完整的消息放到新的存储内容的 ByteBuffer 中再进行后续的处理

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 target = ByteBuffer.allocate(length);
        for (int j = 0; j < length; j++) {
          target.put(source.get());
        }

        // 切换读模式,读取target中的内容
        target.flip();
        BufferUtils.read(target);
      }
    }
    source.compact();
}

倘若经过 split() 方法后,ByteBufferposition 指针和 limit 指针重合,说明这不是完整的包,不能对其完全出,需要进行一次 Buffer 扩容,即申请一个新 Buffer 将原 Buffer 的数据拷贝进来

if (byteBuffer.position() == byteBuffer.limit()) {
  key.attach(resize(byteBuffer));
}

最终处理消息边界的代码可以表示为

public class NIOServer {

  public static void main(String[] args) throws IOException {
    // 创建selector,用于管理channel
    Selector selector = Selector.open();

    ServerSocketChannel socketChannel = ServerSocketChannel.open();
    socketChannel.bind(new InetSocketAddress(8080));
    socketChannel.configureBlocking(false);

    // 注册channel到selector上,设定只关注accept事件
    socketChannel.register(selector, SelectionKey.OP_ACCEPT, null);

    while (true) {
      // 阻塞方法,在没有事件发生时会阻塞,避免线程浪费
      selector.select();
      // selector作为观察者收到了主题发出的通知,开始处理所有发生的事件
      Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
      while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        // 从集合中移除待处理的事件
        iterator.remove();
        // 区分事件类型
        if (key.isAcceptable()) {
          ServerSocketChannel channel = (ServerSocketChannel) key.channel();
          SocketChannel sc = channel.accept();
          sc.configureBlocking(false);
          // 注册到selector上,监听read事件
          ByteBuffer byteBuffer = ByteBuffer.allocate(16);
          // 传入byteBuffer作为附件
          sc.register(selector, SelectionKey.OP_READ, byteBuffer);
        } else if (key.isReadable()) {
          try {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
            int read = channel.read(byteBuffer);
            // 如果返回-1说明客户端正常断开
            if (read == -1) {
              key.cancel();
            } else {
              split(byteBuffer);
              if (byteBuffer.position() == byteBuffer.limit()) {
                ByteBuffer newBuffer = ByteBuffer.allocate(byteBuffer.capacity() * 2);
                byteBuffer.flip();
                newBuffer.put(byteBuffer);
                key.attach(newBuffer);
              }
            }
          } catch (IOException e) {
            // 如果客户端异常断开,在获得channel时出现异常,在这里cancel()来处理掉事件
            key.cancel();
          }
        }
      }
    }

  }

}