TCP 拆、粘包
Netty(三) 什么是 TCP 拆、粘包?如何解决?
前言
记得前段时间我们生产上的一个网关出现了故障。
这个网关逻辑非常简单,就是接收客户端的请求然后解析报文最后发送短信。
但这个请求并不是常见的 HTTP ,而是利用 Netty 自定义的协议。
有个前提是:网关是需要读取一段完整的报文才能进行后面的逻辑。
问题是有天突然发现网关解析报文出错,查看了客户端的发送日志也没发现问题,最后通过日志发现收到了许多不完整的报文,有些还多了。
于是想会不会是 TCP 拆、粘包带来的问题,最后利用 Netty 自带的拆包工具解决了该问题。
这便有了此文。
TCP 协议
问题虽然解决了,但还是得想想原因,为啥会这样?打破砂锅问到底才是一个靠谱的程序员。
这就得从 TCP 这个协议说起了。
TCP 是一个面向字节流的协议,它是性质是流式的,所以它并没有分段。就像水流一样,你没法知道什么时候开始,什么时候结束。
所以他会根据当前的套接字缓冲区的情况进行拆包或是粘包。
下图展示了一个 TCP 协议传输的过程:
发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由接收端获取。
当我们发送两个完整包到接收端的时候:
正常情况会接收到两个完整的报文。
但也有以下的情况:
接收到的是一个报文,它是由发送的两个报文组成的,这样对于应用程序来说就很难处理了(这样称为粘包)。
还有可能出现上面这样的虽然收到了两个包,但是里面的内容却是互相包含,对于应用来说依然无法解析(拆包)。
对于这样的问题只能通过上层的应用来解决,常见的方式有:
在报文末尾增加换行符表明一条完整的消息,这样在接收端可以根据这个换行符来判断消息是否完整。
将消息分为消息头、消息体。可以在消息头中声明消息的长度,根据这个长度来获取报文(比如 808 协议)。
规定好报文长度,不足的空位补齐,取的时候按照长度截取即可。
以上的这些方式我们在 Netty 的 pipline 中里加入对应的解码器都可以手动实现。
但其实 Netty 已经帮我们做好了,完全可以开箱即用。
比如:
LineBasedFrameDecoder 可以基于换行符解决。
DelimiterBasedFrameDecoder可基于分隔符解决。
FixedLengthFrameDecoder可指定长度解决。
字符串拆、粘包
下面来模拟一下最简单的字符串传输。
还是在之前的
https://github.com/crossoverJie/netty-action
进行演示。
在 Netty 客户端中加了一个入口可以循环发送 100 条字符串报文到接收端:
/**
* 向服务端发消息 字符串
* @param stringReqVO
* @return
*/
@ApiOperation("客户端发送消息,字符串")
@RequestMapping(value = "sendStringMsg", method = RequestMethod.POST)
@ResponseBody
public BaseResponse<NULLBody> sendStringMsg(@RequestBody StringReqVO stringReqVO){
BaseResponse<NULLBody> res = new BaseResponse();
for (int i = 0; i < 100; i++) {
heartbeatClient.sendStringMsg(stringReqVO.getMsg()) ;
}
// 利用 actuator 来自增
counterService.increment(Constants.COUNTER_CLIENT_PUSH_COUNT);
SendMsgResVO sendMsgResVO = new SendMsgResVO() ;
sendMsgResVO.setMsg("OK") ;
res.setCode(StatusEnum.SUCCESS.getCode()) ;
res.setMessage(StatusEnum.SUCCESS.getMessage()) ;
return res ;
}
/**
* 发送消息字符串
*
* @param msg
*/
public void sendStringMsg(String msg) {
ByteBuf message = Unpooled.buffer(msg.getBytes().length) ;
message.writeBytes(msg.getBytes()) ;
ChannelFuture future = channel.writeAndFlush(message);
future.addListener((ChannelFutureListener) channelFuture ->
LOGGER.info("客户端手动发消息成功={}", msg));
}
服务端直接打印即可:
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
LOGGER.info("收到msg={}", msg);
}
顺便提一下,这里加的有一个字符串的解码器:.addLast(new StringDecoder()) 其实就是把消息解析为字符串。
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
out.add(msg.toString(charset));
}
在 Swagger 中调用了客户端的接口用于给服务端发送了 100 次消息:
正常情况下接收端应该打印 100 次 hello 才对,但是查看日志会发现:
收到的内容有完整的、多的、少的、拼接的;这也就对应了上面提到的拆包、粘包。
该怎么解决呢?这便可采用之前提到的 LineBasedFrameDecoder 利用换行符解决。
利用 LineBasedFrameDecoder 解决问题
LineBasedFrameDecoder 解码器使用非常简单,只需要在 pipline 链条上添加即可。
//字符串解析,换行防拆包
.addLast(new LineBasedFrameDecoder(1024))
.addLast(new StringDecoder())
构造函数中传入了 1024 是指报的长度最大不超过这个值,具体可以看下文的源码分析。
然后我们再进行一次测试看看结果:
注意,由于 LineBasedFrameDecoder 解码器是通过换行符来判断的,所以在发送时,一条完整的消息需要加上 \n。
最终的结果:
仔细观察日志,发现确实没有一条被拆、粘包。
LineBasedFrameDecoder 的原理
目的达到了,来看看它的实现原理:
第一步主要就是 findEndOfLine 方法去找到当前报文中是否存在分隔符,存在就会返回分隔符所在的位置。
判断是否需要丢弃,默认为 false ,第一次走这个逻辑(下文会判断是否需要改为 true)。
如果报文中存在换行符,就会将数据截取到那个位置。
如果不存在换行符(有可能是拆包、粘包),就看当前报文的长度是否大于预设的长度。大于则需要缓存这个报文长度,并将 discarding 设为 true。
如果是需要丢弃时,判断是否找到了换行符,存在则需要丢弃掉之前记录的长度然后截取数据。
如果没有找到换行符,则将之前缓存的报文长度进行累加,用于下次抛弃。
从这个逻辑中可以看出就是寻找报文中是否包含换行符,并进行相应的截取。
由于是通过缓冲区读取的,所以即使这次没有换行符的数据,只要下一次的报文存在换行符,上一轮的数据也不会丢。
高效的编码方式 Google Protocol
上面提到的其实就是在解码中进行操作,我们也可以自定义自己的拆、粘包工具。
编解码的主要目的就是为了可以编码成字节流用于在网络中传输、持久化存储。
Java 中也可以实现 Serializable 接口来实现序列化,但由于它性能等原因在一些 RPC 调用中用的很少。
而 Google Protocol 则是一个高效的序列化框架,下面来演示在 Netty 中如何使用。
安装
首先第一步自然是安装:
在官网下载对应的包。
本地配置环境变量:
当执行 protoc --version 出现以下结果表明安装成功:
定义自己的协议格式
接着是需要按照官方要求的语法定义自己的协议格式。
比如我这里需要定义一个输入输出的报文格式:
BaseRequestProto.proto:
syntax = "proto2";
package protocol;
option java_package = "com.crossoverjie.netty.action.protocol";
option java_outer_classname = "BaseRequestProto";
message RequestProtocol {
required int32 requestId = 2;
required string reqMsg = 1;
}
BaseResponseProto.proto:
syntax = "proto2";
package protocol;
option java_package = "com.crossoverjie.netty.action.protocol";
option java_outer_classname = "BaseResponseProto";
message ResponseProtocol {
required int32 responseId = 2;
required string resMsg = 1;
}
再通过
protoc --java_out=/dev BaseRequestProto.proto BaseResponseProto.proto
protoc 命令将刚才定义的协议格式转换为 Java 代码,并生成在 /dev 目录。
只需要将生成的代码拷贝到我们的项目中,同时引入依赖:
com.google.protobuf
protobuf-java
3.4.0
利用 Protocol 的编解码也非常简单:
public class ProtocolUtil {
public static void main(String[] args) throws InvalidProtocolBufferException {
BaseRequestProto.RequestProtocol protocol = BaseRequestProto.RequestProtocol.newBuilder()
.setRequestId(123)
.setReqMsg("你好啊")
.build();
byte[] encode = encode(protocol);
BaseRequestProto.RequestProtocol parseFrom = decode(encode);
System.out.println(protocol.toString());
System.out.println(protocol.toString().equals(parseFrom.toString()));
}
/**
* 编码
* @param protocol
* @return
*/
public static byte[] encode(BaseRequestProto.RequestProtocol protocol){
return protocol.toByteArray() ;
}
/**
* 解码
* @param bytes
* @return
* @throws InvalidProtocolBufferException
*/
public static BaseRequestProto.RequestProtocol decode(byte[] bytes) throws InvalidProtocolBufferException {
return BaseRequestProto.RequestProtocol.parseFrom(bytes);
}
}
利用 BaseRequestProto 来做一个演示,先编码再解码最后比较最终的结果是否相同。答案肯定是一致的。
利用 protoc 命令生成的 Java 文件里已经帮我们把编解码全部都封装好了,只需要简单调用就行了。
可以看出 Protocol 创建对象使用的是构建者模式,对使用者来说清晰易读,更多关于构建器的内容可以参考这里。
更多关于 Google Protocol 内容请查看官方开发文档。
结合 Netty
Netty 已经自带了对 Google protobuf 的编解码器,也是只需要在 pipline 中添加即可。
server 端:
// google Protobuf 编解码
.addLast(new ProtobufDecoder(BaseRequestProto.RequestProtocol.getDefaultInstance()))
.addLast(new ProtobufEncoder())
客户端:
// google Protobuf 编解码
.addLast(new ProtobufDecoder(BaseResponseProto.ResponseProtocol.getDefaultInstance()))
.addLast(new ProtobufEncoder())
稍微注意的是,在构建 ProtobufDecoder 时需要显式指定解码器需要解码成什么类型。
我这里服务端接收的是 BaseRequestProto,客户端收到的是服务端响应的 BaseResponseProto 所以就设置了对应的实例。
同样的提供了一个接口向服务端发送消息,当服务端收到了一个特殊指令时也会向客户端返回内容:
@Override
protected void channelRead0(ChannelHandlerContext ctx, BaseRequestProto.RequestProtocol msg) throws Exception {
LOGGER.info("收到msg={}", msg.getReqMsg());
if (999 == msg.getRequestId()){
BaseResponseProto.ResponseProtocol responseProtocol = BaseResponseProto.ResponseProtocol.newBuilder()
.setResponseId(1000)
.setResMsg("服务端响应")
.build();
ctx.writeAndFlush(responseProtocol) ;
}
}
在 swagger 中调用相关接口:
在日志可以看到服务端收到了消息,同时客户端也收到了返回:
虽说 Netty 封装了 Google Protobuf 相关的编解码工具,其实查看它的编码工具就会发现也是利用上文提到的 api 实现的。
Protocol 拆、粘包
Google Protocol 的使用确实非常简单,但还是有值的注意的地方,比如它依然会有拆、粘包问题。
不妨模拟一下:
连续发送 100 次消息看服务端收到的怎么样:
会发现服务端在解码的时候报错,其实就是被拆、粘包了。
这点 Netty 自然也考虑到了,所以已经提供了相关的工具。
//拆包解码
.addLast(new ProtobufVarint32FrameDecoder())
.addLast(new ProtobufVarint32LengthFieldPrepender())
只需要在服务端和客户端加上这两个编解码工具即可,再来发送一百次试试。
查看日志发现没有出现一次异常,100 条信息全部都接收到了。
这个编解码工具可以简单理解为是在消息体中加了一个 32 位长度的整形字段,用于表明当前消息长度。
总结
网络这块同样是计算机的基础,由于近期在做相关的工作所以接触的比较多,也算是给大学补课了。
后面会接着更新 Netty 相关的内容,最后会产出一个高性能的 HTTP 以及 RPC 框架,敬请期待。
上文相关的代码:
https://github.com/crossoverJie/netty-action
TCP 拆、粘包的更多相关文章
- python 全栈开发,Day35(TCP协议 粘包现象 和解决方案)
一.TCP协议 粘包现象 和解决方案 黏包现象让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)执行远程命令的模块 需要用到模块subprocess sub ...
- TCP通信粘包问题分析和解决
转载至https://www.cnblogs.com/kex1n/p/6502002.html 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发 ...
- TCP协议粘包问题详解
TCP协议粘包问题详解 前言 在本章节中,我们将探讨TCP协议基于流式传输的最大一个问题,即粘包问题.本章主要介绍TCP粘包的原理与其三种解决粘包的方案.并且还会介绍为什么UDP协议不会产生粘包. 基 ...
- TCP的粘包现象
看面经时,看到有面试官问TCP的粘包问题.想起来研一做购物车处理数据更新时遇到粘包问题,就总结一下吧. 1 什么是粘包现象 TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看, ...
- TCP拆包粘包之分隔符解码器
TCP以流的方式进行数据传输,上层的应用协议为了对消息进行区分,往往采用如下4种方式. (1)消息长度固定,累计读取到长度总和为定长LEN的报文后,就认为读取到了一个完整的消息:将计数器置位,重新开始 ...
- python 网络编程之TCP传输&粘包传输
只有TCP有粘包现象,UDP永远不会粘包. 所谓粘包问题主要还是C/S两端数据传输时 因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的 根本原因:粘包是由TCP协议本身造成的,T ...
- TCP通信粘包问题分析和解决(全)(转)
TCP通信粘包问题分析和解决(全) 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送 ...
- Netty—TCP的粘包和拆包问题
一.前言 虽然TCP协议是可靠性传输协议,但是对于TCP长连接而言,对于消息发送仍然可能会发生粘贴的情形.主要是因为TCP是一种二进制流的传输协议,它会根据TCP缓冲对包进行划分.有可能将一个大数据包 ...
- C/C++ socket编程教程之九:TCP的粘包问题以及数据的无边界性
C/C++ socket编程教程之九:TCP的粘包问题以及数据的无边界性 上节我们讲到了socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发 ...
- tcp的粘包和拆包示例以及使用LengthFieldFrameDecoder来解决的方法
粘包和拆包是什么? TCP协议是一种字节流协议,没有记录边界,我们在接收消息的时候,不能人为接收到的数据包就是一个整包消息 当客户端向服务器端发送多个消息数据的时候,TCP协议可能将多个消息数据合并成 ...
随机推荐
- 线段上的格点 辗转相除法(GCD)
/*问题描述:线段上的格点给定平面上的两个格点 P1 = (x1, y1) ; P2 = (x2, y2) 线段P1 P2上,除P1 和 P2以外一共有几个格点*//*分析过程在格点上画P1(0,5) ...
- Arduino学习笔记0---开发板的了解
Arduino的入门文档https://wenku.baidu.com/view/4040861d58fafab069dc02d4.html?from=search,共61页的文档,看完就差不多可以入 ...
- C/C++ (一)
c语言中的逻辑运算符都是短路运算,一旦能够确定整个表达式的值就不再计算,配合c的定义的灵活性,可以写出很多漂亮的程序. 例如 如果要在一个长为n的数列s中找到第k个没被标记过的数 for(i=1,j= ...
- 清北省选 DAY last 集锦
这是题目描述的链接: http://lifecraft-mc.com/wp-content/uploads/2018/03/problems1.pdf (虽然这次没去清北,但还是厚颜无耻的做了一下这套 ...
- Android 学习路线图(转载自https://blog.csdn.net/lixuce1234/article/details/77947405)
程序设计 一.java (a)基本语法(如继承.异常.引用.泛型等) Java核心技术 卷I(适合入门) 进阶 Effective Java中文版(如何写好的Java代码) Java解惑 (介绍烂Ja ...
- guava缓存设置return null一直报错空指针
guava缓存设置return null一直报错空指针 因为缓存不允许返回为空
- O2O助汪峰成功逆袭,汪峰最终上头条了
8月2日七夕情人节,汪峰<峰暴来临>演唱会在鸟巢10万人体育场唱响,各大报纸.站点娱乐板块并没有等来汪峰向国际章求婚的"头条",只是,与乐视合作现场演出+付费直播的O2 ...
- Deepin-安装QQ音乐(Windows程序)
打开命令行,输入: sudo apt-get install wine 安装完成后,下载QQ音乐的安装包 然后安装 示例:wine xx.exe 实例:wine QQMusic.exe 安装完成,启动 ...
- Linux下进程信息的深入分析
这里我们主要介绍进程的状态,进程的状态可以通过/proc/PID/status来查看,也可以通过/proc/PID/stat来查看. 如果说到工具大家用的最多的ps也可以看到进程的信息.这里我们通过/ ...
- ZXing-core生成二维码和解析
如今二维码这么流行的时刻.也必须知道二维码是怎么生成.如今我们就来看看,是怎么生成的. 事实上主要是利用goggle公布的jar来使用:本文转自点击打开链接 1.二维码的生成 将Zxing-core. ...