我们都知道TCP是基于字节流的传输协议。那么数据在通信层传播其实就像河水一样并没有明显的分界线,而数据具体表示什么意思什么地方有句号什么地方有分号这个对于TCP底层来说并不清楚。应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段,之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。所以对于这个数据拆分成大包小包的问题就是我们今天要讲的粘包和拆包的问题。

1 TCP粘包拆包问题说明

粘包和拆包这两个概念估计大家还不清楚,通过下面这张图我们来分析一下:

假设客户端分别发送两个数据包D1,D2个服务端,但是发送过程中数据是何种形式进行传播这个并不清楚,分别有下列4种情况:

  1. 服务端一次接受到了D1和D2两个数据包,两个包粘在一起,称为粘包;
  2. 服务端分两次读取到数据包D1和D2,没有发生粘包和拆包;
  3. 服务端分两次读到了数据包,第一次读到了D1和D2的部分内容,第二次读到了D2的剩下部分,这个称为拆包;
  4. 服务器分三次读到了数据部分,第一次读到了D1包,第二次读到了D2包的部分内容,第三次读到了D2包的剩下内容。

2. TCP粘包产生原因

我们知道在TCP协议中,应用数据分割成TCP认为最适合发送的数据块,这部分是通过“MSS”(最大数据包长度)选项来控制的,通常这种机制也被称为一种协商机制,MSS规定了TCP传往另一端的最大数据块的长度。这个值TCP协议在实现的时候往往用MTU值代替(需要减去IP数据包包头的大小20Bytes和TCP数据段的包头20Bytes)所以往往MSS为1460。通讯双方会根据双方提供的MSS值得最小值确定为这次连接的最大MSS值。

tcp为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。

发生粘包拆包的原因主要有以下这些:

  1. 应用程序写入数据的字节大小大于套接字发送缓冲区的大小将发生拆包;

  2. 进行MSS大小的TCP分段。MSS是TCP报文段中的数据字段的最大长度,当TCP报文长度-TCP头部长度>mss的时候将发生拆包;

  3. 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,将发生粘包;

  4. 数据包大于MTU的时候将会进行切片。MTU即(Maxitum Transmission Unit) 最大传输单元,由于以太网传输电气方面的限制,每个以太网帧都有最小的大小64bytes最大不能超过1518bytes,刨去以太网帧的帧头14Bytes和帧尾CRC校验部分4Bytes,那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值我们就把它称之为MTU。这个就是网络层协议非常关心的地方,因为网络层协议比如IP协议会根据这个值来决定是否把上层传下来的数据进行分片。

3. 如何解决TCP粘包拆包

我们知道tcp是无界的数据流,且协议本身无法避免粘包,拆包的发生,那我们只能在应用层数据协议上,加以控制。通常在制定传输数据时,可以使用如下方法:

  1. 设置定长消息,服务端每次读取既定长度的内容作为一条完整消息;

  2. 使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容;

  3. 设置消息边界,服务端从网络流中按消息边界分离出消息内容。比如在消息末尾加上换行符用以区分消息结束。

当然应用层还有更多复杂的方式可以解决这个问题,这个就属于网络层的问题了,我们还是用java提供的方式来解决这个问题。我们先看一个例子看看粘包是如何发生的。

服务端:

  1. public class HelloWordServer {
  2. private int port;
  3. public HelloWordServer(int port) {
  4. this.port = port;
  5. }
  6. public void start(){
  7. EventLoopGroup bossGroup = new NioEventLoopGroup();
  8. EventLoopGroup workGroup = new NioEventLoopGroup();
  9. ServerBootstrap server = new ServerBootstrap().group(bossGroup,workGroup)
  10. .channel(NioServerSocketChannel.class)
  11. .childHandler(new ServerChannelInitializer());
  12. try {
  13. ChannelFuture future = server.bind(port).sync();
  14. future.channel().closeFuture().sync();
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }finally {
  18. bossGroup.shutdownGracefully();
  19. workGroup.shutdownGracefully();
  20. }
  21. }
  22. public static void main(String[] args) {
  23. HelloWordServer server = new HelloWordServer(7788);
  24. server.start();
  25. }
  26. }

服务端Initializer:

  1. public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
  2. @Override
  3. protected void initChannel(SocketChannel socketChannel) throws Exception {
  4. ChannelPipeline pipeline = socketChannel.pipeline();
  5. // 字符串解码 和 编码
  6. pipeline.addLast("decoder", new StringDecoder());
  7. pipeline.addLast("encoder", new StringEncoder());
  8. // 自己的逻辑Handler
  9. pipeline.addLast("handler", new HelloWordServerHandler());
  10. }
  11. }

