Netty学习记录

75 阅读12分钟

NIO-Buffer 使用

从 channel 读取数据到 buffer 中

URL url = ByteBufferExample.class.getClassLoader().getResource("data.txt");
if (url != null) {
    try (FileInputStream resource = new FileInputStream(url.getPath())) {
        FileChannel channel = resource.getChannel();
        // position = 0 | capacity = 1024 | limit = capacity
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        // position = readLen
        while (channel.read(buffer) != -1) { // 将 channel 中的数据写入到 buffer
            // limit = position | position = 0
            buffer.flip(); // 切换至读模式
            while (buffer.hasRemaining()) {
                byte data = buffer.get();
                System.out.println((char) data);
            }
            // position = 0 | limit = capacity
            buffer.clear(); // 切换为写模式
        }
    } catch (IOException e) {
        log.info("读取失败", e);
    }
}

在堆或内存上分配空间

// 在堆内存上分配空间,读写效率低,受 GC 影响
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// 在直接内存上分配空间,读写效率高,不收 GC 影响,分配内存的效率低(使用了系统内存,可能造成内存泄露)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

buffer 的读取操作

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put(new byte[]{'a', 'b', 'c'});
buffer.flip();
byte[] dst = new byte[3];
buffer.get(dst); // 从头读取到 dst
log.info("position-{}", buffer.position());

buffer.rewind(); // 设置为再次读取 position 置为 0
log.info("position-{}", buffer.position());
buffer.get(dst);
log.info("position-{}", buffer.position());

// mark & reset
buffer.rewind();
buffer.mark(); // 标记当前 position 位置
buffer.get(dst);

buffer.reset(); // 重置到 mark 标记位置
buffer.get(dst); // 再次读取
log.info(new String(dst));

// 读取指定位置,不会移动 position 指针
log.info("position-{}", buffer.position());
byte data = buffer.get(0);
System.out.println((char) data);
log.info("position-{}", buffer.position());

buffer & 字符串的转换

// 字符串转 buffer 方式一
String str = "你好,世界";
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(str.getBytes());

// 字符串转 buffer 方式二(encode 会自动切换到读模式)
ByteBuffer encodeBuffer = StandardCharsets.UTF_8.encode(str);

// buffer 转字符串
String convertStr = StandardCharsets.UTF_8.decode(encodeBuffer).toString();
log.info(convertStr);

粘包 & 半包

针对如下原始数据

Hello World\n
Im zhang san\n
How arw you\n

经过网络传输后,组合为了如下形式的两个 buffer

Hello World\nIm zhang san\nHo
w are you\n

原本不同行的消息都合并到了一起(粘包),原本一起的消息被拆分、截断(半包),这主要是由消息发送方的的缓冲区大小限制的

针对这种情况,需要手动将消息进行拆分,有如下示例代码

public static void example() {
    // 模拟分两次接受消息
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    buffer.put("Hello World\nIm zhang san\nHo".getBytes());
    split(buffer);
    buffer.put("w are you\n".getBytes());
    split(buffer);
}

// 0 1 2 3 4
public static void split(ByteBuffer source) {
    source.flip(); // 切换至读模式
    // 以 \n 为依据,尝试找到一条完整消息
    for (int i = 0; i < source.limit(); i++) {
        if (source.get(i) == '\n') {
            // 获取消息的长度
            int len = i + 1 - source.position();
            // 将完整消息写入新的 buffer
            ByteBuffer partBuffer = ByteBuffer.allocate(len);
            for (int j = 0; j < len; j++) {
                partBuffer.put(source.get()); // 从 source 读取
            }
            partBuffer.flip();
            byte[] dts = new byte[partBuffer.limit()];
            partBuffer.get(dts);
            log.info(new String(dts));
        }
    }
    // 将未读取的内容往前移动至 0 索引位置,position 置为当前未读取内容长度索引位置,相当于继续写入
    // 这里会将未找到换行符的剩余消息移动至开头,与后面继续写入的消息进入下一次拆分流程
    source.compact();
}

FileChannel

channel 数据传输

String formPath = Optional.ofNullable(ChannelExample.class.getClassLoader().getResource("form.txt")).orElseThrow().getPath();
String toPath = Optional.ofNullable(ChannelExample.class.getClassLoader().getResource("to.txt")).orElseThrow().getPath();

try (FileInputStream fis = new FileInputStream(formPath);
     FileOutputStream fos = new FileOutputStream(toPath)) {
    FileChannel form = fis.getChannel();
    FileChannel to = fos.getChannel();
    // 底层使用操作系统的零拷贝进行优化, 单次传输存在 2G 的上限
    form.transferTo(0, form.size(), to);
} catch (Exception e) {
    log.error("err", e);
}

