Netty 中 TCP 粘包拆包问题

信息通过tcp传输过程中出现的状况 .

TCP是个“流”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送

产生粘包和拆包问题的主要原因是,操作系统在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。

入图所示:

上图中演示了粘包和拆包的三种情况:

  • D1和D2两个包都刚好满足TCP缓冲区的大小,或者说其等待时间已经达到TCP等待时长,从而还是使用两个独立的包进行发送;
  • D1和D2两次请求间隔时间内较短,并且数据包较小,因而合并为同一个包发送给服务端;
  • 某一个包比较大,因而将其拆分为两个包D*_1和D*_2进行发送,而这里由于拆分后的某一个包比较小,其又与另一个包合并在一起发送。

发生这种情况的代码:

客户端发送数据 快速的发送 10条数据 :

public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    private int count;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//使用客户端发送10条数据 hello,server 编号
for(int i= 0; i< 10; ++i) {
ByteBuf buffer = Unpooled.copiedBuffer("hello,server " +i, Charset.forName("utf-8"));
ctx.writeAndFlush(buffer);
}
} }

服务端接受打印:

服务器接收到数据 hello,server 0
服务器接收到数据 hello,server 1
服务器接收到数据 hello,server 2hello,server 3
服务器接收到数据 hello,server 4hello,server 5
服务器接收到数据 hello,server 6
服务器接收到数据 hello,server 7hello,server 8
服务器接收到数据 hello,server 9

很明显 其中有三条记录被粘在其他数据上,这就是TCP的粘包拆包现象

怎么解决:

  1. Netty自带的 解决方案:

    • 固定长度的拆包器 FixedLengthFrameDecoder,每个应用层数据包的都拆分成都是固定长度的大小

    • 行拆包器 LineBasedFrameDecoder,每个应用层数据包,都以换行符作为分隔符,进行分割拆分

    • 分隔符拆包器 DelimiterBasedFrameDecoder,每个应用层数据包,都通过自定义的分隔符,进行分割拆分

    • 基于数据包长度的拆包器 LengthFieldBasedFrameDecoder,将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度

FixedLengthFrameDecoder 解码器

服务端 添加 FixedLengthFrameDecoder 解码器 并指定长度

public class EchoServer {

  public static void main(String[] args) throws InterruptedException {

      EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//指定长度为9 则每次截取长度为9的字节
ch.pipeline().addLast(new FixedLengthFrameDecoder(9));
// 将 每次截取的字节编码为字符串
ch.pipeline().addLast(new StringDecoder());
//自定义处理类打印
ch.pipeline().addLast(new EchoServerHandler());
}
}); ChannelFuture future = bootstrap.bind(8000).sync();
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}

自定义服务端Handler 打印字符串:

public class EchoServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("message: " + msg.trim());
}
}

客户端发送信息 并添加字符串编码器 将信息已字符串的形式编码:

public class EchoClient {

  public static void main(String[] args) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new EchoClientHandler());
}
}); ChannelFuture future = bootstrap.connect("127.0.0.1", 8000).sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}

客户端Handler 发送信息 刚好长度为9 :

public class EchoClientHandler extends SimpleChannelInboundHandler<String> {

  @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("123456789");
}
}

总结: FixedLengthFrameDecoder 解码器 将按照指定长度截取字节 并添加到List中向后传递 , 以本案例为例,如果字节数刚好为9,则全部打印,如果 字节数为18, 则拆分打印两次,如果为19 则最后一个字节不打印,如果不足9 则什么都不打印.

LineBasedFrameDecoder 行拆分器

通过行换行符 \n 或者 \r\n 进行分割,

将上面案例的FixedLengthFrameDecoder 解码器 换成 LineBasedFrameDecoder

并指定 截取每段的最大长度 (超过报错 不往后传递)

...

.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception { // ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
ch.pipeline().addLast(new LineBasedFrameDecoder(5));
// 将前一步解码得到的数据转码为字符串
ch.pipeline().addLast(new StringDecoder());
// 最终的数据处理
ch.pipeline().addLast(new EchoServerHandler());
}
}); ...

客户端Handler 发送字符串, 最后的"1234" 不会打印,,

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("1\n123456\r\n1234");
}

服务端接收并打印结果 分别打印了 "1" 和 "1234" 而超过字节长度5 的 "123456"则报出TooLongFrameException错误

server receives message: 1

An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
io.netty.handler.codec.TooLongFrameException: frame length (6) exceeds the allowed maximum (5) server receives message: 1234

DelimiterBasedFrameDecoder 自定义分割符

和行分割符类似, 此解码器可以自定义分割符,常用构造方法:

 public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf... delimiters)

接收一个最大长度,和 任意个数的 分隔符(用ByteBuf的形式传入),解码器识别到任意一个 分割符 都会进行拆分

注册解码器:

传入 "$" 和 "*" 作为分割符,并指定最大长度为 5个字节

