摘要:接收方只需要等待,直到读到确定数量的字节,然后处理即可。而这个字节流的前个字节用于表示对象的长度,接下来的字节就是传输的对象的字节流,最后不够最大长度的用任意字节进行填充即可。
什么是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)来填充。比如下图所示:
需要注意的是,前四个字节只是字节,并不是x,需要把这4个字节转成int类型的变量,然后这个int变量对应的10进制数是x。
这个方式看起来具有很明显的局限性?
长度是有限制的?比如一次只能传输最多400-4=396个字节的对象?
但是可以把超大的对象再次分开,每一次都只传输最大包(400)长度,然后再拼接即可解决。
比如现在设计这400个字节的存储格式是这样的:
前4个字节存储这个对象总共被分成几个最大传输的包,接着的4个字节存储这是第几个,然后是长度,然后是内容,最后是填充。
这样看起来最大长度就解决了。
然而。。。基于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响应的是这一次可读,并不响应你还有数据,所以这次不读完下次就没有了。。。
不过经过我测试。。。没有读完的数据再下一次仍然可以读取到,并不会因为这次没读完下一次就没了。。所以不加缓存处理也没问题!!!
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/72139.html
摘要:如果什么事都没得做,它也不会死循环,它会将线程休眠起来,直到下一个事件来了再继续干活,这样的一个线程称之为线程。而请求处理逻辑既可以使用单独的线程池进行处理,也可以跟放在读写线程一块处理。 Netty到底是什么 从HTTP说起 有了Netty,你可以实现自己的HTTP服务器,FTP服务器,UDP服务器,RPC服务器,WebSocket服务器,Redis的Proxy服务器,MySQL的P...
摘要:修改之前的服务端开发代码修改为下面代码绑定端口同步等待成功等待服务端监听端口关闭主要修改了和方法和原理分析的工作原理是它依次遍历中的可读字节判断看是否有或如果有就以此位置为结束位置从可读索引到结束位置区间的字节就组成了一行它是以换行符为结束 修改之前的 Netty 服务端开发 代码, 修改为下面代码 public class TimeServer { public void ...
摘要:概述在简易框架需求与设计这篇文章中已经给出了协议的具体细节,协议类型为二进制协议,如下协议的解码我们称为,编码我们成为,下文我们将直接使用和术语。直接贴代码,参考前文提到的协议格式阅读以下代码协议编码器 概述 在《简易RPC框架:需求与设计》这篇文章中已经给出了协议的具体细节,协议类型为二进制协议,如下: ---------------------------------------...
摘要:的方法,的默认实现会判断是否是类型注意自动拆箱,自动装箱问题。适应自旋锁锁竞争是下的,会经过用户态到内核态的切换,是比较花时间的。在中引入了自适应的自旋锁,说明自旋的时间不固定,要不要自旋变得越来越聪明。 前言 只有光头才能变强 之前在刷博客的时候,发现一些写得比较好的博客都会默默收藏起来。最近在查阅补漏,有的知识点比较重要的,但是在之前的博客中还没有写到,于是趁着闲整理一下。 文本的...
阅读 1922·2021-11-19 09:40
阅读 2132·2021-10-09 09:43
阅读 3291·2021-09-06 15:00
阅读 2808·2019-08-29 13:04
阅读 2764·2019-08-26 11:53
阅读 3510·2019-08-26 11:46
阅读 2318·2019-08-26 11:38
阅读 388·2019-08-26 11:27