1、TCP协议传输过程

TCP协议是面向流的协议,是流式的,没有业务上的分段,只会根据当前套接字缓冲区的情况进行拆包或者粘包:

发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由接收端获取。

2、TCP粘包和拆包概念

因为TCP会根据缓冲区的实际情况进行包的划分,在业务上认为,有的包被拆分成多个包进行发送,也可能多个晓小的包封装成一个大的包发送,这就是TCP的粘包或者拆包。

3、TCP粘包和拆包图解

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下几种情况:

  1. 服务端分两次读取到两个独立的数据包,分别是D1和D2,没有粘包和拆包。
  2. 服务端一次接收到了两个数据包,D1和D2粘在一起,发生粘包。
  3. 服务端分两次读取到数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,发生拆包。
  4. 服务端分两次读取到数据包,第一次读取到部分D1包,第二次读取到剩余的D1包和全部的D2包。

当TCP缓存再小一点的话,会把D1和D2分别拆成多个包发送。

4、TCP粘包和拆包解决策略

因为TCP只负责数据发送,并不处理业务上的数据,所以只能在上层应用协议栈解决,目前的解决方案归纳:

  1. 消息定长,每个报文的大小固定,如果数据不够,空位补空格。
  2. 在包的尾部加回车换行符标识。
  3. 将消息分为消息头与消息体,消息头中包含消息总长度。
  4. 设计更复杂的协议。

5、Netty中的解决办法

Netty提供了多种默认的编码器解决粘包和拆包:

5.1、LineBasedFrameDecoder

基于回车换行符的解码器,当遇到"\n"或者 "\r\n"结束符时,分为一组。支持携带结束符或者不带结束符两种编码方式,也支持配置单行的最大长度。

LineBasedFrameDecoder与StringDecoder搭配时,相当于按行切换的文本解析器,用来支持TCP的粘包和拆包。

使用例子:

private void start() throws Exception {
//创建 EventLoopGroup
NioEventLoopGroup group = new NioEventLoopGroup();
NioEventLoopGroup work = new NioEventLoopGroup();
try {
//创建 ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
b.group(group, work)
//指定使用 NIO 的传输 Channel
.channel(NioServerSocketChannel.class)
//设置 socket 地址使用所选的端口
.localAddress(new InetSocketAddress(port))
//添加 EchoServerHandler 到 Channel 的 ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(new LineBasedFrameDecoder(1024));
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());
p.addLast(new EchoServerHandler());
}
});
//绑定的服务器;sync 等待服务器关闭
ChannelFuture f = b.bind().sync();
System.out.println(EchoServer.class.getName() " started and listen on " f.channel().localAddress());
//关闭 channel 和 块,直到它被关闭
f.channel().closeFuture().sync();
} finally {
//关机的 EventLoopGroup,释放所有资源。
group.shutdownGracefully().sync();
}
}

注意ChannelPipeline 中ChannelHandler的顺序,

5.2、DelimiterBasedFrameDecoder

分隔符解码器,可以指定消息结束的分隔符,它可以自动完成以分隔符作为码流结束标识的消息的解码。回车换行解码器实际上是一种特殊的DelimiterBasedFrameDecoder解码器。

使用例子(后面的代码只贴ChannelPipeline部分):

ChannelPipeline p = ch.pipeline();
p.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("制定的分隔符".getBytes())));
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());
p.addLast(new EchoServerHandler());

5.3、FixedLengthFrameDecoder

固定长度解码器,它能够按照指定的长度对消息进行自动解码,当制定的长度过大,消息过短时会有资源浪费,但是使用起来简单。

 ChannelPipeline p = ch.pipeline();
p.addLast(new FixedLengthFrameDecoder(1 << 5));
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());
p.addLast(new EchoServerHandler());

5.4、LengthFieldBasedFrameDecoder

通用解码器,一般协议头中带有长度字段,通过使用LengthFieldBasedFrameDecoder传入特定的参数,来解决拆包粘包。

