网络通信时,如何解决粘包/半包、丢包或者包乱序的问题?
序列号和包重传应答确认机制
保证数据包的有序和一定被正确发到目的地;所以,对于 TCP 协议来说,我们需要关注的是如何粘包/半包问题。
说明一下,半包
:半包不是说只收到了全包的一半,而是说收到了全包的一部分,有时我们叫拆包。
应用A 通过网络发送数据向应用B 发送消息,大概会经过如下阶段:
假设应用A 分别发送了两个数据包 D1 和 D2 给应用B,由于应用B一次读取到的字节数是不确定的,故可能存在以下 4 种情况。
(1)应用B 分两次读取到了两个独立的数据包,分别是 D1 和 D2,没有粘包和拆包;
(2)应用B 一次接收到了两个数据包,D1 和 D2 粘合在一起,被称为 TCP 粘包;
(3)应用B 分两次读取到了两个数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这被称为 TCP 拆包;
(4)应用B 分两次读取到了两个数据包,第一次读取到了 D1 包的部分内容 D1_1,第二次读取到了 D1 包的剩余内容 D1_2 和 D2 包的整包,这被称为 TCP 拆包;
由于 TCP 协议本身的机制,它会存在 TCP 粘包/半包问题。
TCP发送数据原由:
TCP接收方的原由:
粘包的主要原因:
半包的主要原因:
其实我们可以换个角度看待问题:
根本原因,其实是:
就像上面说的,UDP 之所以不会产生粘包和半包问题,主要是因为消息有边界,因此,我们也可以采取类似的思路。
解决问题的根本手段:找出消息的边界。
将 TCP 连接改成短连接,一个请求一个短连接。这样的话,建立连接到释放连接之间的消息即为传输的信息,消息也就产生了边界。
这样的方法就是十分简单,不需要在我们的应用中做过多修改。但缺点也就很明显了,效率低下,TCP 连接和断开都会涉及三次握手以及四次握手,每个消息都会涉及这些过程,十分浪费性能。
因此,并不推荐这种方式。
封装成帧(Framing),也就是原本发送消息的单位是缓冲大小,现在换成了帧,这样我们就可以自定义边界了。
一般有4种方式:
这种方式下,消息边界也就是固定长度即可。
优点就是实现很简单,缺点就是空间有极大的浪费,如果传递的消息中大部分都比较短,这样就会有很多空间是浪费的。
因此,这种方式一般也是不推荐的。
这种方式下,消息边界也就是分隔符本身。
优点是空间不再浪费,实现也比较简单。缺点是当内容本身出现分割符时需要转义,所以无论是发送还是接受,都需要进行整个内容的扫描。
因此,这种方式效率也不是很高,但可以尝试使用。
这种方式,就有点类似 Http 请求中的 Content-Length,有一个专门的字段存储消息的长度。作为服务端,接受消息时,先解析固定长度的字段(length字段)获取消息总长度,然后读取后续内容。
优点是精确定位用户数据,内容也不用转义。缺点是长度理论上有限制,需要提前限制可能的最大长度从而定义长度占用字节数。
因此,十分推荐用这种方式。
其他方式就各不相同了,比如 JSON 可以看成是使用{}是否成对。这些优缺点就需要大家在各自的场景中进行衡量了。
由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案。
Netty 支持上面所讲的封装成帧(Framing)中的前三种方式,简单介绍下:
FixedLengthFrameDecoder
采用的是定长协议:即把固定的长度的字节数当做一个完整的消息。
FixedLengthFrameDecodert提供了以下构造方法:
public FixedLengthFrameDecoder(int frameLength) {
ObjectUtil.checkPositive(frameLength, "frameLength");
this.frameLength = frameLength;
}
注意:FixedLengthFrameDecoder并没有提供一个对应的编码器,因为接收方只需要根据字节数进行判断即可,发送方无需编码。
例如:我们规定每个报文的大小为固定长度 5个字节,表示一个有效报文,如果不够,空位补空格;
1)服务端
bootstrap.group(bossGroup, workerGroup)
...
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new FixedLengthFrameDecoder(5)) //固定长度 5个字节
.addLast(new FixedLengthServerHandler());
}
});
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 获取客户端发送过来的消息
ByteBuf in = (ByteBuf) msg;
String request = in.toString(CharsetUtil.UTF_8);
System.out.println("Server Accept[" + request + "] and the counter is:" + counter.incrementAndGet());
ctx.writeAndFlush(Unpooled.copiedBuffer("Welcome to Netty!".getBytes()));
}
2)客户端
bootstrap.group(eventExecutors)/*将线程组传入*/
...
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new FixedLengthFrameDecoder(5)) //固定长度 5个字节
.addLast(new FixedLengthClientHandler());
}
});
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
// 接收服务端发送过来的消息
System.out.println("client Accept[" + msg.toString(CharsetUtil.UTF_8) + "] and the counter is:" + counter.incrementAndGet());
}
/**
* 客户端被通知channel活跃后 channel活跃后,做业务处理
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 发送消息到服务端
ByteBuf in1 = Unpooled.buffer().writeBytes("CHARGE".getBytes());
ByteBuf in2 = Unpooled.buffer().writeBytes(" and ".getBytes());
ByteBuf in3 = Unpooled.buffer().writeBytes("ABCDEFGH".getBytes());
ctx.writeAndFlush(in1);
ctx.writeAndFlush(in2);
ctx.writeAndFlush(in3);
}
在包尾增加分割符,比如回车换行符进行分割,例如 FTP 协议;
LineBasedFrameDecoder
采用的通信协议格式非常简单:使用换行符\n或者\r\n
作为依据,遇到\n或者\r\n都认为是一条完整的消息。
LineBasedFrameDecoder提供了2个构造方法,如下:
public LineBasedFrameDecoder(int maxLength) {
this(maxLength, true, false);
}
public LineBasedFrameDecoder(int maxLength, boolean stripDelimiter, boolean failFast) {
this.maxLength = maxLength;
this.failFast = failFast;
this.stripDelimiter = stripDelimiter;
}
其中:
1)服务端
bootstrap.group(bossGroup, workerGroup)/*将线程组传入*/
...
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//回车换行符
socketChannel.pipeline()
.addLast(new LineBasedFrameDecoder(1024))
.addLast(new LineBaseServerHandler());
}
});
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 获取客户端发送过来的消息
ByteBuf in = (ByteBuf) msg;
String request = in.toString(CharsetUtil.UTF_8);
System.out.println("Server Accept[" + request + "] and the counter is:" + counter.incrementAndGet());
String resp = "Hello," + request + ". Welcome to Netty World!" + System.getProperty("line.separator");
ctx.writeAndFlush(Unpooled.copiedBuffer(resp.getBytes()));
}
2)客户端
bootstrap.group(eventExecutors)/*将线程组传入*/
...
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//回车换行符
socketChannel.pipeline()
.addLast(new LineBasedFrameDecoder(1024))
.addLast(new LineBaseClientHandler());
}
});
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 发送消息到服务端
ByteBuf msg = null;
String request = "charge LineBasedFrameDecoder(1024),回车换行符" + System.getProperty("line.separator");
for (int i = 0; i < 5; i++) {
msg = Unpooled.buffer(request.length());
msg.writeBytes(request.getBytes());
ctx.writeAndFlush(msg);
}
}
DelimiterBasedFrameDecoder
是一个分隔符解码器。我们可以自定义消息分隔符。
DelimiterBasedFrameDecoder提供了多个构造方法,比如下面两个
public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {
this(maxFrameLength, true, delimiter);
}
public DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, ByteBuf delimiter) {
this(maxFrameLength, stripDelimiter, true, delimiter);
}
其中:
1)服务端
public class DelimiterEchoServer {
public static final String DELIMITER_SYMBOL = "@~@";
public static final int PORT = 19997;
public static void main(String[] args) throws InterruptedException {
DelimiterEchoServer delimiterEchoServer = new DelimiterEchoServer();
System.out.println("服务器即将启动");
delimiterEchoServer.start();
}
public void start() throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();/*服务端启动必须*/
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(PORT))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 自定义分割符
ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER_SYMBOL.getBytes());
socketChannel.pipeline()
.addLast(new DelimiterBasedFrameDecoder(1024, delimiter))
.addLast(new DelimiterServerHandler());
}
});
System.out.println("MyServer 服务端已经准备就绪...");
ChannelFuture channelFuture = bootstrap.bind().sync();
System.out.println("服务器启动完成,等待客户端的连接和数据...");
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
/**
* 服务端读取到网络数据后,做业务处理
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 获取客户端发送过来的消息
ByteBuf in = (ByteBuf) msg;
String request = in.toString(CharsetUtil.UTF_8);
System.out.println("Server Accept[" + request + "] and the counter is:" + counter.incrementAndGet());
String resp = "Hello," + request + ". Welcome to Netty World!" + DelimiterEchoServer.DELIMITER_SYMBOL;
ctx.writeAndFlush(Unpooled.copiedBuffer(resp.getBytes()));
// ctx.close();
}
/**
* 服务端读取完成网络数据后,做业务处理
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 发送消息给客户端
System.out.println("channelReadComplete------");
// ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
2)客户端
bootstrap.group(eventExecutors)/*将线程组传入*/
...
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//自定义分割符
ByteBuf delimiter = Unpooled.copiedBuffer(DelimiterEchoServer.DELIMITER_SYMBOL.getBytes());
socketChannel.pipeline()
.addLast(new DelimiterBasedFrameDecoder(1024, delimiter))
.addLast(new DelimiterClientHandler());
}
});
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 发送消息到服务端
ByteBuf msg = null;
String request = "charge 自定义分割符 \\@\\~\\@ , 发送数据。" + DelimiterEchoServer.DELIMITER_SYMBOL;
for (int i = 0; i < 10; i++) {
msg = Unpooled.buffer(request.length());
msg.writeBytes(request.getBytes());
ctx.writeAndFlush(msg);
System.out.println("发送数据到服务器");
}
}
专门的 length 字段:将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)
的字段,通常设计思路为消息头的第一个字段使用 int32 来表示消息的总长度,使用
LengthFieldBasedFrameDecoder
,后面再了解使用它。
上面几个都有一个共同的父类 ByteToMessageDecoder 解码器
。
到此,Netty 解决TCP粘包/半包使用有所了解,解码的更多逻辑可以查看源码。
参考文章:
– 求知若饥,虚心若愚。