Netty解决TCP粘包拆包的问题

Netty解决TCP粘包拆包的问题

薛定谔的汪

什么是 TCP 粘包拆包问题?

可以先想象这么一个实际场景,一条河的两岸通过船来运输货品,如果每次运输的货品太少,就先把需要运输的货品积攒起来,差不多了再一起运走,这样可以减少船来往的次数节省成本;如果每次运输的货品太多,超过了船的承重,那就把这批货分成两批或者多批运走。

这个例子的第一种情况就是 TCP 协议中的粘包,第二种是拆包。TCP协议可以理解为“船”,当然 TCP 粘包拆包的原因有很多,我做通信的时候是了解过 TCP协议的,现在都快忘了😌。

如图所示:

image-20181025151638997

客户端向服务端发送两个数据包,因为 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");

//channel.closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}

代码里客户端向服务端发送了四次,每次发送一个字符串,启动 Server 和 Client 观察打印:

1
2
3
接收到客户端发来的信息:aaaaa
接收到客户端发来的信息:bbbbbccccc
接收到客户端发来的信息:ddddd

正常情况下服务端应该按照客户端发送的顺序打印4次,然而实际服务端接收到了三次请求,第二次发生了粘包现象。

拆包代码示例:

1
2
3
4
5
6
//演示拆包,客户端发送大字符串"AAAAA......"
String a = "A";
for (int i = 0; i < 1000; i++) {
a += "A";
}
channel.writeAndFlush(a);

观察 Server 端接收情况:

1
2
接收到客户端发来的信息:AAAA.....(省略)
接收到客户端发来的信息:AAAA.....(省略)

发现一个大字符串”AAAA……“被拆成了两个包,分两次发送,这就是拆包。

如何解决?

目前主要有三种方式解决粘包拆包问题

  1. 将消息定长,长度不足的用空格补充。
  2. 每条消息末尾使用自定义分隔符,换行符也行。
  3. 将消息分为消息头和消息体,在消息头中包含消息总长度的字段。

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/

  • Title: Netty解决TCP粘包拆包的问题
  • Author: 薛定谔的汪
  • Created at : 2018-09-14 18:01:54
  • Updated at : 2023-11-17 19:37:37
  • Link: https://www.zhengyk.cn/2018/09/14/netty/tcp-zb-cb/
  • License: This work is licensed under CC BY-NC-SA 4.0.