io.netty.handler.codec.LengthFieldBasedFrameDecoder的实例化:

    /**
* Creates a new instance.
*
* @param maxFrameLength 最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃。
* @param lengthFieldOffset 长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域。
* @param lengthFieldLength 长度域字节数。用几个字节来表示数据长度。
* @param lengthAdjustment 数据长度修正。因为长度域指定的长度可以是header body的整个长度,也可以只是body的长度。如果表示header body的整个长度,那么我们需要修正数据长度。
* @param initialBytesToStrip 跳过的字节数。如果你需要接收header body的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。
* @param failFast 如果为true,则在解码器注意到帧的长度将超过maxFrameLength时立即抛出TooLongFrameException,而不管是否已读取整个帧。
* 如果为false,则在读取了超过maxFrameLength的整个帧之后引发TooLongFrameException。
*/
public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
//略
}
  • maxFrameLength

    最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃。
  • lengthFieldOffset

    长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域。
  • lengthFieldLength

    长度域字节数。用几个字节来表示数据长度。
  • lengthAdjustment

    数据长度修正。因为长度域指定的长度可以是header body的整个长度,也可以只是body的长度。如果表示header body的整个长度,那么我们需要修正数据长度。
  • initialBytesToStrip

    跳过的字节数。如果你需要接收header body的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。
  • failFast

    如果为true,则在解码器注意到帧的长度将超过maxFrameLength时立即抛出TooLongFrameException,而不管是否已读取整个帧。

    如果为false,则在读取了超过maxFrameLength的整个帧之后引发TooLongFrameException。

下面通过Netty源码中LengthFieldBasedFrameDecoder的注释几个例子看一下参数的使用:

5.4.1、2 bytes length field at offset 0, do not strip header

本例中的length字段的值是12 (0x0C),它表示“HELLO, WORLD”的长度。默认情况下,解码器假定长度字段表示长度字段后面的字节数。

  • lengthFieldOffset = 0: 开始的2个字节就是长度域,所以不需要长度域偏移。
  • lengthFieldLength = 2: 长度域2个字节。
  • lengthAdjustment = 0: 数据长度修正为0,因为长度域只包含数据的长度,所以不需要修正。
  • initialBytesToStrip = 0: 发送和接收的数据完全一致,所以不需要跳过任何字节。

5.4.2、2 bytes length field at offset 0, strip header

因为我们可以通过调用readableBytes()来获得内容的长度,所以可能希望通过指定initialbystrip来删除长度字段。在本例中,我们指定2(与length字段的长度相同)来去掉前两个字节。

  • lengthFieldOffset = 0: 开始的2个字节就是长度域,所以不需要长度域偏移。
  • lengthFieldLength = 2 :长度域2个字节。
  • lengthAdjustment = 0: 数据长度修正为0,因为长度域只包含数据的长度,所以不需要修正。
  • initialBytesToStrip = 2 :我们发现接收的数据没有长度域的数据,所以要跳过长度域的2个字节。

5.4.3、2 bytes length field at offset 0, do not strip header, the length field represents the length of the whole message

在大多数情况下,length字段仅表示消息体的长度,如前面的示例所示。但是,在一些协议中,长度字段表示整个消息的长度,包括消息头。在这种情况下,我们指定一个非零长度调整。因为这个示例消息中的长度值总是比主体长度大2,所以我们指定-2作为补偿的长度调整。

  • lengthFieldOffset = 0: 开始的2个字节就是长度域,所以不需要长度域偏移。
  • lengthFieldLength = 2: 长度域2个字节。
  • lengthAdjustment = -2 :因为长度域为总长度,所以我们需要修正数据长度,也就是减去2。
  • initialBytesToStrip = 0 :发送和接收的数据完全一致,所以不需要跳过任何字节。

5.4.4、3 bytes length field at the end of 5 bytes header, do not strip header

