详解什么是TCP粘包和拆包现象并演示Netty是如何解决的
概述
本文介绍什么是 TCP 粘包和拆包现象,并通过 Netty 编写详细的案例来重现 TCP 粘包问题,最后再通过一个 Netty 的 demo 来解决这个问题。具体内容如下 什么是 TCP 粘包和拆包现象 重现 TCP 粘包和拆包现象 Netty 解决 TCP 粘包和拆包现象带来的问题 什么是 TCP 粘包和拆包现象
TCP 编程底层都有粘包和拆包机制,因为我们在C/S这种传输模型下,以TCP协议传输的时候,在网络中的byte其实就像是河水,TCP就像一个搬运工,将这流水从一端转送到另一端,这时又分两种情况: 如果客户端的每次制造的水比较多,也就是我们常说的客户端给的包比较大,TCP这个搬运工就会分多次去搬运 如果客户端每次制造的水比较少的话,TCP可能会等客户端多次生产之后,把所有的水一起再运输到另一端 对于第一种情况,TCP 会再客户端先进行拆包,在另一端接收的时候,需要把多次获取的结果组合在一起,变成我们可以理解的信息 对于第二种情况,TCP 会在客户端先进行粘包,在另一端接收的时候,就必须进行拆包处理,因为每次接收的信息,可能是另一个远程端多次发送的包,被TCP粘在一起的 重现 TCP 粘包和拆包现象通过在客户端 1 次发送超大数据包给服务器端来重现 TCP 拆包现象 通过在客户端分 10 次发送较小的数据包给服务器端来重现 TCP 粘包现象
下面通过 Netty 重现 TCP 粘包和拆包现象。 Netty maven 依赖 io.netty netty-all 4.1.76.Final 通过 Netty 重现 TCP 拆包现象Netty 客户端启动类:NettyClient package com.ckjava.test.client; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Slf4j @Component public class NettyClient { static final String HOST = System.getProperty("host", "127.0.0.1"); static final int PORT = Integer.parseInt(System.getProperty("port", "8080")); static final int SIZE = Integer.parseInt(System.getProperty("size", "256")); public static void main(String[] args) throws Exception { // 初始化客户端事件组 EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) // 初始化通道 .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer() { // 初始化通道处理器 @Override public void initChannel(SocketChannel ch) { ChannelPipeline p = ch.pipeline(); p.addLast(new NettyClientHandler()); } }); b.connect(HOST, PORT).addListener(future -> { log.info(String.format("连接服务器端:%s:%s 成功!", HOST, PORT)); }).await(); } catch (Exception e) { log.error("启动客户端出现异常", e); } } } Netty 客户端通道处理类:NettyClientHandler package com.ckjava.test.client; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @Slf4j public class NettyClientHandler extends SimpleChannelInboundHandler { private final AtomicInteger countRef = new AtomicInteger(0); //客户端读取服务器发送的信息 @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { byte[] buffer = new byte[msg.readableBytes()]; msg.readBytes(buffer); String message = new String(buffer, StandardCharsets.UTF_8); log.info(String.format("客户端接收到消息:[%s]", message)); log.info(String.format("客户端接收到消息的次数:%s", countRef.accumulateAndGet(1, Integer::sum))); log.info("---------------------------------------------------"); } // 重写 channelActive, 当客户端启动的时候 自动发送数据给服务端 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // 客户端只发送一次,但是本次数据量很大 // tcp 会将数据拆分成多份后依次进行发送 String data = "ckjava"; StringBuilder stringBuilder = new StringBuilder(data); for (int i = 0; i < 10000; i++) { stringBuilder.append(data); } ByteBuf buffer = Unpooled.copiedBuffer("数据" + stringBuilder.toString(), StandardCharsets.UTF_8); ctx.writeAndFlush(buffer); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ctx.close(); } }
其中关键的代码如下 // 重写 channelActive, 当客户端启动的时候 自动发送数据给服务端 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // 客户端只发送一次,但是本次数据量很大 // tcp 会将数据拆分成多份后依次进行发送 String data = "ckjava"; StringBuilder stringBuilder = new StringBuilder(data); for (int i = 0; i < 10000; i++) { stringBuilder.append(data); } ByteBuf buffer = Unpooled.copiedBuffer("数据" + stringBuilder.toString(), StandardCharsets.UTF_8); ctx.writeAndFlush(buffer); } Netty 客户端启动类:NettyClient package com.ckjava.test.server; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.net.InetSocketAddress; @Slf4j @Component public class NettyServer { private final int port; public NettyServer(int port) { this.port = port; } public void start() { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap sbs = new ServerBootstrap() .group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer() { protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new NettyServerHandler()); } }).option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); // 绑定端口,开始接收进来的连接 sbs.bind(port).addListener(future -> { log.info(String.format("服务器端启动成功,开放端口:%s", port)); }); } catch (Exception e) { log.error("启动服务器端出现异常", e); } } public static void main(String[] args) { int port = 8080; new NettyServer(port).start(); } } Netty 服务器端通道处理类:NettyServer package com.ckjava.test.server; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.util.UUID; @Slf4j public class NettyServerHandler extends SimpleChannelInboundHandler { private int counter; private int count; @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { byte[] buffer = new byte[msg.readableBytes()]; msg.readBytes(buffer); //将buffer转为字符串 String message = new String(buffer, StandardCharsets.UTF_8); System.out.println("服务器收到的数据=" + message); System.out.println("服务器收到的数据次数=" + (++this.count)); //服务器回送数据给客户端 回送一个随机id ByteBuf buffer1 = Unpooled.copiedBuffer(UUID.randomUUID().toString(), StandardCharsets.UTF_8); ctx.writeAndFlush(buffer1); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } 分别先启动服务器端后,再启动客户端,服务器端的输出如下
客户端的输出如下 17:03:24.474 [nioEventLoopGroup-2-1] INFO com.ckjava.test.client.NettyClient - 连接服务器端:127.0.0.1:8080 成功! 17:03:24.535 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[c471a239-abe5-4401-93aa-b3d5e432c422021b6ae3-4939-4d59-b451-235af6c9e2190536b0aa-3b53-4b03-bb68-b0637d619d0f] 17:03:24.537 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:1 17:03:24.537 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 从服务器端和客户端的输出结果来看:客户端只发送了 1 次数据,但是服务器端却收到了 3 次数据,说明 tcp 在客户端拆包后分 3 次发送了;并且客户端之后只收到了一次数据,说明服务器的回复数据在服务器端也出现了粘包现象,并且导致了数据无法区分的问题。 通过 Netty 重现 TCP 粘包现象还用上面的例子,将客户端通道处理类:NettyClientHandler 中的 channelActive 方法修改成如下的方式 // 重写 channelActive, 当客户端启动的时候 自动发送数据给服务端 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // 使用客户端分10次发送,每次数据量少 // tcp 会等客户端多次生产后,一次性进行发送 for (int i = 0; i < 10; i++) { ByteBuf buffer = Unpooled.copiedBuffer("ckjava" + i + " ", StandardCharsets.UTF_8); ctx.writeAndFlush(buffer); } } 分别先启动服务器端后,再启动客户端,服务器端的输出如下 17:12:27.239 [nioEventLoopGroup-2-1] INFO com.ckjava.test.server.NettyServer - 服务器端启动成功,开放端口:8080 服务器收到的数据=ckjava0 ckjava1 ckjava2 ckjava3 ckjava4 ckjava5 ckjava6 ckjava7 ckjava8 ckjava9 服务器收到的数据次数=1 客户端的输出如下 17:12:36.917 [nioEventLoopGroup-2-1] INFO com.ckjava.test.client.NettyClient - 连接服务器端:127.0.0.1:8080 成功! 17:12:36.961 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[31b25c25-bd32-4ff1-b390-0c31b2558d12] 17:12:36.962 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:1 17:12:36.962 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 从服务器端和客户端的输出结果来看:客户端只发送了 10 次数据,但是服务器端却收到了 1 次数据,说明 tcp 在客户端粘包后一次性发送了全部的数据。 Netty 解决 TCP 粘包和拆包现象带来的问题TCP 粘包和拆包现象带来的问题
从上面的案例可以发现当出现 TCP 粘包和拆包现象后会出现下面的问题: tcp 在粘包的时候,数据混合后,接收方不能正确区分数据的头尾,如果是文件类型的数据,会导致文件破坏。 tcp 在拆包的时候,数据拆分后,接收方不能正确区分数据的头尾,导致收到的消息错乱,影响语义。 如何解决 TCP 粘包和拆包现象带来的问题
由于 TCP 粘包和拆包现象会导致不能正确区分数据的头尾,那么解决的办法也挺简单的,通过 特殊字符串 来分隔消息体或者使用 定长消息 就能够正确区分数据的头尾。
目前的主流解决方式有以下几种: 使用定长消息,Client 和 Server 双方约定报文长度,Server 端接受到报文后,按指定长度解析; 使用特定分隔符,比如在消息尾部增加分隔符。Server 端接收到报文后,按照特定的分割符分割消息后,再解析; 将消息分割为消息头和消息体两部分,消息头中指定消息或者消息体的长度,通常设计中使用消息头第一个字段 int32 表示消息体的总长度;
Netty 中也提供了基于分隔符实现的半包解码器和定长的半包解码器: LineBasedFrameDecoder 使用" "和"r "作为分割符的解码器 DelimiterBasedFrameDecoder 使用自定义的分割符的解码器 FixedLengthFrameDecoder 定长解码器 通过 Netty 的 DelimiterBasedFrameDecoder 解码器 来解决 TCP 粘包和拆包现象带来的问题
使用 DelimiterBasedFrameDecoder 可以确保收到的数据会自动通过 自定义的分隔符 进行分隔。发送的时候消息的后面只需要增加上 自定义的分隔符 即可。 基于上面的例子,服务器端 NettyServer 改动如下 public void start() { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap sbs = new ServerBootstrap() .group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer() { protected void initChannel(SocketChannel ch) { // 使用分隔符"$_"的半包解码器 ByteBuf byteBuf = Unpooled.copiedBuffer(DELIMITER.getBytes()); ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, byteBuf)); ch.pipeline().addLast(new NettyServerHandler()); } }).option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); // 绑定端口,开始接收进来的连接 sbs.bind(port).addListener(future -> { log.info(String.format("服务器端启动成功,开放端口:%s", port)); }); } catch (Exception e) { log.error("启动服务器端出现异常", e); } } 服务器端 NettyServerHandler 改动如下 @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { byte[] buffer = new byte[msg.readableBytes()]; msg.readBytes(buffer); //将buffer转为字符串 String message = new String(buffer, StandardCharsets.UTF_8); System.out.println("服务器收到的数据=" + message); System.out.println("服务器收到的数据次数=" + (++this.count)); //服务器回送数据给客户端 回送一个随机id String replyData = UUID.randomUUID().toString(); ByteBuf buffer1 = Unpooled.copiedBuffer(replyData.concat(NettyServer.DELIMITER), StandardCharsets.UTF_8); ctx.writeAndFlush(buffer1); System.out.println("服务器回复数据=" + replyData); } 客户端 NettyClient 改动如下 public static void main(String[] args) throws Exception { // 初始化客户端事件组 EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) // 初始化通道 .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer() { // 初始化通道处理器 @Override public void initChannel(SocketChannel ch) { ChannelPipeline p = ch.pipeline(); // 使用分隔符"$_"的半包解码器 ByteBuf byteBuf = Unpooled.copiedBuffer(DELIMITER.getBytes()); ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, byteBuf)); p.addLast(new NettyClientHandler()); } }); b.connect(HOST, PORT).addListener(future -> { log.info(String.format("连接服务器端:%s:%s 成功!", HOST, PORT)); }).await(); } catch (Exception e) { log.error("启动客户端出现异常", e); } } 客户端 NettyClientHandler 中接收数据的部分不变,发送数据的地方改动如下 // 重写 channelActive, 当客户端启动的时候 自动发送数据给服务端 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // tcp 粘包现象 // 使用客户端分10次发送,每次数据量少 // tcp 会等客户端多次生产后,一次性进行发送 for (int i = 0; i < 10; i++) { String data = "ckjava" + i; log.info(String.format("客户端发送消息:[%s]", data)); ByteBuf buffer = Unpooled.copiedBuffer(data.concat(NettyClient.DELIMITER), StandardCharsets.UTF_8); ctx.writeAndFlush(buffer); } } 服务器端输出如下 18:14:33.627 [nioEventLoopGroup-2-1] INFO com.ckjava.test.server.NettyServer - 服务器端启动成功,开放端口:8080 服务器收到的数据=ckjava0 服务器收到的数据次数=1 服务器回复数据=c6129b89-c869-4e06-97ca-55518c55aff7 服务器收到的数据=ckjava1 服务器收到的数据次数=2 服务器回复数据=bc3426cb-072f-4cb9-9f69-d2797863c9e4 服务器收到的数据=ckjava2 服务器收到的数据次数=3 服务器回复数据=43790702-1978-462b-a865-15c0ff2803af 服务器收到的数据=ckjava3 服务器收到的数据次数=4 服务器回复数据=4eb3e4e6-0c6a-4cef-a639-d6c40ebc27d2 服务器收到的数据=ckjava4 服务器收到的数据次数=5 服务器回复数据=6a9f02f9-9e0d-4eae-a380-605c3ba410d2 服务器收到的数据=ckjava5 服务器收到的数据次数=6 服务器回复数据=7ab9e20e-a86b-4f68-8673-5bc024643274 服务器收到的数据=ckjava6 服务器收到的数据次数=7 服务器回复数据=3b6b68cf-c066-4e32-8b5a-961c995fdd6d 服务器收到的数据=ckjava7 服务器收到的数据次数=8 服务器回复数据=cf2a5c51-96d9-4309-8f05-1c09abbe04f2 服务器收到的数据=ckjava8 服务器收到的数据次数=9 服务器回复数据=4d586684-be55-4c10-8071-a88dad5f0684 服务器收到的数据=ckjava9 服务器收到的数据次数=10 服务器回复数据=22fd511e-e65a-4f10-9426-f14b4524d4d0 客户端输出如下 18:14:50.056 [nioEventLoopGroup-2-1] INFO com.ckjava.test.client.NettyClient - 连接服务器端:127.0.0.1:8080 成功! 18:14:50.058 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端发送消息:[ckjava0] 18:14:50.075 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端发送消息:[ckjava1] 18:14:50.076 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端发送消息:[ckjava2] 18:14:50.076 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端发送消息:[ckjava3] 18:14:50.076 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端发送消息:[ckjava4] 18:14:50.076 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端发送消息:[ckjava5] 18:14:50.076 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端发送消息:[ckjava6] 18:14:50.077 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端发送消息:[ckjava7] 18:14:50.077 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端发送消息:[ckjava8] 18:14:50.077 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端发送消息:[ckjava9] 18:14:50.104 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[c6129b89-c869-4e06-97ca-55518c55aff7] 18:14:50.105 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:1 18:14:50.105 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 18:14:50.105 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[bc3426cb-072f-4cb9-9f69-d2797863c9e4] 18:14:50.105 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:2 18:14:50.105 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[43790702-1978-462b-a865-15c0ff2803af] 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:3 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[4eb3e4e6-0c6a-4cef-a639-d6c40ebc27d2] 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:4 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[6a9f02f9-9e0d-4eae-a380-605c3ba410d2] 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:5 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[7ab9e20e-a86b-4f68-8673-5bc024643274] 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:6 18:14:50.106 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[3b6b68cf-c066-4e32-8b5a-961c995fdd6d] 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:7 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[cf2a5c51-96d9-4309-8f05-1c09abbe04f2] 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:8 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[4d586684-be55-4c10-8071-a88dad5f0684] 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:9 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息:[22fd511e-e65a-4f10-9426-f14b4524d4d0] 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - 客户端接收到消息的次数:10 18:14:50.107 [nioEventLoopGroup-2-1] INFO c.c.test.client.NettyClientHandler - --------------------------------------------------- 从上面的例子可以看出 DelimiterBasedFrameDecoder 会帮自动帮我们把消息切割好,确保收到的数据都是基于 自定义分隔符 分隔好的数据, 但是不要忘记在发送数据的时候添加上 自定义分隔符 。