超出2G上限,分多次传输

String formPath = "D:\\randomfile.bin";
String toPath = "D:\\randomfileto.bin";

try (FileInputStream fis = new FileInputStream(formPath);
     FileOutputStream fos = new FileOutputStream(toPath)) {
    FileChannel form = fis.getChannel();
    FileChannel to = fos.getChannel();
    long size = form.size();
    // 分多次传输
    for (long left = form.size(); left > 0; ) {
        // left 当前剩余的大小; size - left 即下次传输的位置
        left -= form.transferTo(size - left, left, to);
    }
} catch (Exception e) {
    log.error("err", e);
}

阻塞 & 非阻塞

客户端

try (SocketChannel sc = SocketChannel.open()) {
    sc.connect(new InetSocketAddress("localhost", 8080));
    sc.write(StandardCharsets.UTF_8.encode("Hello"));
    log.info("waiting...");
} catch (Exception e) {
    log.error("err", e);
}

服务端阻塞通信

try (ServerSocketChannel ssc = ServerSocketChannel.open();) {
    ByteBuffer buffer = ByteBuffer.allocate(16);
    // 绑定监听端口
    ssc.bind(new InetSocketAddress(8080));

    // 客户端连接集合
    List<SocketChannel> channels = new ArrayList<>();
    log.debug("connecting...");
    while (true) {
        // accept 建立与客户端连接,阻塞模式下,线程会停止运行,等待一个客户端连接后继续往下执行
        SocketChannel sc = ssc.accept();
        log.debug("connected... {}", sc);
        channels.add(sc);
        for (SocketChannel channel : channels) {
            // 接收客户端发送的数据
            log.info("read...");
            channel.read(buffer); // 阻塞模式下,线程停止运行
            buffer.flip();
            log.info("receive:{}", StandardCharsets.UTF_8.decode(buffer));
            buffer.clear();
            log.debug("after read...{}", channel);
        }
    }
} catch (Exception e) {
    log.error("err", e);
}

阻塞模式下,单线程无法很好的处理多个客户端,如果每个客户端都开辟线程,势必会导致 OOM,如果使用线程池,多个线程的上下文切换效率也不高

服务端非阻塞通信

try (ServerSocketChannel ssc = ServerSocketChannel.open()) {
    ByteBuffer buffer = ByteBuffer.allocate(16);
    ssc.configureBlocking(false); // 非阻塞模式
    ssc.bind(new InetSocketAddress(8080));
    List<SocketChannel> channels = new ArrayList<>();

    while (true) {
        SocketChannel sc = ssc.accept(); // 非阻塞,线程会继续运行,如果没有连接建立,则 sc 为 null
        if (sc != null) {
            log.debug("connected... {}", sc);
            sc.configureBlocking(false); // 非阻塞模式
            channels.add(sc);
        }
        for (SocketChannel channel : channels) {
            // 接收客户端发送的数据
            int read = channel.read(buffer);// 非阻塞,线程仍然会继续运行,如果没有读到数据,read 返回 0
            if (read > 0) {
                buffer.flip();
                log.info("receive:{}", StandardCharsets.UTF_8.decode(buffer));
                buffer.clear();
                log.debug("after read...{}", channel);
            }
        }
    }
} catch (Exception e) {
    log.error("err", e);
}

非阻塞模式下,线程空转,浪费 cpu 性能且数据读取的过程中,线程仍然是阻塞的

Selector

非阻塞 Server & Client

通过 selector 可以对多个 channel 进行统一管理,每个 channel 可以注册自己自己关心的事件,在没有 channel 注册的事件发生时,selector 处于阻塞状态,只有对应 channel 注册的事件发生时,线程才会继续执行,这样就避免了线程长时间空转消耗 cpu