.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new DelimiterBasedFrameDecoder(5,
Unpooled.wrappedBuffer("$".getBytes()),Unpooled.wrappedBuffer("*".getBytes())));
// 将前一步解码得到的数据转码为字符串
ch.pipeline().addLast(new StringDecoder()); // 最终的数据处理
ch.pipeline().addLast(new EchoServerHandler());
}
});

客户端 发送数据:

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("1$123456*1234$789$");
}

服务端只打印了 "1" 当解析到 "123456" 时 就报错了 后面就没有再解析了,会缓存着 等到该通道关闭 或者有后续数据发送过来时 才继续解析

LengthFieldBasedFrameDecoder

自定义数据长度,发送的 字节数组中 包含 描述 数据长度的字段 和 数据本身,

解码过程

常用字段:

  • maxFrameLength:指定了每个包所能传递的最大数据包大小,(上图中的最大长度为11)
  • lengthFieldOffset:指定了长度字段在字节码中的偏移量;(11这个描述长度的数据是在数组的第几位开始)
  • lengthFieldLength:指定了长度字段所占用的字节长度;(11 占 1个字节)
  • lengthAdjustment: 长度域的偏移量矫正。 如果长度域的值,除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长 - 长度域的值 – 长度域偏移 – 长度域长。 ( 11 这个域 不光光描述 Hello,world, 一般设置为0,)
  • initialBytesToStrip : 丢弃的起始字节数。丢弃处于有效数据前面的字节数量。比如前面有1个节点的长度域,则它的值为1. ( 如果为0代表不丢弃,则将长度域也向后传递)

服务端添加 解码器:

  • 最大长度 为 长度描述域 的值11 + 长度描述域本身占用的长度 1 = 12
  • 长度描述域放在数据包的第一位, 没有偏移 为0
  • 长度描述域 长度为1
  • 无需矫正
  • 一个字节也不丢弃
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 这里将FixedLengthFrameDecoder添加到pipeline中,指定长度为20
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(12,0,1,0,0));
// 将前一步解码得到的数据转码为字符串 ch.pipeline().addLast(new StringDecoder());
// 最终的数据处理
ch.pipeline().addLast(new EchoServerHandler());
}
});

客户端发送数据 发送最Netty 底层操作 的ByteBuf对象 发送时 无需任何编码:

 @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buffer = Unpooled.buffer();
buffer.writeByte(11);
buffer.writeBytes("Hello,World".getBytes());
ctx.writeAndFlush(buffer);
}

服务端接收数据为 (11代表的制表符)Hello,World

这样发送 每次都要计算 数据长度,并手动添加到 数据的前面,很不方便 配合LengthFieldPrepender 使用,这个编码码器可以计算 长度,并自动添加到 数据的前面

改造客户端 先拦截数据按字符串编码,再计算字节长度 添加 长度描述字段 并占用一个字节 (这个长度要与客户端的解码器 lengthFieldLength参数 值保持一致) :

 .handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LengthFieldPrepender(1));
ch.pipeline().addLast(new StringEncoder());
// 客户端发送消息给服务端,并且处理服务端响应的消息
ch.pipeline().addLast(new EchoClientHandler());
}
});

客户端发送 有字符串编码器 可以直接发送字符串:

  @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("Hello,World");
}

自定义协议

上面介绍的 各种解码器 已经可以应付绝大多数场景, 如果遇到 特殊的状况 我们也可以自定义协议

定义 协议对象:

//协议包
public class MessageProtocol {
private int len; //关键
private byte[] content; public int getLen() {
return len;
} public void setLen(int len) {
this.len = len;
} public byte[] getContent() {
return content;
} public void setContent(byte[] content) {
this.content = content;
}
}

客户端发送:

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { for(int i = 0; i< 5; i++) {
String mes = "Hello,World";
byte[] content = mes.getBytes(Charset.forName("utf-8"));
int length = mes.getBytes(Charset.forName("utf-8")).length; //创建协议包对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
ctx.writeAndFlush(messageProtocol); }
}

该协议的 自定义 编码器 将协议包发送出去:

public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
System.out.println("MyMessageEncoder encode 方法被调用");
out.writeInt(msg.getLen());
out.writeBytes(msg.getContent());
}
}

将客户端 发送数据的Handler 和 编码器 注册 这里就不写了

服务端解码器 读取长度 并 判断可读数据的长度是否足够 :

public class MyMessageDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { in.markReaderIndex(); //读取长度
int length = in.readInt();
//如果可读长度大于 数据长度 说明数据完整
if (in.readableBytes()>length){
byte[] content = new byte[length];
in.readBytes(content);
//封装成 MessageProtocol 对象,放入 out, 传递下一个handler业务处理
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
out.add(messageProtocol);
}else{
//如果数据不够长 将已经读过的的int 数据还原回去 留下次读取
in.resetReaderIndex();
}
}
}

服务端成功读取:

