1、tcp粘包和拆包问题说明
我们可以通过下图来说明tcp粘包和拆包问题的说明:
假设客户端发送d1和d2两个数据包到服务端,此时服务端收到包有如下几种情况:
1:服务端正常收到d1和d2两个数据包;
2:服务端收到一个数据包即是D1和D2粘在一起,此时称为粘包;
3:服务端分2次收到2个数据包,第一次是d1的完整包和d2的部分包,第二次是d2剩余的包,此时就是拆包;
4:服务端分2次收到2个数据包,第一次是d1的部分包,第二次是d1的剩余包和d2的包。
还有一种情况是数据包较大,但tcp的接收窗口非常小,此时服务端要分多次才能收完D1和D2的数据包。
2、tcp粘包和拆包发生的原因
1、应用程序write写入的字节大小大于套接口发送缓冲区大小;
2、进行Mss大小的tcp分段;
3、以太网贞的payload大于MTU进行ip分片。
3、粘包问题的解决策略
由于底层的tcp无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计解决。根据业界的主流协议的解决方案,可以归纳如下:
1)消息定长,例如每个报文大小长度200字节,如果不够,空位补空格;
2)在包尾增加回车换行进行分割,例如ftp协议;
3)将消息分为消息头和消息体,消息头包含表示消息总长度的字段,通常设计思路为消息头的第一字段使用int32来 表示消息的总长度;
4)更复杂的应用层协议;
4、tcp粘包异常案例
这里改造下timeServer,在收到消息的时候去除回车换行符。
package zou;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
public class TimeServerHandler extends ChannelHandlerAdapter {
private int counter;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//读取客户端发送的字节
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "utf-8").substring(0, req.length - System.getProperty("line.separator").length());
//String body = (String) msg;
System.out.println("the time server receive order :" + body + "the counter:" + ++counter);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";
currentTime += System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
//进行发送消息到客户端
ctx.writeAndFlush(resp);
}
// @Override
// public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// //通过调用此方法,将发送的缓冲区的消息全部写到SocketChannel中
// ctx.flush();
// }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
timeClientHandler如下:
public class TimeClientHandle extends ChannelHandlerAdapter {
private static final Logger logger = Logger.getLogger(TimeClientHandle.class.getName());
private int counter;
private byte[] req;
public TimeClientHandle() {
req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
}
//连接成功后发送指令
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message = null;
for (int i = 0; i < 100; i++) {
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "utf-8");
//String body = (String) msg;
System.out.println("Now is:" + body + "the counter is " + ++counter);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
这里会进行发送100次查询时间的消息,按道理服务端和客户端都应该有100次的显示,但执行后结果如下:
the time server receive order :QUERY TIME ORDER
QUERY TIME ORDER
。。。。。
the time server receive order :Y TIME ORDER
QUERY TIME ORDER
。。。。。
the time server receive order :Y TIME ORDER
客户端执行如下:
Now is:BAD ORDER
BAD ORDER
the counter is 1
BAD ORDER
the counter is 1
此时说明已经产生了粘包。
5、利用LineBaseFrameDecoder解决tcp粘包问题
为了解决tcp的粘包/拆包问题,netty默认提供了多种编码解码器用于处理半包。只要熟练掌握这些类库的使用,tcp粘包问题从此就会变得非常容易。调整点如下,TimeServer进行handler的初始化如下:
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
arg0.pipeline().addLast(new LineBasedFrameDecoder(1024));
arg0.pipeline().addLast(new StringDecoder());
arg0.pipeline().addLast(new TimeServerHandler());
}
}
这里添加了2个编码器,lineBaseFrameDecode和StringDecoder,后面的TimeServerHandler 读取消息调整如下:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//读取客户端发送的字节
String body = (String) msg;
System.out.println("the time server receive order :" + body + "the counter:" + ++counter);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";
currentTime += System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
//进行发送消息到客户端
ctx.writeAndFlush(resp);
}
此时代码很简洁,直接将消息转换为字符串,且已经是去除回车换行的。TimeClient的调整如下,也是增加解码器:
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeClientHandle());
}
其timeClientHandler进行解码如下:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("Now is:" + body + "the counter is " + ++counter);
}
非常简洁。相对于之前的调整有增加了解码器,另外在进行读取消息时候代码也简洁了很多,不用编码解码。
进行运行发现没有产生tcp粘包的问题。
6、LineBaseFrameDecoder和StringDecoder原理分析
LineBaseFrameDecoder的原理就是依次遍历ByteBuf中的可读字节,判断是否有“\n”或者是“\r\n”,如果有就以此为换行。另外它也能指定长度,如果在指定的长度内没有换行符那么就会抛出异常。
StringDecoder非常简单,就是将接收到对象转换为字符串,然后继续后面的Handler调用。LineBaseFrameDecoder和StringDecoder结合起来使用就是按行切换的文本解码器,它被设计用来支持tcp的粘包和拆包问题。