// 创建 selector 管理多个 channel, 同时创建服务端的 channel: ssc
try (Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open()) {
    ssc.configureBlocking(false); // 非阻塞模式
    ssc.bind(new InetSocketAddress(8080));

    // 将服务端的 channel 注册到 selector 上(注意:selector 可以管理多个 channel)
    SelectionKey serverSocketChannelKey = ssc.register(selector, 0, null);

    /*
    几种事件:
    accept 有连接请求时触发
    connect 客户端建立连接后触发
    read 可读事件
    write 可写事件
     */
    // 对于 ServerSocketChannel,只需要关心 accept 连接事件
    serverSocketChannelKey.interestOps(SelectionKey.OP_ACCEPT);
    log.info("connecting...");
    while (true) {
        // 无事件发生,阻塞(这是通过 selector 管理 channel 的统一 api)
        selector.select();
        // 拿到所有可用事件集合(只针对注册到此 selector 上的 channel)
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> itr = selectionKeys.iterator();
        while (itr.hasNext()) {
            SelectionKey selectionKey = itr.next();
            // 区分事件类型执行对应操作
            if (selectionKey.isAcceptable()) {
                // 拿到事件对应的通道,因为是 accept 事件,这里转为服务端的 ServerSocketChannel
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                /*
                 建立客户端连接,或者取消处理事件:selectionKey.cancel(); 不能不处理
                 */
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                // 将与客户端建立的 channel 也注册到 selector,下次遍历时,就会得到对应的 SelectionKey
                SelectionKey socketChannelKey = socketChannel.register(selector, 0, null);
                socketChannelKey.interestOps(SelectionKey.OP_READ); // 与客户端的通道暂时只关心 read 事件
                log.info("connected...");
            } else if (selectionKey.isReadable()) {
                // 除了一般的 read 事件,客户端正常断开或异常断开也会产生 read 事件
                try {
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    int read = socketChannel.read(buffer);
                    if (read == -1) { // 正常断开,read() 返回值为 -1
                        log.info("客户端:{},断开", selectionKey);
                        selectionKey.cancel();
                    }
                    if (read != 0) {
                        buffer.flip();
                        log.info("receive:{}", Charset.defaultCharset().decode(buffer));
                        buffer.clear();
                    }
                } catch (Exception e) {
                    // 异常断开的 read() 方法会抛出异常,需要取消客户端在 selector 中的注册
                    log.error("客户端强制断开:", e);
                    selectionKey.cancel();
                }
            }
            itr.remove(); // 处理完的 key 需要删除,否则下次再次处理时会返回 null
        }
    }
} catch (Exception e) {
    log.error("err", e);
}

客户端发送的消息存在粘包、半包的情况,这里通过 \n 处理消息的边界,引入对消息进行拆分的逻辑,如下

public static void split(ByteBuffer source, Consumer<ByteBuffer> consumer) {
    source.flip();
    for (int i = 0; i < source.limit(); i++) {
        if (source.get(i) == '\n') {
            // 从当前位置开始计算数据的长度
            int len = i - (source.position() - 1);
            ByteBuffer partBuffer = ByteBuffer.allocate(len);
            for (int j = 0; j < len; j++) {
                partBuffer.put(source.get());
            }
            consumer.accept(partBuffer);
        }
    }
    source.compact();
}

服务的对客户端消息的处理逻辑改造如下

// 除了一般的 read 事件,客户端正常断开或异常断开也会产生 read 事件
try {
    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
    ByteBuffer buffer = ByteBuffer.allocate(16);
    int read = socketChannel.read(buffer);
    if (read == -1) { // 正常断开,read() 返回值为 -1
        log.info("客户端:{},断开", selectionKey);
        selectionKey.cancel();
    }
    if (read != 0) {
        // 以 \n 拆分消息
        split(buffer, data -> {
            data.flip();
            log.info("receive:{}", Charset.defaultCharset().decode(data));
        });
    }
} catch (IOException e) {
    // 异常断开的 read() 方法会抛出异常,需要取消客户端在 selector 中的注册
    log.error("客户端强制断开:", e);
    selectionKey.cancel();
}

然而除了对消息的边界进行处理,消息容量大小的超出也要考虑到,客户端发送消息的长度可能会超出服务端 Buffer 缓冲区的大小,为此需要对 Buffer 进行扩容

而当客户端消息的长度超过 Buffer 容量时,没有读完的消息会触发多次 read 事件,目前的 Buffer 是作为一个局部变量进行创建的,如果一次性没有读取完数据,第二次读取时,又创建了新的 Buffer,这会导致之前的消息丢失,扩容操作也无法进行

如果将 Buffer 的创建抽取到事件处理外部,这又会导致多个 channel 都使用同一个 Buffer,实际上应该为每一个 channel 都分配自己的 Buffer,这在注册 channel 到 selector 时,可以通过att参数指定,注册 channel 的代码修改如下

// 通过第三个参数指定 attachment(附件),附件可以是任意类型,这里将其用作 ByteBuffer 的关联使用
SelectionKey socketChannelKey = socketChannel.register(selector, 0, ByteBuffer.allocate(16));