下面的消息是第一个示例的简单变体。一个额外的头值被预先写入消息中。长度调整再次为零,因为译码器在计算帧长时总是考虑到预写数据的长度。

  • lengthFieldOffset = 2 :(= the length of Header 1)跳过2字节之后才是长度域
  • lengthFieldLength = 3:长度域3个字节。
  • lengthAdjustment = 0:数据长度修正为0,因为长度域只包含数据的长度,所以不需要修正。
  • initialBytesToStrip = 0:发送和接收的数据完全一致,所以不需要跳过任何字节。

5.4.5、3 bytes length field at the beginning of 5 bytes header, do not strip header

这是一个高级示例,展示了在长度字段和消息正文之间有一个额外头的情况。您必须指定一个正的长度调整,以便解码器将额外的标头计数到帧长度计算中。

  • lengthFieldOffset = 0:开始的就是长度域,所以不需要长度域偏移。
  • lengthFieldLength = 3:长度域3个字节。
  • lengthAdjustment = 2 :(= the length of Header 1) 长度修正2个字节,加2
  • initialBytesToStrip = 0:发送和接收的数据完全一致,所以不需要跳过任何字节。

5.4.6、2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field

这是上述所有示例的组合。在长度字段之前有预写的header,在长度字段之后有额外的header。预先设置的header会影响lengthFieldOffset,而额外的leader会影响lengthAdjustment。我们还指定了一个非零initialBytesToStrip来从帧中去除长度字段和预定的header。如果不想去掉预写的header,可以为initialBytesToSkip指定0。

  • lengthFieldOffset = 1 :(= the length of HDR1) ,跳过1个字节之后才是长度域
  • lengthFieldLength = 2:长度域2个字节
  • lengthAdjustment = 1: (= the length of HDR2)
  • initialBytesToStrip = 3 :(= the length of HDR1 LEN)

5.4.7、2 bytes length field at offset 1 in the middle of 4 bytes header, strip the first header field and the length field, the length field represents the length of the whole message

让我们对前面的示例进行另一个修改。与前一个示例的惟一区别是,length字段表示整个消息的长度,而不是消息正文的长度,就像第三个示例一样。我们必须把HDR1的长度和长度计算进长度调整里。请注意,我们不需要考虑HDR2的长度,因为length字段已经包含了整个头的长度。

  • lengthFieldOffset = 1:长度域偏移1个字节,之后才是长度域。
  • lengthFieldLength = 2:长度域2个字节。
  • lengthAdjustment = -3: (= the length of HDR1 LEN, negative)数据长度修正-3个字节。
  • initialBytesToStrip = 3:因为接受的数据比发送的数据少3个字节,所以跳过3个字节。

