Netty处理TCP拆包、粘包
一分钟看懂TCP粘包拆包 https://mp.weixin.qq.com/s/bcwgnL5CGlLMUTxRogbQKQ
一分钟看懂TCP粘包拆包
平时大家在网络编程过程中可能会遇到这样一种现象:客户端发送了一长串消息,服务端接受的消息揉在一起或者被拆分了,这样就会造成消息难以被正确理解。
比如说有一天你特别想喝奶茶,看了一下外卖,「一点点」的奶茶看着不错,(一点点赶紧给我打钱 doge),于是你在群里发了一条消息,想找几个人拼奶茶:
一点点奶茶有人喝吗?
结果群里同事回了一句:
现在不是已经三点了吗?
你觉得莫名其妙,看了一眼同事的手机,他收到的消息是这样的两行:
一点
点奶茶有人喝吗?
哈哈,讲了一个冷笑话。用专业的术语来说这种现象就是「拆包」了,我们接着往下讲。
TCP 粘包拆包的现象
粘包拆包问题一般是处于应用层下的问题,在数据链路层、网络层以及传输层都有可能发生。我们日常的网络应用开发大多都在传输层进行,因此本文着重讲解传输层粘包拆包问题。
传输层有两个协议我们都很熟悉:UDP 和 TCP,UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。
下面用一个简单的例子来讲解什么是粘包和拆包。
假设客户端向服务端连续发送了两个数据包,用 packet1
和 packet2
来表示,那么服务端收到的数据可能有四种:
(1)第一种情况,服务端按顺序正常收到两个包,即未出现粘包和拆包的现象。
(2)第二种情况,服务端只收到一个数据包,由于 TCP 保证送达的特性,所以这一个数据包包含了客户端发送的两个数据包的信息,这种现象就是粘包。除非客户端发送的数据包有明确的规则,否则服务端不知道两个包的界限,难以处理数据。
(3)第三种情况,服务端收到了三个数据包,Package1
数据包被拆分为两个数据包:Package1.1
和Package1.2
,这种现象就是拆包,至于拆包的原因下面会讲,服务端收到拆开的数据包也很难处理。
(4)第四种情况,一些大的数据包被拆分为小的数据包,小的数据包与其他数据包粘在一起,这种现象是将上面的粘包和拆包综合在一块。
TCP 粘包拆包的原因
TCP 是一个面向「流」的协议,所谓流就是没有界限的一长串二进制数据。TCP 作为传输层协议并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就会出现粘包拆包的问题。
例如,TCP缓冲区是1024个字节大小,如果应用一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,站在业务上来看这就是「粘包」;
如果应用一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是「拆包」,也就是将一个大的包拆分为多个小包进行发送。
TCP 粘包拆包的解决方法
TCP 是面向流的,会发生粘包和拆包,那作为应用程序,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?通常会有以下一些常用的方法:
(1)发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
如下图,在每个包前面加上包的实际长度。
(2)发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
下图每个包的固定长度为 4,接收端很容易进行区分。
(3)可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
如下图,在每个包的后面加上特殊字符:/
Netty 框架如何解决粘包拆包问题
Netty 作为一款高性能的 Java 网络编程框架,不仅是基于 Java NIO 进行了深度封装,还在客户端与服务端之间的数据传输上做了有效处理。
前面讲过 TCP 传输会出现粘包和拆包的现象,Netty 针对这一点内置了多款数据流编解码器,客户端服务端按照约定好的规则进行数据传输即可解决这个问题。
Netty 提供了多款开箱即用的编解码器:
(1)FixedLengthFrameDecoder 固定长度解码器
(2)DelimiterBasedFrameDecoder 指定分隔符解码器
(3)LengthFieldBasedFrameDecoder 基于数据包长度解码器
(4)等等……这里不再列举
小结
TCP 是一个面向「流」的协议,所谓流就是没有界限的一长串二进制数据。在实际的传输过程中,TCP 会根据网络情况将数据包进行拆分或者拼装,如果业务没有定义一个明确的界限规则,在应用层的业务上就会出现粘包拆包的现象。
针对 TCP 粘包拆包的现象,常见的解决思路如下:
(1)发送端给每个数据包添加包首部。
(2)发送端将每个数据包封装为固定长度(。
(3)可以在数据包之间设置边界。
为了解决粘包拆包,Netty 框架也提供了很多开箱即用的编解码器,极大简化网络编程解决此类问题的难度。
Netty实践(二):TCP拆包、粘包问题-学海无涯 心境无限-51CTO博客 http://blog.51cto.com/zhangfengzhe/1890577
什么是TCP拆包、粘包?
在网络通信中,数据在底层都是以字节流形式在流动,那么发送方和接受方理应有一个约定(协议),只有这样接受方才知道需要接受多少数据,哪些数据需要在一起处理;如果没有这个约定,就会出现本应该一起处理的数据,被TCP划分为多个包发给接收方进行处理,如下图:
看一个TCP拆包、粘包的实例
客户端Handler:
服务端Handler:
运行结果:
上面的程序本意是CLIENT发送3次消息给SERVER,SERVER端理应处理3次,可是结果SERVER却将3条消息一次处理了。
那么如何解决TCP拆包、粘包问题呢?其实思路不外乎有3种:
第一种:发定长数据
接收方拿固定长度的数据,发送方发送固定长度的数据即可。但是这样的缺点也是显而易见的:如果发送方的数据长度不足,需要补位,浪费空间。
第二种:在包尾部增加特殊字符进行分割
发送方发送数据时,增加特殊字符;在接收方以特殊字符为准进行分割
第三种:自定义协议
类似于HTTP协议中的HEAD信息,比如我们也可以在HEAD中,告诉接收方数据的元信息(数据类型、数据长度等)
Netty如何解决TCP拆包、粘包问题?
在《Java通信实战:编写自定义通信协议实现FTP服务》中,涉及到了JAVA SOCKET这方面的处理,大家可以参考。接下来,我们来看Netty这个框架是如何帮助我们解决这个问题的。本篇博客的代码在《Netty实践(一):轻松入门》基础上进行。
方式一:定长消息
Server启动类:
Client Handler:
运行结果:
利用FixedLengthFrameDecoder,加入到管道流处理中,长度够了接收方才能收到。
方式二:自定义分隔符
Server启动类:
Client Handler:
运行结果:
方式三:自定义协议
下面我们将简单实现一个自定义协议:
HEAD信息中包含:数据长度、数据版本
数据内容
MyHead public class MyHead { //数据长度 private int length; //数据版本 private int version; public MyHead(int length, int version) { this.length = length; this.version = version; } public int getLength() { return length; } public void setLength(int length) { this.length = length; } public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } } MyMessage public class MyMessage { //消息head private MyHead head; //消息body private String content; public MyMessage(MyHead head, String content) { this.head = head; this.content = content; } public MyHead getHead() { return head; } public void setHead(MyHead head) { this.head = head; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public String toString() { return String.format("[length=%d,version=%d,content=%s]",head.getLength(),head.getVersion(),content); } } 编码器 /** * Created by Administrator on 17-1-9. * 编码器 将自定义消息转化成ByteBuff */ public class MyEncoder extends MessageToByteEncoder { @Override protected void encode(ChannelHandlerContext channelHandlerContext, MyMessage myMessage, ByteBuf byteBuf) throws Exception { int length = myMessage.getHead().getLength(); int version = myMessage.getHead().getVersion(); String content = myMessage.getContent(); byteBuf.writeInt(length); byteBuf.writeInt(version); byteBuf.writeBytes(content.getBytes(Charset.forName("UTF-8"))); } } 解码器 /** * Created by Administrator on 17-1-9. * 解码器 将ByteBuf数据转化成自定义消息 */ public class MyDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List
运行结果
到这里,你会发现Netty处理TCP拆包、粘包问题很简单,通过编解码技术支持,让我们编写自定义协议也很方便,在后续的Netty博客中,我将继续为大家介绍Netty在实际中的一些应用(比如实现心跳检测),See You~