什么是 TCP 粘包拆包问题?
可以先想象这么一个实际场景,一条河的两岸通过船来运输货品,如果每次运输的货品太少,就先把需要运输的货品积攒起来,差不多了再一起运走,这样可以减少船来往的次数节省成本;如果每次运输的货品太多,超过了船的承重,那就把这批货分成两批或者多批运走。
这个例子的第一种情况就是 TCP 协议中的粘包,第二种是拆包。TCP协议可以理解为“船”,当然 TCP 粘包拆包的原因有很多,我做通信的时候是了解过 TCP协议的,现在都快忘了😌。
如图所示:
客户端向服务端发送两个数据包,因为 D1和 D2的大小不确定,所以可能存在三种情况
第一种两个数据包分两次发送,未发生粘包和拆包。
第二种是发生了粘包,D1和 D2一次都发过去。
第三种是发生了拆包,第一次发送 D1和 D2的一部分,第二次发送 D2的另一部分。
Netty 中的粘包拆包现象
粘包代码示例:
NettyServer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class NettyServer {
public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap().group(bossGroup,workGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new StringDecoder()); pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("handler", new ServerHandler()); } });
try { ChannelFuture future = server.bind(8080).sync(); future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } }
|
ServerHandler
1 2 3 4 5 6
| public class ServerHandler extends SimpleChannelInboundHandler<String> { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { System.out.println("接收到客户端发来的信息:"+msg); } }
|
Server 端接收到 String 字符串后,简单地将其打印出来。
NettyClient
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class NettyClient {
public static void main(String[] args) throws Exception { EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new StringDecoder()); pipeline.addLast("encoder", new StringEncoder()); } }); try { ChannelFuture future = bootstrap.connect("127.0.0.1",8080).sync(); Channel channel = future.channel(); channel.writeAndFlush("aaaaa"); channel.writeAndFlush("bbbbb"); channel.writeAndFlush("ccccc"); channel.writeAndFlush("ddddd"); }finally { group.shutdownGracefully(); } } }
|
代码里客户端向服务端发送了四次,每次发送一个字符串,启动 Server 和 Client 观察打印:
1 2 3
| 接收到客户端发来的信息:aaaaa 接收到客户端发来的信息:bbbbbccccc 接收到客户端发来的信息:ddddd
|
正常情况下服务端应该按照客户端发送的顺序打印4次,然而实际服务端接收到了三次请求,第二次发生了粘包现象。
拆包代码示例:
1 2 3 4 5 6
| String a = "A"; for (int i = 0; i < 1000; i++) { a += "A"; } channel.writeAndFlush(a);
|
观察 Server 端接收情况:
1 2
| 接收到客户端发来的信息:AAAA.....(省略) 接收到客户端发来的信息:AAAA.....(省略)
|
发现一个大字符串”AAAA……“被拆成了两个包,分两次发送,这就是拆包。
如何解决?
目前主要有三种方式解决粘包拆包问题
- 将消息定长,长度不足的用空格补充。
- 每条消息末尾使用自定义分隔符,换行符也行。
- 将消息分为消息头和消息体,在消息头中包含消息总长度的字段。
LineBasedFrameDecoder
该解码器可以用来解析消息某个位置是否有”\n“或者”\r\n“换行符,如果有就以此位置为结束位置。
DelimiterBasedFrameDecoder
我们可以自定义一个”消息分隔符“,该解码器解析到消息某个位置有自定义“消息分隔符”时,就以此位置为结束位置,如果“消息分隔符”是换行符,那么该解码器的作用和LineBasedFrameDecoder一样了。
代码示例:
NettyServer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class NettyServer {
public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap().group(bossGroup,workGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline();
ByteBuf delimiter = Unpooled.copiedBuffer("_$".getBytes()); pipeline.addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
pipeline.addLast("decoder", new StringDecoder()); pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("handler", new ServerHandler()); } });
try { ChannelFuture future = server.bind(8080).sync(); future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workGroup.shutdownGracefully(); } } }
|
NettyClient:
1 2 3 4 5
| 每条消息的末尾加上"_$"分隔符 channel.writeAndFlush("aaaaa_$"); channel.writeAndFlush("bbbbb_$"); channel.writeAndFlush("ccccc_$"); channel.writeAndFlush("ddddd_$");
|
启动观察 Server 端接收消息情况:
1 2 3 4
| 接收到客户端发来的信息:aaaaa 接收到客户端发来的信息:bbbbb 接收到客户端发来的信息:ccccc 接收到客户端发来的信息:ddddd
|
可以正常接收消息。
LengthFieldBasedFrameDecoder
大多数协议可以将传输的消息分为两部分,消息头和消息体,消息头有个字段标识消息体的长度,比如 Http 协议中的 Content-Length,
LengthFieldBasedFrameDecoder的具体使用可参照这篇~http://blog.163.com/linfenliang@126/blog/static/127857195201210821145721/