Netty中粘包和拆包的解决方案
粘包和拆包是TCP网络编程中不可避免的,无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制。
TCP粘包和拆包
TCP是个“流”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
如图所示,假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
- 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
- 服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
- 服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包
- 服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
TCP粘包和拆包产生的原因
数据从发送方到接收方需要经过操作系统的缓冲区,而造成粘包和拆包的主要原因就在这个缓冲区上。粘包可以理解为缓冲区数据堆积,导致多个请求数据粘在一起,而拆包可以理解为发送的数据大于缓冲区,进行拆分处理。
详细来说,造成粘包和拆包的原因主要有以下三个:
- 应用程序write写入的字节大小大于套接口发送缓冲区大小
- 进行MSS大小的TCP分段
- 以太网帧的payload大于MTU进行IP分片。
粘包和拆包的解决方法
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。
- 消息长度固定,累计读取到长度和为定长LEN的报文后,就认为读取到了一个完整的信息
- 将回车换行符作为消息结束符
- 将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符
- 通过在消息头中定义长度字段来标识消息的总长度
Netty中的粘包和拆包解决方案
针对上一小节描述的粘包和拆包的解决方案,对于拆包问题比较简单,用户可以自己定义自己的编码器进行处理,Netty并没有提供相应的组件。对于粘包的问题,由于拆包比较复杂,代码比较处理比较繁琐,Netty提供了4种解码器来解决,分别如下:
- 固定长度的拆包器 FixedLengthFrameDecoder,每个应用层数据包的都拆分成都是固定长度的大小
- 行拆包器 LineBasedFrameDecoder,每个应用层数据包,都以换行符作为分隔符,进行分割拆分
- 分隔符拆包器 DelimiterBasedFrameDecoder,每个应用层数据包,都通过自定义的分隔符,进行分割拆分
- 基于数据包长度的拆包器 LengthFieldBasedFrameDecoder,将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度
以上解码器在使用时只需要添加到Netty的责任链中即可,大多数情况下这4种解码器都可以满足了,当然除了以上4种解码器,用户也可以自定义自己的解码器进行处理。具体可以参考以下代码示例:
// Server主程序
public class XNettyServer {
public static void main(String[] args) throws Exception {
// accept 处理连接的线程池
NioEventLoopGroup acceptGroup = new NioEventLoopGroup();
// read io 处理数据的线程池
NioEventLoopGroup readGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(acceptGroup, readGroup)
.channel(NioServerSocketChannel.class)
.childHandler(
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 增加解码器
pipeline.addLast(new XDecoder());
// 打印出内容 handdler
pipeline.addLast(new XHandler());
}
});
System.out.println("启动成功,端口 7777");
serverBootstrap.bind(7777).sync().channel().closeFuture().sync();
} finally {
acceptGroup.shutdownGracefully();
readGroup.shutdownGracefully();
}
}
}
// 解码器
public class XDecoder extends ByteToMessageDecoder {
static final int PACKET_SIZE = 220;
// 用来临时保留没有处理过的请求报文
ByteBuf tempMsg = Unpooled.buffer();
/**
* @param ctx
* @param in 请求的数据
* @param out 将粘在一起的报文拆分后的结果保留起来
* @throws Exception
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println(Thread.currentThread() + "收到了一次数据包,长度是:" + in.readableBytes());
// 合并报文
ByteBuf message = null;
int tmpMsgSize = tempMsg.readableBytes();
// 如果暂存有上一次余下的请求报文,则合并
if (tmpMsgSize > 0) {
message = Unpooled.buffer();
message.writeBytes(tempMsg);
message.writeBytes(in);
System.out.println("合并:上一数据包余下的长度为:" + tmpMsgSize + ",合并后长度为:" + message.readableBytes());
} else {
message = in;
}
int size = message.readableBytes();
int counter = size / PACKET_SIZE;
for (int i = 0; i < counter; i++) {
byte[] request = new byte[PACKET_SIZE];
// 每次从总的消息中读取220个字节的数据
message.readBytes(request);
// 将拆分后的结果放入out列表中,交由后面的业务逻辑去处理
out.add(Unpooled.copiedBuffer(request));
}
// 多余的报文存起来
// 第一个报文: i+ 暂存
// 第二个报文: 1 与第一次
size = message.readableBytes();
if (size != 0) {
System.out.println("多余的数据长度:" + size);
// 剩下来的数据放到tempMsg暂存
tempMsg.clear();
tempMsg.writeBytes(message.readBytes(size));
}
}
}
// 处理器
public class XHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
byte[] content = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(content);
System.out.println(Thread.currentThread() + ": 最终打印" + new String(content));
((ByteBuf) msg).release();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

Netty中粘包和拆包的解决方案的更多相关文章
- Netty解决粘包和拆包问题的四种方案
在RPC框架中,粘包和拆包问题是必须解决一个问题,因为RPC框架中,各个微服务相互之间都是维系了一个TCP长连接,比如dubbo就是一个全双工的长连接.由于微服务往对方发送信息的时候,所有的请求都是使 ...
- Netty入门系列(2) --使用Netty解决粘包和拆包问题
前言 上一篇我们介绍了如果使用Netty来开发一个简单的服务端和客户端,接下来我们来讨论如何使用解码器来解决TCP的粘包和拆包问题 TCP为什么会粘包/拆包 我们知道,TCP是以一种流的方式来进行网络 ...
- 如何基于Netty处理粘包、拆包问题?
涉及到相关重要组件: ByteToMessageDecoder MessageToMessageDecoder 这两个组件都实现了ChannelInboundHandler接口,这说明这两个组件都是用 ...
- 服务端NETTY 客户端非NETTY处理粘包和拆包的问题
之前为了调式和方便一直没有处理粘包的问题,今天专门花了时间来搞NETTY的粘包处理,要知道在高并发下,不处理粘包是不可能的,数据流的混乱会造成业务的崩溃什么的我就不说了.所以这个问题 在我心里一直是个 ...
- 2.Netty的粘包、拆包(一)
Netty粘包.拆包 1.什么是拆包.粘包 (1)拆包.粘包介绍 TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间并没有分界线.TC ...
- 3.Netty的粘包、拆包(二)
Netty提供的TCP数据拆包.粘包解决方案 1.前言 关于TCP的数据拆包.粘包的介绍,我在上一篇文章里面已经有过介绍. 想要了解一下的,请点击这里 Chick Here! 今天我们要讲解的是Net ...
- TCP粘包、拆包
TCP粘包.拆包 熟悉tcp编程的可能都知道,无论是服务端还是客户端,当我们读取或发送数据的时候,都需要考虑TCP底层的粘包/拆包机制. TCP是一个“流”协议,所谓流就是没有界限的遗传数据.可以想象 ...
- Netty4 学习笔记之三:粘包和拆包
前言 在上一篇Netty 心跳 demo 中,了解了Netty中的客户端和服务端之间的心跳.这篇就来讲讲Netty中的粘包和拆包以及相应的处理. 名词解释 粘包: 会将消息粘粘起来发送.类似吃米饭,一 ...
- 粘包和拆包及Netty解决方案
在RPC框架中,粘包和拆包问题是必须解决一个问题,因为RPC框架中,各个微服务相互之间都是维系了一个TCP长连接,比如dubbo就是一个全双工的长连接.由于微服务往对方发送信息的时候,所有的请求都是使 ...
随机推荐
- lightoj 1382 - The Queue(树形dp)
题目链接:http://www.lightoj.com/volume_showproblem.php?problem=1382 题解:简单的树形dp加上组合数学. #include <iostr ...
- HDU - 3966 树链刨分
题目传送门 操作就是询问某个点的值, 然后就是对一条路径上的值全部修改. 最基本的树刨题目了. 树刨的思想: 1. 对于每个点找到他的重儿子. void dfs1(int o, int u){ sz[ ...
- Unity 3D,地形属性
Terrain Width 地形高度 Terrain Height 地形宽度 Terrain Lenght 地形长度 HeughtMap Resolution 地形高度图的分辨率 Detail Re ...
- webpack4 公共模块打包,怎么抽取出来一个需要经常修改的包
项目中有一个需求: 所有页面引用了一个公共ad.js 因为广告需要不断投放新渠道,所以ad.js需要经常上线更新,这样会导致打包出来的 commons.js经常更新,缓存一下无效了.所以急需将ad.j ...
- JS-DOM ~ 01. 了解DOM,动手做一下就明白了!/鼠标事件(好吧 其实我卡了三天
DOM概述 html加载完毕,渲染引擎会在内存中把html文档生成一个DOM树,getElementById是获取内DOM上的元素,然后操作的时候修改的是该元素的属性 体验事件/事件三要素1.事件源( ...
- 【Offer】[57-1] 【和为S的两个数字】
题目描述 思路分析 测试用例 Java代码 代码链接 题目描述 输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s.如果有多对数字的和等于s,则输出任意一对即可. 牛客网刷题 ...
- Toad客户端连接Oracle数据库报错 ORA-12170:TNS:连接超时
Oracle 客户端连接Oracle数据库报错 ORA-12170:TNS:连接超时 排错步骤: 1. 查看网络是否畅通: 打开cmd,ping数据库IP 2. 查看端口是否畅通: 打开cmd ...
- SpringCloud学习笔记(3):使用Feign实现声明式服务调用
简介 Feign是一个声明式的Web Service客户端,它简化了Web服务客户端的编写操作,相对于Ribbon+RestTemplate的方式,开发者只需通过简单的接口和注解来调用HTTP API ...
- 搭建自己的技术博客系列(二)把 Hexo 博客部署到 GitHub 上
1.在GitHub上建一个新仓库
- 上传文件的C#代码
1 <%@ WebHandler Language="C#" Class="UpLoadFile" %> 2 3 using System; 4 u ...