Netty学习(二)—拆包粘包问题
无论是服务端还是客户端在进行数据发送收取的时候需要考虑TCP底层的粘包/拆包机制,因为如果不进行处理会造成收取的数据和预想的不一致;
个人主页:tuzhenyu’s page
原文地址:Netty学习(二)—拆包粘包问题
(0) 拆包粘包的原因
TCP粘包发生的原因
粘包现象出现的根本原因是TCP协议是一个面向数据流的通信协议,数据流中没有分界线;在发送或接收时并不会考虑业务数据的具体含义,而是会根据发送缓冲区或者接收缓冲区的情况进行数据包的划分;
为了提高发送效率,发送端会采用Nagle算法优化发送,将时间间隔较短的多次小数据包发送合并成一个大数据包发送,接收端不能辨别出合并情况就出现了粘包情况;
接收端的应用程序未能及时的读取接收缓冲区的数据,多次发送的数据堆积在套接字缓冲区,当应用程序读取时无法分辨就出现了粘包现象;
TCP拆包发生的原因
拆包现象出现的根本原因在于数据包的发送受到链路MTU的限制,也就是链路能传输的最大数据包的大小的限制;
当发送的数据包大小大于MTU-20(TCP报文头部)-20(IP报文头部)时就会出现TCP分组,将大数据包拆分成多个小数据包分别发送;
(1) 拆包粘包的解决策略
数据定长,发送数据报文的长度小于MSS(最大可发送长度)如果不够则用空格补齐,接收时也进行定长接收,能解决粘包问题,不能解决拆包问题;
特殊分割符,在数据结尾用回车换行符等分割,接收到的数据也以特殊字符进行分割,能解决粘包问题,不能解决拆包问题;
将数据长度和数据一起发送,将数据分为消息头和消息体两部分,消息头中包含数据总长度,能解决拆包和粘包问题;
(2) 粘包的示例
- 客户连续端发送数据到服务端
public void channelActive(ChannelHandlerContext ctx) throws Exception {
byte[] req = null;
ByteBuf buffer = null;
for (int i=0;i<100;i++){
req = ("this is No."+i+" server sent the message ").getBytes();
buffer = Unpooled.buffer(req.length);
buffer.writeBytes(req);
ctx.writeAndFlush(buffer);
}
}
- 服务端接收数据
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
System.out.println("receive the bytes: "+new String(bytes,"UTF-8"));
}
- 如果没有出现粘包现象则服务端会接收100次数据,输出100行数据;但是真实的情况服务端只接收了四次数据,出现了粘包现象,具体的是客户端发送粘包还是服务端接粘包不能判断;
receive the bytes: this is No.0 server sent the message this is No.1 server sent the message this is ...
receive the bytes: No.27 server sent the message this is No.28 server sent the message this is...
receive the bytes: s No.54 server sent the message this is No.55 server sent the message this is...
receive the bytes: is No.81 server sent the message this is No.82 server sent the message this is...
(3) 分包的示例
- 客户端发送大数据包到服务端
public void channelActive(ChannelHandlerContext ctx) throws Exception {
byte[] req = null;
ByteBuf buffer = null;
StringBuilder sb = new StringBuilder();
for (int i=0;i<100;i++){
sb.append("abcdefghijklmnopqrstuvwxyz");
}
req = sb.toString().getBytes();
buffer = Unpooled.buffer(req.length);
buffer.writeBytes(req);
ctx.writeAndFlush(buffer);
}
- 服务端接收数据,每接一次输出一行
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
System.out.println("receive the bytes: "+new String(bytes,"UTF-8"));
}
- 按照预想的效果如果没有分包则客户端发送一次,服务端接收输出一次;实际上由于发送的数据包过大则客户端分多次进行发送,服务端也接收输出了多次;
receive the bytes: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz...
receive the bytes: klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgh...
receive the bytes: uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst...
(4) LineBasedFrameDecoder解决粘包问题
LineBasedFrameDecoder解决粘包问题主要是通过对输入流以换行符“\n”对数据流进行分割,每次读取时只读取换行符之前的数据;LineBasedFrameDecoder不能解决拆包问题;
服务端发送数据,在每次发送结尾处添加换行符
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workGroup).channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ServerHandler());
}
});
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes,"UTF-8");
System.out.println("the client says: "+ body);
byte[] req = null;
ByteBuf buffer = null;
for (int i=0;i<100;i++){
req = ("this is No."+i+" server sent the message\n").getBytes();
buffer = Unpooled.buffer(req.length);
buffer.writeBytes(req);
ctx.writeAndFlush(buffer);
}
}
- 客户端接收数据,添加LineBasedFrameDecoder对数据流进行分割
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
socketChannel.pipeline().addLast(new ClientHandler());
}
});
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes,"UTF-8");
System.out.println("the server says: "+ body);
}
- 接收端添加LineBasedFrameDecoder解码器后对输入数据流进行分割,解决了粘包的情况
the server says: this is No.0 server sent the message
the server says: this is No.1 server sent the message
the server says: this is No.2 server sent the message
the server says: this is No.3 server sent the message
the server says: this is No.4 server sent the message
the server says: this is No.5 server sent the message
the server says: this is No.6 server sent the message
the server says: this is No.7 server sent the message
the server says: this is No.8 server sent the message
the server says: this is No.9 server sent the message
the server says: this is No.10 server sent the message
(4) DelimiterBasedFrameDecoder解决粘包问题
DelimiterBasedFrameDecoder特殊字符解码器和LineBasedFrameDecoder换行符解码器类似,都是在读取输入流时以特殊字符为分割符读取数据;DelimiterBasedFrameDecoder特殊字符解码器不能解决拆包问题,只能解决粘包问题;
服务端发送数据到客户端
byte[] req = null;
ByteBuf buffer = null;
for (int i=0;i<100;i++){
req = ("this is No."+i+" server sent the message$").getBytes();
buffer = Unpooled.buffer(req.length);
buffer.writeBytes(req);
ctx.writeAndFlush(buffer);
}
- 客户端添加DelimiterBasedFrameDecoder解码器按照特殊字符对输入数据流进行分割;
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
Unpooled.copiedBuffer("$".getBytes())));
socketChannel.pipeline().addLast(new ClientHandler());
}
});
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf)msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String body = new String(bytes,"UTF-8");
System.out.println("the server says: "+ body);
}
(5) FixedLengthFrameDecoder定长解码器解决粘包问题
FixedLengthFrameDecoder定长解码器是提前设定消息数据的长度,接收端按照设定的长度进行数据流的分割;FixedLengthFrameDecoder只能解决粘包问题不能解决拆包问题;
客户端接收数据,添加定长解码器分割数据流;
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(("hello world").getBytes().length));
socketChannel.pipeline().addLast(new ClientHandler());
}
});
- 服务端发送特定长度的数据,如果数据长度不够则使用空格填充保证数据包长度的一定;
for (int i=0;i<100;i++){
req = ("hello world").getBytes();
buffer = Unpooled.buffer(req.length);
buffer.writeBytes(req);
ctx.writeAndFlush(buffer);
}
总结
- 本文主要总结了粘包/拆包问题出现的原因和解决办法,以及Netty利用编码解码器对拆包/粘包问题的解决办法,并以LineBasedFrameDecoder,DelimiterBasedFrameDecoder和FixedLengthBasedFrameDecoder解码器为例解决TCP粘包问题;