在网络传输中,粘包和半包应该是最常出现的问题,做为 Java 中最常使用的 NIO 网络框架 Netty,它又是如何解决的呢?今天就让咱们来看看。 git
TCP 传输中,客户端发送数据,实际是把数据写入到了 TCP 的缓存中,粘包和半包也就会在此时产生。github
客户端给服务端发送了两条消息ABC
和DEF
,服务端这边的接收会有多少种状况呢?有多是一次性收到了全部的消息ABCDEF
,有多是收到了三条消息AB
、CD
、EF
。缓存
上面所说的一次性收到了全部的消息ABCDEF
,相似于粘包。若是客户端发送的包的大小比 TCP 的缓存容量小,而且 TCP 缓存能够存放多个包,那么客户端和服务端的一次通讯就可能传递了多个包,这时候服务端从 TCP 缓存就可能一下读取了多个包,这种现象就叫粘包
。网络
上面说的后面那种收到了三条消息AB
、CD
、EF
,相似于半包。若是客户端发送的包的大小比 TCP 的缓存容量大,那么这个数据包就会被分红多个包,经过 Socket 屡次发送到服务端,服务端第一次从接受缓存里面获取的数据,实际是整个包的一部分,这时候就产生了半包
(半包不是说只收到了全包的一半,是说收到了全包的一部分)。框架
其实从上面的定义,咱们就能够大概知道产生的缘由了。性能
粘包的主要缘由:编码
半包的主要缘由:code
其实咱们能够换个角度看待问题:cdn
收发
的角度看,即是一个发送可能被屡次接收,多个发送可能被一次接收。 传输
的角度看,即是一个发送可能占用多个传输包,多个发送可能共用一个传输包。 根本缘由,实际上是get
TCP 是流式协议,消息无边界。
(PS : UDP 虽然也能够一次传输多个包或者屡次传输一个包,但每一个消息都是有边界的,所以不会有粘包和半包问题。)
就像上面说的,UDP 之因此不会产生粘包和半包问题,主要是由于消息有边界,所以,咱们也能够采起相似的思路。
将 TCP 链接改为短链接,一个请求一个短链接。这样的话,创建链接到释放链接之间的消息即为传输的信息,消息也就产生了边界。
这样的方法就是十分简单,不须要在咱们的应用中作过多修改。但缺点也就很明显了,效率低下,TCP 链接和断开都会涉及三次握手以及四次握手,每一个消息都会涉及这些过程,十分浪费性能。
所以,并不推介这种方式。
封装成帧(Framing),也就是本来发送消息的单位是缓冲大小,如今换成了帧,这样咱们就能够自定义边界了。通常有4种方式:
这种方式下,消息边界也就是固定长度便可。
优势就是实现很简单,缺点就是空间有极大的浪费,若是传递的消息中大部分都比较短,这样就会有不少空间是浪费的。
所以,这种方式通常也是不推介的。
这种方式下,消息边界也就是分隔符自己。
优势是空间再也不浪费,实现也比较简单。缺点是当内容自己出现分割符时须要转义,因此不管是发送仍是接受,都须要进行整个内容的扫描。
所以,这种方式效率也不是很高,但能够尝试使用。
这种方式,就有点相似 Http 请求中的 Content-Length,有一个专门的字段存储消息的长度。做为服务端,接受消息时,先解析固定长度的字段(length字段)获取消息总长度,而后读取后续内容。
优势是精肯定位用户数据,内容也不用转义。缺点是长度理论上有限制,须要提早限制可能的最大长度从而定义长度占用字节数。
所以,十分推介用这种方式。
其余方式就各不相同了,好比 JSON 能够当作是使用{}
是否成对。这些优缺点就须要你们在各自的场景中进行衡量了。
Netty 支持上文所讲的封装成帧(Framing)中的前三种方式,简单介绍下:
方式 | 解码 | 编码 |
---|---|---|
固定长度 | FixedLengthFrameDecoder | 简单 |
分割符 | DelimiterBasedFrameDecoder | 简单 |
专门的 length 字段 | LengthFieldBasedFrameDecoder | LengthFieldPrepender |
今天主要介绍了粘包和半包问题、解决思路和 Netty 中的支持,我会在下一篇文章里重点讲述 Netty 中的具体实现,敬请期待。
有兴趣的话能够访问个人博客或者关注个人公众号、头条号,说不定会有意外的惊喜。