这样当 read 事件发生时,通过如下方式获取关联的 Buffer

SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 拿到当前 selectionKey 关联的附件
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
int read = socketChannel.read(buffer);

接下来是 Buffer 的扩容逻辑

if (read != 0) {
    // 以 \n 拆分消息
    split(buffer, data -> {
        data.flip();
        log.info("receive:{}", Charset.defaultCharset().decode(data));
    });
    /*
     compact() 方法执行后,position 会被置为剩余未读字节数 limit 会被置为 buffer 的 capacity
     如果剩余未读字节数等于容量,就意味着需要扩容
     */
    if (buffer.position() == buffer.limit()) {
        // 扩容为两倍大小
        ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
        buffer.flip(); // 拷贝的旧 buffer 需要切换为读模式
        newBuffer.put(buffer);
        selectionKey.attach(newBuffer); // 替换附件
        buffer.clear();
    }
}

最终代码如下

// 创建 selector 管理多个 channel, 同时创建服务端的 channel: ssc
try (Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open()) {
    ssc.configureBlocking(false); // 非阻塞模式
    ssc.bind(new InetSocketAddress(8080));
    // 将服务端的 channel 注册到 selector 上(注意:selector 可以管理多个 channel)
    SelectionKey serverSocketChannelKey = ssc.register(selector, 0, null);

    /*
    几种事件:
    accept 有连接请求时触发
    connect 客户端建立连接后触发
    read 可读事件
    write 可写事件
     */
    // 对于 ServerSocketChannel,只需要关心 accept 连接事件
    serverSocketChannelKey.interestOps(SelectionKey.OP_ACCEPT);
    log.info("connecting...");
    while (true) {
        // 无事件发生,阻塞(这是通过 selector 管理 channel 的统一 api)
        selector.select();
        // 拿到所有可用事件集合(只针对注册到此 selector 上的 channel)
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> itr = selectionKeys.iterator();
        while (itr.hasNext()) {
            SelectionKey selectionKey = itr.next();
            // 区分事件类型执行对应操作
            if (selectionKey.isAcceptable()) {
                // 拿到事件对应的通道,因为是 accept 事件,这里转为服务端的 ServerSocketChannel
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                /*
                 建立客户端连接,或者取消处理事件:selectionKey.cancel(); 不能不处理
                 */
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                // 将与客户端建立的 channel 也注册到 selector,下次遍历时,就会得到对应的 SelectionKey
                SelectionKey socketChannelKey = socketChannel.register(selector, 0, ByteBuffer.allocate(16));
                socketChannelKey.interestOps(SelectionKey.OP_READ); // 与客户端的通道暂时只关心 read 事件
                log.info("connected...");
            } else if (selectionKey.isReadable()) {
                // 除了一般的 read 事件,客户端正常断开或异常断开也会产生 read 事件
                try {
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    // 拿到当前 selectionKey 关联的附件
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    int read = socketChannel.read(buffer);
                    if (read == -1) { // 正常断开,read() 返回值为 -1
                        log.info("客户端:{},断开", selectionKey);
                        selectionKey.cancel();
                    }
                    if (read != 0) {
                        // 以 \n 拆分消息
                        split(buffer, data -> {
                            data.flip();
                            log.info("receive:{}", Charset.defaultCharset().decode(data));
                        });
                        /*
                         compact() 方法执行后,position 会被置为剩余未读字节数 limit 会被置为 buffer 的 capacity
                         如果剩余未读字节数等于容量,就意味着需要扩容
                         */
                        if (buffer.position() == buffer.limit()) {
                            // 扩容为两倍大小
                            ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
                            buffer.flip(); // 拷贝的旧 buffer 需要切换为读模式
                            newBuffer.put(buffer);
                            selectionKey.attach(newBuffer); // 替换附件
                            buffer.clear();
                        }
                    }
                } catch (IOException e) {
                    // 异常断开的 read() 方法会抛出异常,需要取消客户端在 selector 中的注册
                    log.error("客户端强制断开:", e);
                    selectionKey.cancel();
                }
            }
            itr.remove(); // 处理完的 key 需要删除,否则下次再次处理时会返回 null
        }
    }
} catch (Exception e) {
    log.error("err", e);
}

消息拆分

// 1 2 3 4 5 6
// 0 1 2  3 4 5 6  7 8 9
// 1 2 3 \n 4 5 6 \n 7 8
public static void split(ByteBuffer source, Consumer<ByteBuffer> consumer) {
    source.flip();
    for (int i = 0; i < source.limit(); i++) {
        if (source.get(i) == '\n') {
            // 从当前位置开始计算数据的长度
            int len = i - (source.position() - 1);
            ByteBuffer partBuffer = ByteBuffer.allocate(len);
            for (int j = 0; j < len; j++) {
                partBuffer.put(source.get()); // 从 source 读取
            }
            consumer.accept(partBuffer);
        }
    }
    source.compact();
}