服务端handler:

  1. public class HelloWordServerHandler extends ChannelInboundHandlerAdapter {
  2. private int counter;
  3. @Override
  4. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  5. String body = (String)msg;
  6. System.out.println("server receive order : " + body + ";the counter is: " + ++counter);
  7. }
  8. @Override
  9. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  10. super.exceptionCaught(ctx, cause);
  11. }
  12. }

客户端:

  1. public class HelloWorldClient {
  2. private int port;
  3. private String address;
  4. public HelloWorldClient(int port,String address) {
  5. this.port = port;
  6. this.address = address;
  7. }
  8. public void start(){
  9. EventLoopGroup group = new NioEventLoopGroup();
  10. Bootstrap bootstrap = new Bootstrap();
  11. bootstrap.group(group)
  12. .channel(NioSocketChannel.class)
  13. .handler(new ClientChannelInitializer());
  14. try {
  15. ChannelFuture future = bootstrap.connect(address,port).sync();
  16. future.channel().closeFuture().sync();
  17. } catch (Exception e) {
  18. e.printStackTrace();
  19. }finally {
  20. group.shutdownGracefully();
  21. }
  22. }
  23. public static void main(String[] args) {
  24. HelloWorldClient client = new HelloWorldClient(7788,"127.0.0.1");
  25. client.start();
  26. }
  27. }

客户端Initializer:

  1. public class ClientChannelInitializer extends ChannelInitializer<SocketChannel> {
  2. protected void initChannel(SocketChannel socketChannel) throws Exception {
  3. ChannelPipeline pipeline = socketChannel.pipeline();
  4. pipeline.addLast("decoder", new StringDecoder());
  5. pipeline.addLast("encoder", new StringEncoder());
  6. // 客户端的逻辑
  7. pipeline.addLast("handler", new HelloWorldClientHandler());
  8. }
  9. }

客户端handler:

  1. public class HelloWorldClientHandler extends ChannelInboundHandlerAdapter {
  2. private byte[] req;
  3. private int counter;
  4. public BaseClientHandler() {
  5. req = ("Unless required by applicable law or agreed to in writing, software\n" +
  6. " distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
  7. " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
  8. " See the License for the specific language governing permissions and\n" +
  9. " limitations under the License.This connector uses the BIO implementation that requires the JSSE\n" +
  10. " style configuration. When using the APR/native implementation, the\n" +
  11. " penSSL style configuration is required as described in the APR/native\n" +
  12. " documentation.An Engine represents the entry point (within Catalina) that processes\n" +
  13. " every request. The Engine implementation for Tomcat stand alone\n" +
  14. " analyzes the HTTP headers included with the request, and passes them\n" +
  15. " on to the appropriate Host (virtual host)# Unless required by applicable law or agreed to in writing, software\n" +
  16. "# distributed under the License is distributed on an \"AS IS\" BASIS,\n" +
  17. "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" +
  18. "# See the License for the specific language governing permissions and\n" +
  19. "# limitations under the License.# For example, set the org.apache.catalina.util.LifecycleBase logger to log\n" +
  20. "# each component that extends LifecycleBase changing state:\n" +
  21. "#org.apache.catalina.util.LifecycleBase.level = FINE"
  22. ).getBytes();
  23. }
  24. @Override
  25. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  26. ByteBuf message;
  27. //将上面的所有字符串作为一个消息体发送出去
  28. message = Unpooled.buffer(req.length);
  29. message.writeBytes(req);
  30. ctx.writeAndFlush(message);
  31. }
  32. @Override
  33. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  34. String buf = (String)msg;
  35. System.out.println("Now is : " + buf + " ; the counter is : "+ (++counter));
  36. }
  37. @Override
  38. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  39. ctx.close();
  40. }
  41. }

运行客户端和服务端我们能看到:

我们看到这个长长的字符串被截成了2段发送,这就是发生了拆包的现象。同样粘包我们也很容易去模拟,我们把BaseClientHandler中的channelActive方法里面的:

  1. message = Unpooled.buffer(req.length);
  2. message.writeBytes(req);
  3. ctx.writeAndFlush(message);

这几行代码是把我们上面的一长串字符转成的byte数组写进流里发送出去,那么我们可以在这里把上面发送消息的这几行循环几遍这样发送的内容增多了就有可能在拆包的时候把上一条消息的一部分分配到下一条消息里面了,修改如下:

  1. for (int i = 0; i < 3; i++) {
  2. message = Unpooled.buffer(req.length);
  3. message.writeBytes(req);
  4. ctx.writeAndFlush(message);
  5. }

