Netty—TCP的粘包和拆包问题
一.前言
虽然TCP协议是可靠性传输协议,但是对于TCP长连接而言,对于消息发送仍然可能会发生粘贴的情形。主要是因为TCP是一种二进制流的传输协议,它会根据TCP缓冲对包进行划分。有可能将一个大数据包拆分成多个小的数据包,也有可能将多个小的数据包合并成一个数据包。
本篇文章将对TCP粘包和拆包进行介绍:
- TCP粘包拆包问题及现象
- 解决方式
### 二.TCP粘包拆包问题及现象
假设Client端发送两个数据包给Server端,如下图:
但是Server端实际接收到的数据包形式可能存在以上三种形式:
- 第一种形式是接收到一个数据包,其中客户端发送的两个数据包粘贴在一起。如:client端分别发送两个数据包都为Hello World,但是Server端只收到一个数据包为Hello WorldHello World。这就属于TCP粘包
- 第二种形式是先接受到了第一个数据包的一部分,然后又接收到了另外一部分和第二个数据包粘贴。同样以上例为准,Server端先接收到了Hell,然后又接收到了o WorldHello World。这属于TCP拆包
- 第三种形式也是接收到两个数据包,但是是先接收到了第一个数据包和第二个数据包的一部分的粘贴,然后又接收到第二个数据包的另外一部分。这就属于TCP粘包拆包
无论是以上哪种情况,从应用层的角度而言,Server端都将处理错误。首先以没有考虑TCP拆包和粘包的场景为例,分析下TCP拆包粘包将造成什么样的现象:
1.客户端编码:
public static class EchoClientHandler extends ChannelHandlerAdapter {
static final String ECHO_REQ = "Hi, huaijin.Welcome to Netty.";
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 发送100次消息至server端
for (int i = 0; i < 100; i++) {
System.out.println("This is " + (i + 1) + " times send server: [" + ECHO_REQ + "]");
ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes()));
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
这里关于Client的启动代码省略,重点关注业务Handler。其中Echo总共发送了100次消息给Server,如果按照正确的情形,Server端应该接受到100次,然后分别进行处理。但是实际的情形并不是这样。
2.服务端编码
public static class EchoServerHandler extends ChannelHandlerAdapter {
/**
* 原子计数器,统计接受到的次数
*/
private AtomicInteger counter = new AtomicInteger(0);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 接受到消息打印
String body = (String) msg;
System.out.println("This is " + counter.incrementAndGet() + " times receive client: [" + body + "]");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
服务端中使用原子计数器统计接收到的包的次数并打印接受到的消息。下面运行下实例,客户端输出如下:
This is 1 times send server: [Hi, huaijin.Welcome to Netty.]
This is 2 times send server: [Hi, huaijin.Welcome to Netty.]
This is 3 times send server: [Hi, huaijin.Welcome to Netty.]
This is 4 times send server: [Hi, huaijin.Welcome to Netty.]
.... 中间部分省略
This is 97 times send server: [Hi, huaijin.Welcome to Netty.]
This is 98 times send server: [Hi, huaijin.Welcome to Netty.]
This is 99 times send server: [Hi, huaijin.Welcome to Netty.]
This is 100 times send server: [Hi, huaijin.Welcome to Netty.]
从中可以看出,Client端总共发送了100条消息至Server,但是Server端接收情况如下:
This is 1 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 2 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.]
This is 3 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.]
This is 4 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.]
This is 5 times receive client: [Hi, huaijin.Welcome to Netty.]
... 省略
This is 69 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 70 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.Hi, hu]
This is 71 times receive client: [aijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.]
This is 72 times receive client: [Hi, huaijin.Welcome to Netty.]
... 省略
This is 84 times receive client: [Hi, huaijin.Welcome to Netty.Hi, huaijin.Welcome to Netty.]
This is 93 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 94 times receive client: [Hi, huaijin.Welcome to Netty.]
由于发生了粘包导致Server端只接收到94次,其中有两条消息粘合在一起。
有以上的情形可以看出,当应用使用长连接并发发送请求时,会造成Server端接收到的请求数据发生混乱,从而处理错误。
### 三.解决方式
关于TCP拆包粘包的解决方式有很多,目前的主流解决方式有以下几种:
- 使用定长消息,Client和Server双方约定报文长度,Server端接受到报文后,按指定长度解析;
- 使用特定分隔符,比如在消息尾部增加分隔符。Server端接收到报文后,按照特定的分割符分割消息后,再解析;
- 将消息分割为消息头和消息体两部分,消息头中指定消息或者消息体的长度,通常设计中使用消息头第一个字段int32表示消息体的总长度;
当然netty作为成熟框架,提供了多种方式解决TCP的拆包粘包问题,通常称作为半包解码器。
netty中提供了基于分隔符实现的半包解码器和定长的半包解码器:
- LineBasedFrameDecoder使用"\n"和"\r\n"作为分割符的解码器
- DelimiterBasedFrameDecoder使用自定义的分割符的解码器
- FixedLengthFrameDecoder定长解码器
这里仍然以上例为主,使用DelimiterBasedFrameDecoder作为半包解码器。
1.客户端编码
public static class EchoClientHandler extends ChannelHandlerAdapter {
/**
* 消息使用"$_"分割
*/
static final String ECHO_REQ = "Hi, huaijin.Welcome to Netty.$_";
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("This is " + (i + 1) + " times send server: [" + ECHO_REQ + "]");
ctx.writeAndFlush(Unpooled.copiedBuffer(ECHO_REQ.getBytes()));
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
客户端代码改动较小,只是每条消息后使用分割符"$_"分割,然后发送消息。
2.服务端编码
服务端需要使用分割符解码器,利用其对粘包消息进行拆分:
/**
* netty实现echo server
*
* @author huaijin
*/
public class EchoServer {
public void bind(int port) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 使用分隔符"$_"的半包解码器
ByteBuf byteBuf = Unpooled.copiedBuffer("$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, byteBuf));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new EchoServerHandler());
}
});
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new EchoServer().bind(8080);
}
public static class EchoServerHandler extends ChannelHandlerAdapter {
/**
* 原子计数器,统计接受到的次数
*/
private AtomicInteger counter = new AtomicInteger(0);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 接受到消息打印
String body = (String) msg;
System.out.println("This is " + counter.incrementAndGet() + " times receive client: [" + body + "]");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
当再次运行客户端和服务端代码时,服务端表现正常,接收到了100次:
This is 1 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 2 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 3 times receive client: [Hi, huaijin.Welcome to Netty.]
... 省略
This is 98 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 99 times receive client: [Hi, huaijin.Welcome to Netty.]
This is 100 times receive client: [Hi, huaijin.Welcome to Netty.]
### 四.总结
本篇文章主要介绍了什么是TCP的拆包和粘包,并展示了拆包和粘包带来的现象。并通过netty提供的方案,是如何解决TCP拆包和粘包问题。
Netty—TCP的粘包和拆包问题的更多相关文章
- tcp的粘包和拆包示例以及使用LengthFieldFrameDecoder来解决的方法
粘包和拆包是什么? TCP协议是一种字节流协议,没有记录边界,我们在接收消息的时候,不能人为接收到的数据包就是一个整包消息 当客户端向服务器端发送多个消息数据的时候,TCP协议可能将多个消息数据合并成 ...
- Netty 中的粘包和拆包
Netty 底层是基于 TCP 协议来处理网络数据传输.我们知道 TCP 协议是面向字节流的协议,数据像流水一样在网络中传输那何来 "包" 的概念呢? TCP是四层协议不负责数据逻 ...
- 关于TCP的粘包和拆包
问题产生 一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题. 下面可以看一张图,是客户端向服务端发送包: 1. 第一种情况 ...
- TCP的粘包和拆包问题及解决
前言 TCP属于传输层的协议,传输层除了有TCP协议外还有UDP协议.那么UDP是否会发生粘包或拆包的现象呢?答案是不会.UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit ...
- TCP的粘包和拆包问题及解决办法(C#)
本文参考:https://blog.csdn.net/wxy941011/article/details/80428470 原因 如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数 ...
- TCP的粘包、拆包及解决方法
TCP粘包,拆包及解决方法 粘包拆包问题是处于网络比较底层的问题,在数据链路层.网络层以及传输层都有可能发生.我们日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因 ...
- netty 解决TCP粘包与拆包问题(一)
1.什么是TCP粘包与拆包 首先TCP是一个"流"协议,犹如河中水一样连成一片,没有严格的分界线.当我们在发送数据的时候就会出现多发送与少发送问题,也就是TCP粘包与拆包.得不到我 ...
- 【Netty】TCP粘包和拆包
一.前言 前面已经基本上讲解完了Netty的主要内容,现在来学习Netty中的一些可能存在的问题,如TCP粘包和拆包. 二.粘包和拆包 对于TCP协议而言,当底层发送消息和接受消息时,都需要考虑TCP ...
- Netty入门系列(2) --使用Netty解决粘包和拆包问题
前言 上一篇我们介绍了如果使用Netty来开发一个简单的服务端和客户端,接下来我们来讨论如何使用解码器来解决TCP的粘包和拆包问题 TCP为什么会粘包/拆包 我们知道,TCP是以一种流的方式来进行网络 ...
随机推荐
- [C]#include和链接
概述 对于刚接触C语言的同学来说,通常对“在文件中用#include预处理操作符引入文件”和“编译时链接多个文件”这两个操作会有所混淆,这个文章主要为了解析一下它们的区别. #include预处理操作 ...
- 推荐系统| ② 离线推荐&基于隐语义模型的协同过滤推荐
一.离线推荐服务 离线推荐服务是综合用户所有的历史数据,利用设定的离线统计算法和离线推荐算法周期性的进行结果统计与保存,计算的结果在一定时间周期内是固定不变的,变更的频率取决于算法调度的频率. 离线推 ...
- 调试seanbell/intrinsic遇到的坑
那些遗忘过去的人注定要重蹈覆辙.——乔治•桑塔亚纳 Authorized error 刚开始按作者 GitHub 上的指示,当运行环境配置好,并且 make 之后,因为生成的 decompose.p ...
- 【Java基础】JDBC简明教程
目录 1. 常用类 2. JDBC编程步骤 3. 事务处理 4. 数据库连接池 5. JDBC列子代码 6. 使用Apache的JDBC工具类 虽然在平时的开发过程中我们不会直接使JDBC的API来操 ...
- Prism_简介(1)
Prism 6 Introduction介绍 Initializing初始化 Managing-Dependencies管理依赖 Modules模块 Implementing-MVVM实时MVVM A ...
- .netcore控制台->定时任务Quartz
之前做数据同步时,用过timer.window服务,现在不用那么费事了,可以使用Quartz,并且配置灵活,使用cron表达式配置XML就可以.我用的是3.0.7版本支持.netcore. 首先创建一 ...
- 从零开始制作cli工具,快速创建项目脚手架
背景 在工作过程中,我们常常会从一个项目工程复制代码到一个新的项目,改项目配置信息.删除不必要的代码. 这样做的效率比较低,也挺繁琐,更不易于分享协作. 所以,我们可以制作一个cli工具,用来快速创建 ...
- 对cookie-parser的理解(签名、加密)
1.为什么说要利用签名防止cookie被恶意篡改 我们在浏览器输入用户名和密码发送post请求到后端服务器,后端服务器验证合法,返回响应,并Set-Cookie为sessionid=***;usern ...
- Pycharm2019最新激活注册码(pycharm激活教程)
给大家分享一下PyCharm2019最新可用的激活注册码.激活Pycharm专业版的方法有很多,这里主要给大家分享最有效的两种,一种是使用最新可用的注册激活码,一种是使用破解补丁的方法,这种方法虽然麻 ...
- SQL Server如何查看存储过程的执行计划
有时候,我们需要查看存储过程的执行计划,那么我们有什么方式获取存储过程的历史执行计划或当前的执行计划呢? 下面总结一下获取存储过程的执行计划的方法. 1:我们可以通过下面脚本查看存储过程的执行计划,但 ...