ByteBuffer消息边界消息丢失、扩容操作以及绑定服务端绑定读触发事件向客户端发送消息

问题陈述

        使用Java语言编写Server及Client代码如下:

/**
 * Selector学习
 */
@Slf4j
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(8080));
        // 创建Selector
        Selector selector = Selector.open();
        ssc.configureBlocking(false);
        // 注册事件到selector上
        SelectionKey sscKey = ssc.register(selector, 0, null);
        // 设置只关注accept事件
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("sscKey : {}", sscKey);
        while(true){
            // select方法,没有事件发生时,线程阻塞,有事件,线程才会恢复运行
            // select 在事件未处理时,它不会阻塞,事件发生后要么处理,要么取消,不能置之不理
            int count = selector.select();
            log.info("select count : {}", count);
            // 处理事件selectedKeys 内部包含了所有发生的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                log.debug("key : {}", key);
                iterator.remove();
                // 区分事件类型
                if(key.isAcceptable()){
                    // 是连接事件
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    ByteBuffer buffer = ByteBuffer.allocate(16);
                    // 将一个byteBuffer作为附件关联到selectionKey上
                    SelectionKey scKey = sc.register(selector, 0, buffer);
                    // Key只关注可读事件
                    scKey.interestOps(SelectionKey.OP_READ);
                    log.debug("sc : {}", sc);
                    log.debug("scKey : {}", scKey);
                }else if(key.isReadable()){
                    // 是可读事件
                    try {
                        SocketChannel chanel = (SocketChannel) key.channel();   // 拿到触发事件的channel
                        ByteBuffer buffer = ByteBuffer.allocate(4);
                        // 获取selectionKey上关联的附件
//                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        int read = chanel.read(buffer);
                        if(read == -1){
                            // 正常断开,read方法的返回值是-1,此时需要将key取消
                            // 正常断开需要走四次挥手,服务端依旧需要读取客户端发送的断开连接的报文,此时依旧会触发一次读事件.
                            key.cancel();
                        }else{
                            buffer.flip();
                            ByteBufferUtil.debugRead(buffer);
                            System.out.println(Charset.defaultCharset().decode(buffer));
//                            split(buffer);
//                            if(buffer.position() == buffer.limit()){
//                                // 此时说明关联的byteBuffer容量不够需要扩容
//                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
//                                buffer.flip();
//                                newBuffer.put(buffer);
//                                key.attach(newBuffer);
//                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        // 客户端断开了,因此需要将Key取消(从selector 的 keys集合中真正删除key)
                        key.cancel();
                    }
                }
            }
        }
    }
}



import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
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(InetAddress.getLocalHost(), 8080));
        sc.write(Charset.defaultCharset().encode("中国\n"));
        System.out.println("waiting ...");
    }
}

        上述代码中Server端通过Selector监听Readable可读事件,当Client端向服务端发起连接并发送数据后,服务端接收客户端发送的数据并读取Chanel中的数据并写入buffer。结果如下:

         

        客户端发送数据:sc.write(Charset.defaultCharset().encode("中国\n")); 明显发送数据不符合预期,主要原因为发送中文字符"中国"共占有6个字节,buffer缓冲区大小为4个字节,第一次读取到buffer中的内容为4字节,输出后只有一个"中"随后乱码,第二次读取原因相似。

消息边界的处理

        上述问题产生的原因归结于消息边界的处理不正确。使用ByteBuffer接收数据时主要有以下几种情况:(拆包与粘包)

        上述问题可以概括为Server端的拆包与粘包。对于网络通信中消息接收的处理主要有以下三种方案:

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

        采用按分隔符拆分处理消息边界,此时需要构建动态的buffer,能够实现扩容操作,当读取数据时buffer已经填充满,但此时仍未达到分隔符,此时需要将byteBuffer扩容并将上次读取的内容填充至新的byteBuffer中。

        使用以下命令将byteBuffer和具体的SelectionKey关联起来,随后当key触发读事件时,获取相关联的byteBuffer,并读取chanel中的数据至buffer,若buffer中未包含分隔符,此时需要将关联的byteBuffer取出、扩容、填充已读取数据、将新的byteBuffer关联到SelectionKey上。

 

        测试效果:首次关联的byteBuffer大小为16,客户端传递的消息为"1234567890abcdef3333\n"若byteBuffer接收消息时不会自动扩容处理,则最终接收到的消息为3333,扩容处理后可以得到原始数据,结果如下:

ByteBuffer 大小分配

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

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

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

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

ByteBuffer处理write事件

        使用write写数据一次不能够写完,此时需要循环写入,如果直接循环直至将数据完全写入,此时相当于BIO阻塞,线程无法处理其他任务。

        上述阻塞模式下线程整体工作效率并不高,应当只有接收方缓冲区有空闲,发送方才进行数据的发送。提高线程的工作效率。

  • 非阻塞模式下,无法保证把 buffer 中所有数据都写入 channel,因此需要追踪 write 方法的返回值(代表实际写入字节数)

  • 用 selector 监听所有 channel 的可写事件,每个 channel 都需要一个 key 来跟踪 buffer,但这样又会导致占用内存过多,就有两阶段策略

    • 当消息处理器第一次写入消息时,才将 channel 注册到 selector 上。

    • selector 检查 channel 上的可写事件,如果所有的数据写完了,就取消 channel 的注册

    • 如果不取消,会每次可写均会触发 write 事件。

        整体代码如下:

package com.example.code.c4.serverwrite;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;

/**
 * 测试服务端向客户端写数据
 */
public class Server {
    public static void main(String[] args) throws IOException {
        // 创建serverSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 设置非阻塞模式
        ssc.configureBlocking(false);
        // 绑定端口
        ssc.bind(new InetSocketAddress(8080));
        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        while(true){
            selector.select();
            // 获取触发的事件
            Iterator<SelectionKey> itor = selector.selectedKeys().iterator();
            while(itor.hasNext()){
                SelectionKey key = itor.next();
                itor.remove();
                if (key.isAcceptable()) {
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    SelectionKey scKey = sc.register(selector, SelectionKey.OP_READ);
                    // 向客户端发送数据
                    StringBuilder stringBuilder = new StringBuilder();
                    for (int i = 0; i < 30000000; i++) {
                        stringBuilder.append("a");
                    }
                    ByteBuffer buffer = Charset.defaultCharset().encode(stringBuilder.toString());
//                    while (buffer.hasRemaining()) {
//                        int write = sc.write(buffer);
//                        System.out.println(write);
//                    }
                    int write = sc.write(buffer);
                    System.out.println(write);
                    if(buffer.hasRemaining()){
                        scKey.interestOps(scKey.interestOps() | SelectionKey.OP_WRITE);
                        scKey.attach(buffer);
                    }
                }else if(key.isWritable()){
                    ByteBuffer bufffer = (ByteBuffer) key.attachment();
                    SocketChannel sc = (SocketChannel) key.channel();
                    int write = sc.write(bufffer);
                    System.out.println(write);
                    if(!bufffer.hasRemaining()){
                        key.interestOps(key.interestOps() ^ SelectionKey.OP_WRITE);
                        key.attach(null);
                    }
                }
            }
        }
    }
}



package com.example.code.c4.serverwrite;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

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

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

 

  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值