改完之后我们再运行一下,输出太长不好截图,我们在输出结果中能看到循环3次之后的消息服务端收到的就不是之前的完整的一条了,而是被拆分了4次发送。

对于上面出现的粘包和拆包的问题,Netty已有考虑,并且有实施的方案:LineBasedFrameDecoder。

我们重新改写一下ServerChannelInitializer:

  1. public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
  2. @Override
  3. protected void initChannel(SocketChannel socketChannel) throws Exception {
  4. ChannelPipeline pipeline = socketChannel.pipeline();
  5. pipeline.addLast(new LineBasedFrameDecoder(2048));
  6. // 字符串解码 和 编码
  7. pipeline.addLast("decoder", new StringDecoder());
  8. pipeline.addLast("encoder", new StringEncoder());
  9. // 自己的逻辑Handler
  10. pipeline.addLast("handler", new BaseServerHandler());
  11. }
  12. }

新增:pipeline.addLast(new LineBasedFrameDecoder(2048))。同时,我们还得对上面发送的消息进行改造BaseClientHandler:

  1. public class BaseClientHandler extends ChannelInboundHandlerAdapter {
  2. private byte[] req;
  3. private int counter;
  4. req = ("Unless required by applicable dfslaw or agreed to in writing, software" +
  5. " distributed under the License is distributed on an \"AS IS\" BASIS," +
  6. " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." +
  7. " See the License for the specific language governing permissions and" +
  8. " limitations under the License.This connector uses the BIO implementation that requires the JSSE" +
  9. " style configuration. When using the APR/native implementation, the" +
  10. " penSSL style configuration is required as described in the APR/native" +
  11. " documentation.An Engine represents the entry point (within Catalina) that processes" +
  12. " every request. The Engine implementation for Tomcat stand alone" +
  13. " analyzes the HTTP headers included with the request, and passes them" +
  14. " on to the appropriate Host (virtual host)# Unless required by applicable law or agreed to in writing, software" +
  15. "# distributed under the License is distributed on an \"AS IS\" BASIS," +
  16. "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." +
  17. "# See the License for the specific language governing permissions and" +
  18. "# limitations under the License.# For example, set the org.apache.catalina.util.LifecycleBase logger to log" +
  19. "# each component that extends LifecycleBase changing state:" +
  20. "#org.apache.catalina.util.LifecycleBase.level = FINE\n"
  21. ).getBytes();
  22. @Override
  23. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  24. ByteBuf message;
  25. message = Unpooled.buffer(req.length);
  26. message.writeBytes(req);
  27. ctx.writeAndFlush(message);
  28. }
  29. @Override
  30. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  31. String buf = (String)msg;
  32. System.out.println("Now is : " + buf + " ; the counter is : "+ (++counter));
  33. }
  34. @Override
  35. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  36. ctx.close();
  37. }
  38. }

去掉所有的”\n”,只保留字符串末尾的这一个。原因稍后再说。channelActive方法中我们不必再用循环多次发送消息了,只发送一次就好(第一个例子中发送一次的时候是发生了拆包的),然后我们再次运行,大家会看到这么长一串字符只发送了一串就发送完毕。程序输出我就不截图了。下面来解释一下LineBasedFrameDecoder。

LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf 中的可读字节,判断看是否有”\n” 或者” \r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器。支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。这个对于我们确定消息最大长度的应用场景还是很有帮助。

对于上面的判断看是否有”\n” 或者” \r\n”以此作为结束的标志我们可能回想,要是没有”\n” 或者” \r\n”那还有什么别的方式可以判断消息是否结束呢。别担心,Netty对于此已经有考虑,还有别的解码器可以帮助我们解决问题,下节我们继续学习。

