基于定长消息的java nio半包粘包处理

什么是tcp半包粘包?
简单来讲就是接收到的tcp包并不一定是一个完整的包。
它可能是1个包的一部分,也可能是多个完整包加上1个包的一部分。
为什么?
因为tcp的定义是面向字节流的传输协议,所以操作系统实现这个协议的时候,只保证字节的正确传输,而至于字节的应用层语义(可能这个字节是个分隔符,也可能这个字节和周围3个字节组成一个int,代表类的某个字段),操作系统是不管的。
比如下面这个例子(基于java):

public class Account {
    private int accountnum;
    private double balance;
    private int num;
}

要传输这个Account对象,实际上就是传输它的三个字段accountnum,balance和num,它们的大小分别是4,8,4个字节。当传输时,实现tcp协议的系统只负责把这16个字节传输到接收方,而不知道这些字节的含义。比如前4个字节是accountnum,然而这是应用层的语义,实现tcp协议的系统并不知道,也不需要知道,因为tcp规范里就没规定需要知道上层的语义。
那么接收方如何接收呢?
对于这个例子实在是太简单了,接收方只需要每次都接收16个字节就能保证每一次都能得到一个完整的Account对象,连分隔符都不需要。在已知确定传输对象长度(字节数目)的时候:

即使接收到的tcp包并不一定是一个完整的包。
接收方只需要等待,直到读到确定数量的字节,然后处理即可。
比如现在只传输了4个字节,我们知道16个字节才能组成完整的Account对象,那么再读12个字节后进行处理即可。

这个例子有什么意义?
根据这个例子受到启发,只要传输对象的长度是确定的,那么接收端很容易就能够对传输对象进行解析(就是处理tcp粘包半包)。
然而对象的长度是确定的吗?往往都不是,比如一个上面的对象现在加一个String类型的成员字段,这个String字段变成字节的时候长度就是未知的,但这并不影响我们把它变成定长的对象。
HOW?
设置最大传输长度,每次都接收最大传输长度的字节流。而这个字节流的前4个字节用于表示对象的长度,接下来的字节就是传输的对象的字节流,最后不够最大长度的用任意字节进行填充即可。
比如:

public class Account {
private int accountnum;
private double balance;
private int num;
private String extra; 
}

对于增加了String类型字段extra的新Account类来说,它的一个对象长度是不确定的,现在要传输它该怎么办?设置最大的长度为400字节,前4个字节存储实例对象的长度x,之后的x个字节为对象,最后没用到的位置用0x0(也就是0)来填充。比如下图所示:

基于定长消息的java nio半包粘包处理_第1张图片

需要注意的是,前四个字节只是字节,并不是x,需要把这4个字节转成int类型的变量,然后这个int变量对应的10进制数是x。
这个方式看起来具有很明显的局限性?
长度是有限制的?比如一次只能传输最多400-4=396个字节的对象?
但是可以把超大的对象再次分开,每一次都只传输最大包(400)长度,然后再拼接即可解决。
比如现在设计这400个字节的存储格式是这样的:
前4个字节存储这个对象总共被分成几个最大传输的包,接着的4个字节存储这是第几个,然后是长度,然后是内容,最后是填充。
基于定长消息的java nio半包粘包处理_第2张图片

这样看起来最大长度就解决了。
然而。。。基于java nio的传输适合传输大文件(巨长的字节流)吗?
nio是什么原理?
是I/O多路复用,简单来讲就是我有一个叫做选择器(Selector)的类不断轮询不同的连接(Socket)的i/o事件,发现了i/o事件就处理,处理时可以用Selector所在的线程,也可以另开启一个线程。如果要用Selector的线程处理i/o事件,那么i/o的操作时间必须很短,否则可能会丢失消息,而如果开启一个线程,i/o的时间也应该很短。why?因为如果i/o时间很长,并且线程很多,那么就退化成了bio的模型。。。那么就没必要用nio了。。。
归结起来就是nio就不适合多用户传输大文件,否则必然退化为bio模型。
所以实际上不要考虑这种大文件的传输,如果要传输大文件还是用bio模型比较好,并且在bio的传输模式下java提供了对象的序列化和反序列化,这样都不需要我们定义长度字段了。

具体的代码
参考:https://github.com/ItCrazyer/...

说明一点的是,这个例子里传输的对象是不定长的字符串,不是一个定义的类(不是像上面的Account这种),并且使用了SelectionKey对象的attachment方法,来暂存数据,暂存数据存储在TempData这个类的对象里,为什么?
因为虽然我们知道确定的长度(比如是600),并且据此处理定长的数据,但是一次传来的数据很可能是好几个定长的数据包,而且每一次我们都必须读完,比如传来了1300字节的数据,就必须1300字节都读完,不能这一次i/o事件我只读600,然后下一次i/o事件在读接下来的700个字节,那是没办法做到的,因为i/o响应的是这一次可读,并不响应你还有数据,所以这次不读完下次就没有了。。。


不过经过我测试。。。没有读完的数据再下一次仍然可以读取到,并不会因为这次没读完下一次就没了。。所以不加缓存处理也没问题!!!

你可能感兴趣的:(基于定长消息的java nio半包粘包处理)