本例中存在很多问题, 明白这个意思就行, 感兴趣的话 可以 自己动手优化

Netty笔记(6) - 粘包拆包问题及解决方案的更多相关文章

  1. 1. Netty解决Tcp粘包拆包

    一. TCP粘包问题 实际发送的消息, 可能会被TCP拆分成很多数据包发送, 也可能把很多消息组合成一个数据包发送 粘包拆包发生的原因 (1) 应用程序一次写的字节大小超过socket发送缓冲区大小 ...

  2. netty之==TCP粘包/拆包问题解决之道(一)

    一.TCP粘包/拆包是什么 TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在 ...

  3. TCP 粘包 - 拆包问题及解决方案

    目录 TCP粘包拆包问题 什么是粘包 - 拆包问题 为什么存在粘包 - 拆包问题 粘包 - 拆包 演示 粘包 - 拆包 解决方案 方式一: 固定缓冲区大小 方式二: 封装请求协议 方式三: 特殊字符结 ...

  4. Netty的TCP粘包/拆包(源码二)

    假设客户端分别发送了两个数据包D1和D2给服务器,由于服务器端一次读取到的字节数是不确定的,所以可能发生四种情况: 1.服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包. 2.服 ...

  5. Netty解决TCP粘包/拆包问题 - 按行分隔字符串解码器

    服务端 package org.zln.netty.five.timer; import io.netty.bootstrap.ServerBootstrap; import io.netty.cha ...

  6. 深入学习Netty(5)——Netty是如何解决TCP粘包/拆包问题的?

    前言 学习Netty避免不了要去了解TCP粘包/拆包问题,熟悉各个编解码器是如何解决TCP粘包/拆包问题的,同时需要知道TCP粘包/拆包问题是怎么产生的. 在此博文前,可以先学习了解前几篇博文: 深入 ...

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

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

  8. 《精通并发与Netty》学习笔记(13 - 解决TCP粘包拆包(一)概念及实例演示)

    一.粘包/拆包概念 TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认 ...

  9. Netty(三)TCP粘包拆包处理

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

  10. Netty(二)——TCP粘包/拆包

    转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7814644.html 前面讲到:Netty(一)--Netty入门程序 主要内容: TCP粘包/拆包的基础知 ...

随机推荐

  1. 对象中是否有某一个属性是否存在有三种方法 in hasOwnProperty Object.hasOwn

    如何看某个对象中没有某一个属性 如果我们要检测对象是否拥有某一属性,可以用in操作符 var obj= { name: '类老师', age: 18, school: '家具' }; console. ...

  2. 【代码片段分享】比 url.QueryEscape 快 7.33 倍的 FastQueryEscape

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 做 profile 发现 url.QueryEscape ...

  3. docker容器中部署 kafka 和 elk

    1.下载zookeeper docker pull wurstmeister/zookeeper 2.下载kafka docker pull wurstmeister/kafka:2.11-0.11. ...

  4. CE修改器入门:运用代码注入

    从本关开始,各位会初步接触到CE的反汇编功能,这也是CE最强大的功能之一.在第6关的时候我们说到指针的找法,用基址定位动态地址.但这一关不用指针也可以进行修改,即使对方是动态地址,且功能更加强大. 代 ...

  5. LeetCode刷题日记2020/8/24

    题目描述 给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成.给定的字符串只含有小写英文字母,并且长度不超过10000. 示例 1: 输入: "abab" 输出: Tr ...

  6. 零基础入门学习JAVA课堂笔记 ——DAY08

    异常 1.什么是异常? Exception 异常是指程序在运行过程中出现的不期而至的各种状况 异常发生在程序运行期间,它影响了正常程序执行流程 通俗易懂的表达就是,程序在发生意料之外或者拿到的不是想要 ...

  7. 单片机 IAP 功能进阶开发篇之BOOT升级(一)

    引言 目的 主要介绍单片机 IAP 开发的设计思路,如何不使用下载烧录器的方式对单片机的程序进行升级,升级区域包括 bootloader 和用户程序的升级,升级方式有 UASRT 通信.CAN 通信和 ...

  8. 《ASP.ENT Core 与 RESTful API 开发实战》-- (第6章)-- 读书笔记(上)

    第 6 章 高级查询和日志 6.1 分页 在 EF Core 中,数据的查询通过集成语言查询(LINQ)实现,它支持强类型,支持对 DbContext 派生类的 DbSet 类型成员进行访问,DbSe ...

  9. es6 快速入门 系列 —— 模块

    其他章节请看: es6 快速入门 系列 模块 es6 以前,每个 javascript 都共享这一个全局作用域,随着代码量的增加,容易引发一些问题,比如命名冲突. 其他语言有包这样的概念来定义作用域, ...

  10. Java设计模式-建造者模式Builder

    介绍 建造者模式(Builder Pattern) 又叫生成器模式,是一种对象构建模式.它可以 将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方 法可以构造出不同表现(属性)的对象 ...