1、前言
我们在进行NIO编程时,通常会使用缓冲区进行消息的通信(ByteBuffer),而缓冲区的大小是固定的。那么除非你进行自动扩容(Netty就是这么处理的),否则的话,当你的消息存进该缓冲区就会存在消息边界的问题,典型的边界问题就是黏包和半包现象。
2、什么是消息黏包?
当ByteBuffer设置足够大时,会有多条消息从channel写进ByteBuffer,这时候就无法愤青数据包的边界,所有数据包粘连在一起,称为黏包问题。
如:
3、什么是消息半包?
当数据包足够大,ByteBuffer直接被填满,但是又不包含完整的数据包。这就会导致从缓冲区中取出的消息不完整,有点像消息被“砍了一半”,称为半包问题。
如:
4、三种解决思路
4.1、固定缓冲区和数据包大小
固定缓冲区和数据包大小,顾名思义就是服务端按照预定的长度读取。数据包发送的大小和ByteBuffer固定大小填充传输,就算数据包小于ByteBuffer容量,也需要填充满。
如:
很明显这种方案的缺点就是浪费带宽。因为如果数据包有多大,就算只有1字节,剩下的也需要用多余的数据填充。
4.2、按分隔符拆分不同缓冲区
按既定的分隔符拆分(如\r,\n)。缓冲区读取按既定分隔符截取,依次判断如果是分隔符,就创建相应缓冲区进行存储。保证了分隔符前后数据不会冲突。
如:
很明显这种方案有个致命问题,就是效率低。每分割一条消息就需要创建自动扩容的ByteBuffer。
参考代码:
privatestaticvoidsplit(finalByteBufferbuffer) { buffer.flip(); for(inti=0;i<buffer.limit();i++){ if (buffer.get(i)=='\n') { //遇到\n,表示一个完整的语句。写入的bufferintlength=i+1-buffer.position(); ByteBuffertarget=ByteBuffer.allocate(length); //将数据写入targetfor (intj=0; j<target.limit(); j++) { // 将buffer中的数据写入target中target.put(buffer.get()); } debugAll(target); } } //读取完毕之后读取剩余的部分,不能使用clear。clear会从头开始的buffer.compact(); }
4.3、报文头添加消息长度字段
这种方案也是最常用的方案,就是在传输的报文头添加一个固定长度的字段,用来存储当前这条消息具体数据的长度。这样当我们接收到这条报文之后,只要固定解析报文头部几个字节,就可以知道当前这条消息的长度,然后进而进行解析。
这也就是TLV格式,即 Type 类型、Length 长度、Value 数据(也就是在消息开头用一些空间存放后面数据的长度),如HTTP请求头中的Content-Type与Content-Length。类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量。
- Http 1.1 是TLV格式
- Http 2.0 是LTV格式
如以下的http请求响应头,便可以看到Content-Length:121。这就是消息具体数据的长度。
如:
或