写入内容过多

如下代码,演示了服务端一次性写入了过多的内容,客户端一次性未处理完的情况

服务端

try (Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open()) {
    ssc.configureBlocking(false);
    ssc.bind(new InetSocketAddress(8080));
    ssc.register(selector, SelectionKey.OP_ACCEPT);

    while (true) {
        selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();

        Iterator<SelectionKey> itr = selectionKeys.iterator();

        while (itr.hasNext()) {
            SelectionKey selectionKey = itr.next();
            if (selectionKey.isAcceptable()) {
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                // 向客户端发送大量数据
                StringBuilder data = new StringBuilder();
                for (int i = 0; i < 30000000; i++) {
                    data.append('a');
                }
                ByteBuffer buffer = Charset.defaultCharset().encode(data.toString());
                while (buffer.hasRemaining()) { // 数据过多,分多次写入
                    // 返回值代表实际写入的字节数
                    int writeLen = socketChannel.write(buffer);
                    log.info("写入:{}", writeLen);
                }
            }
            itr.remove();
        }
    }
} catch (IOException e) {
    log.error("error", e);
}

客户端

try (SocketChannel sc = SocketChannel.open()) {
    sc.connect(new InetSocketAddress(8080));
    sc.configureBlocking(false);
    ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
    while (true) {
        int read = sc.read(buffer); // 数据过多,分多次读取
        if (read != 0) {
            buffer.flip();
            String readContent = Charset.defaultCharset().decode(buffer).toString();
            log.info("读取到字节长度:{}", read);

            buffer.clear();
        }
    }
} catch (Exception e) {
    log.info("error:", e);
}

服务端部分打印如下

23:51:14.664 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:655355
23:51:14.671 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:1179639
23:51:14.675 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:917497
23:51:14.678 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:1048568
23:51:14.681 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:786426
23:51:14.683 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:0
23:51:14.685 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:0
23:51:14.688 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:0
23:51:14.690 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:0
23:51:14.692 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:0

可以看到,由于内容写入过度,使得客户端在未处理完写入内容时,服务端依然在尝试写入,导致实际写入的字节数为0,这不符合非阻塞的概念,应该在可以写入的时候进行写入操作

修改后的服务端代码如下(事件处理部分)

try (Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open()) {
    ssc.configureBlocking(false);
    ssc.bind(new InetSocketAddress(8080));
    ssc.register(selector, SelectionKey.OP_ACCEPT);

    while (true) {
        selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();

        Iterator<SelectionKey> itr = selectionKeys.iterator();

        while (itr.hasNext()) {
            SelectionKey selectionKey = itr.next();
            itr.remove();
            if (selectionKey.isAcceptable()) {
                // 与客户端建立链接
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);

                // 注册客户端的 channel,暂时监听读事件
                SelectionKey socketChannelKey = socketChannel
                        .register(selector, SelectionKey.OP_READ);

                // 向客户端发送大量数据
                ByteBuffer buffer = Charset.defaultCharset().encode("a".repeat(30000000));
                int write = socketChannel.write(buffer);
                log.info("写入长度:{}", write);

                // 还有剩余,监听可写事件,实现非阻塞写入
                if (buffer.hasRemaining()) {
                    /*
                     socketChannelKey.interestOps() + SelectionKey.OP_WRITE
                     意为在原来监听的事件基础上,再加上 OP_WRITE 事件的监听,否则直接
                     监听 OP_WRITE 事件的话,会覆盖原来的事件
                     */
                    socketChannelKey.interestOps(socketChannelKey.interestOps() + SelectionKey.OP_WRITE);
                    socketChannelKey.attach(buffer); // 将未写完的数据作为附件
                }
            }

            if (selectionKey.isWritable()) {
                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                if (buffer.hasRemaining()) {
                    int write = socketChannel.write(buffer);
                    log.info("写入长度:{}", write);
                } else {
                    selectionKey.attach(null); // 数据写入完毕,清理
                    // 当前内容写入完毕,暂时无需关注 OP_WRITE 事件
                    selectionKey.interestOps(selectionKey.interestOps() - SelectionKey.OP_WRITE);
                }
            }
        }
    }
} catch (IOException e) {
    log.error("error", e);
}