Netty学习(四)-TCP粘包和拆包的更多相关文章

  1. 【Netty】TCP粘包和拆包

    一.前言 前面已经基本上讲解完了Netty的主要内容,现在来学习Netty中的一些可能存在的问题,如TCP粘包和拆包. 二.粘包和拆包 对于TCP协议而言,当底层发送消息和接受消息时,都需要考虑TCP ...

  2. netty 解决TCP粘包与拆包问题(一)

    1.什么是TCP粘包与拆包 首先TCP是一个"流"协议,犹如河中水一样连成一片,没有严格的分界线.当我们在发送数据的时候就会出现多发送与少发送问题,也就是TCP粘包与拆包.得不到我 ...

  3. tcp粘包和拆包的处理方案

    随着智能硬件越来越流行,很多后端开发人员都有可能接触到socket编程.而很多情况下,服务器与端上需要保证数据的有序,稳定到达,自然而然就会选择基于tcp/ip协议的socekt开发.开发过程中,经常 ...

  4. TCP粘包,拆包及解决方法

    在进行Java NIO学习时,发现,如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题.我们都知道TCP属于传输 ...

  5. TCP粘包、拆包

    TCP粘包.拆包 熟悉tcp编程的可能都知道,无论是服务端还是客户端,当我们读取或发送数据的时候,都需要考虑TCP底层的粘包/拆包机制. TCP是一个“流”协议,所谓流就是没有界限的遗传数据.可以想象 ...

  6. netty的解码器与粘包和拆包

    tcp是一个“流”的协议,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题. 假设客户端分别发送数据包D1和D2给服务端,由于服务 ...

  7. 【游戏开发】网络编程之浅谈TCP粘包、拆包问题及其解决方案

    引子 现如今手游开发中网络编程是必不可少的重要一环,如果使用的是TCP协议的话,那么不可避免的就会遇见TCP粘包和拆包的问题,马三觉得haifeiWu博主的 TCP 粘包问题浅析及其解决方案 这篇博客 ...

  8. TCP粘包和拆包问题

    问题产生 一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题. 下面可以看一张图,是客户端向服务端发送包: 1. 第一种情况 ...

  9. 关于TCP粘包和拆包的终极解答

    关于TCP粘包和拆包的终极解答 程序员行业有一些奇怪的错误的观点(误解),这些误解非常之流行,而且持有这些错误观点的人经常言之凿凿,打死也不相信自己有错,实在让人啼笑皆非.究其原因,还是因为这些错误观 ...

随机推荐

  1. CentOS 常用命令合集

    tail -f ../logs/catalina.out    在Tomcat中的bin目录下查看Tomcat日志 ps -ef|grep java                 查看Tomcat服 ...

  2. Ural 2062:Ambitious Experiment(树状数组 || 分块)

    http://acm.timus.ru/problem.aspx?space=1&num=2062 题意:有n个数,有一个值,q个询问,有单点询问操作,也有对于区间[l,r]的每个数i,使得n ...

  3. spring boot freemarker 导出word 带echarts图形报表

    创建word文件内容如下 将word导出为xml格式 将文件后缀名改为 .ftl 在springboot项目中添加freemarker依赖 <!-- 导出word文档--> <dep ...

  4. 嵊州D2T2 八月惊魂 全排列 next_permutation()

    嵊州D2T2 八月惊魂 这是一个远古时期的秘密,至今已无人关心. 这个世界的每个时代可以和一个 1 ∼ n 的排列一一对应. 时代越早,所对应的排列字典序就越小. 我们知道,公爵已经是 m 个时代前的 ...

  5. c++学习书籍推荐《面向对象程序设计:C++语言描述(原书第2版)》下载

    百度云及其他网盘下载地址:点我 <面向对象程序设计:C++语言描述(原书第2版)>内容丰富,结构合理,写作风格严谨,深刻地论述了c++语言的面向对象编程的各种技术,主要内容包括:面向对象编 ...

  6. android布局几点随想

    1. 正式布局界面时,先在纸上画出整个布局,并考虑用什么布局比较适合: 2. 布局界面先做出框架,并用不同的背景颜色标记出来,确保大的布局框架式正确的: 3. 接着在每个大的布局框架内布局小的布局: ...

  7. ng-bootstrap 组件集中 tabset 组件的实现分析

    ng-bootstrap: tabset 本文介绍了 ng-bootstrap 项目中,tabset 的实现分析. 使用方式 <ngb-tabset> 作为容器元素,其中的每个页签以一个 ...

  8. 一次使用InfluxDB数据库的总结

    前言 因当前的项目需要记录每秒钟服务器的状态信息,例如负载.cpu等等信息,这些数据都是和时间相关联的. 因为一秒钟就要存储挺多的数据.而且我还在前端做了echart的折线图,使用websocket实 ...

  9. javascript案例之照片墙

    效果图: ----------------------------------------------------------------------------------------------- ...

  10. micropython TPYBoard v201 简易的web服务器的实现过程

    转载请注明文章来源,更多教程可自助参考docs.tpyboard.com,QQ技术交流群:157816561,公众号:MicroPython玩家汇 前言 TPYBoard v201开发板上搭载了以太网 ...