TCP粘包/拆包介绍
TCP是个"流"协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据 TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。
粘包/拆包的四种情况
- 服务端分两次读取到了两个独立的数据包,分别是 D1和 D2,没有粘包和拆包;
- 服务端一次接收到了两个数据包,D1和 D2 粘合在一起,被称为 TCP 粘包;
- 服务端分两次读取到了两个数据包,第一次读取到了完整的 D1包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这被称为 TCP 拆包;
- 服务端分两次读取到了两个数据包,第一次读取到了 D1 包的部分内容 D1_1,第二次读取到了 D1包的剩余内容 D1_2 和 D2 包的整包。如果此时服务端 TCP接收滑窗非常小,而数据包 D1和 D2 比较大,很有可能会发生第五种可能,即服务端分多次才能将 D1 和 D2 包接收完全,期间发生多次拆包。
粘包问题的解决策略
由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。
- 消息定长,例如每个报文的大小为固定长度 200 字节,如果不够,空位补空格;
- 在包尾增加回车换行符进行分割,例如 FTP 协议;
- 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用 int32 来表示消息的总长度;
- 更复杂的应用层协议。
TCP粘包案例
本次使用的是Netty 5.0版本
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha2</version>
</dependency>
Server端
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
public class TimeServer {
public static void main(String[] args) {
new TimeServer().bind(8080);
}
public void bind(int bindPort){
//用于网络连接的线程组
NioEventLoopGroup boss = new NioEventLoopGroup();
//用户socket读写线程组
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss,worker);
//设置channel类型(对应jdk的nio)
serverBootstrap.channel(NioServerSocketChannel.class);
//服务端接受连接前的缓冲大小(最多有2048个请求等待被accept)
serverBootstrap.option(ChannelOption.SO_BACKLOG,2048);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new TimeServerHandler());
}
});
ChannelFuture future = serverBootstrap.bind(bindPort).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
private static class TimeServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("The time server receive order : " + body);
ByteBuf resp = Unpooled.copiedBuffer((String.valueOf(System.currentTimeMillis())).getBytes());
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
Client端
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
public class TimeClient {
public static void main(String[] args) {
new TimeClient().connect("localhost", 8080);
}
public void connect(String host, int port) {
//IO读写线程组
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.TCP_NODELAY, true);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new TimeClientHandler());
}
});
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
private static class TimeClientHandler extends ChannelHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
byte[] req = ("hello").getBytes();
ByteBuf buffer = Unpooled.buffer(req.length);
buffer.writeBytes(req);
ctx.writeAndFlush(buffer);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("Now is : " + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
运行结果
Server端:
The time server receive order : hellohellohellohellohellohellohellohellohellohello
Client端:
Now is : 1628217161726
客户端写入了10次数据包,而服务端只接受到了一次(把10个数据包粘成一个包传到了应用层),此时发生了TCP粘包现象。
下面我们利用Netty编解码器来解决粘包问题
对Server端进行改造
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class TimeServer {
public static void main(String[] args) {
new TimeServer().bind(8080);
}
public void bind(int bindPort){
//用于网络连接的线程组
NioEventLoopGroup boss = new NioEventLoopGroup();
//用户socket读写线程组
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss,worker);
//设置channel类型(对应jdk的nio)
serverBootstrap.channel(NioServerSocketChannel.class);
//服务端接受连接前的缓冲大小(最多有2048个请求等待被accept)
serverBootstrap.option(ChannelOption.SO_BACKLOG,2048);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
ChannelPipeline pipeline = socketChannel.pipeline();
//以换行符作为结束读取标准 超过指定的最大大小若还没有读取到结束标志 则抛出异常 同时忽略到之前读取的异常码流
pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new StringDecoder());
pipeline.addLast(new TimeServerHandler());
}
});
ChannelFuture future = serverBootstrap.bind(bindPort).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
private static class TimeServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("The time server receive order : " + body);
ByteBuf resp = Unpooled.copiedBuffer((System.currentTimeMillis() + System.getProperty("line.separator")).getBytes());
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
XX行管道中增加了LineBasedFrameDecoder解码器用于对换行符来进行粘包拆包的处理。 XX行写出的消息增加换行符。
对Client端进行改造
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class TimeClient {
public static void main(String[] args) {
new TimeClient().connect("localhost", 8080);
}
public void connect(String host, int port) {
//IO读写线程组
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.TCP_NODELAY, true);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new StringDecoder());
pipeline.addLast(new TimeClientHandler());
}
});
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
private static class TimeClientHandler extends ChannelHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
byte[] req = ("hello" + System.getProperty("line.separator")).getBytes();
ByteBuf buffer = Unpooled.buffer(req.length);
buffer.writeBytes(req);
ctx.writeAndFlush(buffer);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("Now is : " + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
XX行管道中增加了LineBasedFrameDecoder解码器用于对换行符来进行粘包拆包的处理。
XX行写出的消息增加换行符。
运行结果
Server端:
The time server receive order : hello
The time server receive order : hello
The time server receive order : hello
The time server receive order : hello
The time server receive order : hello
The time server receive order : hello
The time server receive order : hello
The time server receive order : hello
The time server receive order : hello
The time server receive order : hello
Client端:
Now is : 1628218042106
Now is : 1628218042111
Now is : 1628218042111
Now is : 1628218042111
Now is : 1628218042111
Now is : 1628218042111
Now is : 1628218042111
Now is : 1628218042111
Now is : 1628218042111
Now is : 1628218042112
可以看到这次没有发生粘包现象,应用层接收到了10次数据包。
下面介绍其他几种常用解码器
Netty之常用解码器
LineBasedFrameDecoder(换行符)
使用示例: pipeline.addLast(new LineBasedFrameDecoder(1024));
LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断看是否有"\n"或者"\r\n",如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。
提供了两种构造方式
-
new LineBasedFrameDecoder(int maxLength, boolean stripDelimiter, boolean failFast); -
new LineBasedFrameDecoder(int maxLength){this(maxLength, true, true)};
DelimiterBasedFrameDecoder(分隔符)
使用示例: pipeline.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("$_".getBytes())));
FixedLengthFrameDecoder(定长)
使用示例: pipeline.addLast(new FixedLengthFrameDecoder(1024));