深入了解Netty【八】TCP拆包、粘包和解决方案的更多相关文章

  1. 架构师养成记--20.netty的tcp拆包粘包问题

    问题描述 比如要发ABC DEFG HIJK 这一串数据,其中ABC是一个包,DEFG是一个包,HIJK是一个包.由于TCP是基于流发送的,所以有可能出现ABCD EFGH 这种情况,那么ABC和D就 ...

  2. TCP拆包粘包之分隔符解码器

    TCP以流的方式进行数据传输,上层的应用协议为了对消息进行区分,往往采用如下4种方式. (1)消息长度固定,累计读取到长度总和为定长LEN的报文后,就认为读取到了一个完整的消息:将计数器置位,重新开始 ...

  3. 使用Netty如何解决拆包粘包的问题

    首先,我们通过一个DEMO来模拟TCP的拆包粘包的情况:客户端连续向服务端发送100个相同消息.服务端的代码如下: AtomicLong count = new AtomicLong(0); NioE ...

  4. tomcat Http11NioProtocol如何解析http请求及如何解决TCP拆包粘包

    前言 tomcat是常用的Web 应用服务器,目前国内有很多文章讲解了tomcat架构,请求流程等,但是没有如何解析http请求及如何解决TCP粘包拆包,所以这篇文章的目的就是介绍这块内容,一下内容完 ...

  5. Netty—TCP的粘包和拆包问题

    一.前言 虽然TCP协议是可靠性传输协议,但是对于TCP长连接而言,对于消息发送仍然可能会发生粘贴的情形.主要是因为TCP是一种二进制流的传输协议,它会根据TCP缓冲对包进行划分.有可能将一个大数据包 ...

  6. Netty处理TCP拆包、粘包

    Netty实践(二):TCP拆包.粘包问题-学海无涯 心境无限-51CTO博客 http://blog.51cto.com/zhangfengzhe/1890577 2017-01-09 21:56: ...

  7. 《精通并发与Netty》学习笔记(14 - 解决TCP粘包拆包(二)Netty自定义协议解决粘包拆包)

    一.Netty粘包和拆包解决方案 Netty提供了多个解码器,可以进行分包的操作,分别是: * LineBasedFrameDecoder (换行)   LineBasedFrameDecoder是回 ...

  8. Netty 拆包粘包和服务启动流程分析

    Netty 拆包粘包和服务启动流程分析 通过本章学习,笔者希望你能掌握EventLoopGroup的工作流程,ServerBootstrap的启动流程,ChannelPipeline是如何操作管理Ch ...

  9. netty的解码器和粘包拆包

    Tcp是一个流的协议,一个完整的包可能会被Tcp拆成多个包进行发送,也可能把一个小的包封装成一个大的数据包发送,这就是所谓的粘包和拆包问题 粘包.拆包出现的原因: 在流传输中出现,UDP不会出现粘包, ...

随机推荐

  1. @property@classmethod@staticmethod

    一.静态属性@property将方法标记成数据属性:可以访问实例和类的属性 @classmethod标记成类的方法,不需要实例化,可以类直接调用的方法.可以访问类的属性方法,不能访问实例的 class ...

  2. SpringBoot中使用AOP打印接口日志的方法(转载)

    前言 AOP 是 Aspect Oriented Program (面向切面)的编程的缩写.他是和面向对象编程相对的一个概念.在面向对象的编程中,我们倾向于采用封装.继承.多态等概念,将一个个的功能在 ...

  3. 3、Template Method 模板方法 行为型设计模式

    1.了解模板方法 1.1 模式定义 定义一个操作算法中的框架,而将这些步骤延迟加载到子类中. 它的本质就是固定算法框架. 1.2 解决何种问题 让父类控制子类方法的调用顺序 模板方法模式使得子类可以不 ...

  4. Spring Boot Logback 默认配置

    Spring Boot Logback 默认配置 标签(空格分隔): Spring Boot Intro(介绍) Spring Boot 1.5.9 默认使用的日志框架是 Logback. 生效的默认 ...

  5. Consul服务治理发现学习记录

    Consul 简介 Consul是一个服务网格(微服务间的 TCP/IP,负责服务之间的网络调用.限流.熔断和监控)解决方案,它是一个一个分布式的,高度可用的系统,而且开发使用都很简便.它提供了一个功 ...

  6. 性能分析(4)- iowait 使用率过高案例

    性能分析小案例系列,可以通过下面链接查看哦 https://www.cnblogs.com/poloyy/category/1814570.html 前言 前面两个案例讲的都是上下文切换导致的 CPU ...

  7. 2018-04-19:innodb和myisam区别

    福哥答案2020-04-19:

  8. C#算法设计排序篇之10-桶排序(附带动画演示程序)

    桶排序(Bucket Sort) 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/693 访问. 桶排序的工作原理是将数组 ...

  9. 【POJ2728】Desert King - 01分数规划

    Description David the Great has just become the king of a desert country. To win the respect of his ...

  10. 《MySQL必知必会》简介、使用

    2.MySQL简介 2.1 什么是MySQL 我们在前一-章中介绍了数据库和SQL.正如所述,数据的所有存储. 检索.管理和处理实际上是由数据库软件一 DBMS (数据库管理系统) 完成的. MySQ ...