目录
1 TCP粘包/拆包
TCP粘包/拆包问题说明
TCP粘包/拆包发生的原因
粘包问题的解决策略
2 Netty遇到粘包和拆包
3 Netty解决粘包和拆包
TCP是一个“流”协议,所谓流就是没有边界的一串字符串,其间没有分界线。下面就用一个列子来说明
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
问题产生的原因有三个,分别如下。
(1)应用程序write写入的字节大小大于套接口发送缓冲区大小;
(2)进行MSS大小的TCP分段;
(3)以太网帧的payload大于MTU进行IP分片。
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。
(1)消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;
(2)在包尾增加回车换行符进行分割,例如FTP协议;
(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度;
(4)更复杂的应用层协议。
在前面一章(Netty的HelloWord)中客户端只发送了一次请求给服务端,如果发送多次请求而且没有进行TCP粘包和拆包的处理,会发生什么样的情况。
我们把上一章的例子的TimeClientHandler和TimeServerHandler进行修改
TimeClientHandler类 把发送消息加上一个换行符 然后发送一百次
req = ("QUERY TIME ORDER"+System.getProperty("line.separator")).getBytes();
System.out.println("从服务端得到的时间:"+body+" counter:"+ ++counter);
public class TimeClientHandler extends ChannelHandlerAdapter {
private static final Logger logger = Logger.getLogger(TimeClientHandler.class.getName());
private byte[] req;
private int counter = 0;
public TimeClientHandler() {
req = ("QUERY TIME ORDER"+System.getProperty("line.separator")).getBytes();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message;
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[] time = new byte[buf.readableBytes()];
buf.readBytes(time);
String body = new String(time,"utf-8");
System.out.println("从服务端得到的时间:"+body+" counter:"+ ++counter);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.warning(cause.getMessage());
ctx.close();
}
}
TimeServerHandler类 服务端获取信息
String body = new String(req,"utf-8").substring(0,req.length-(System.getProperty("line.separator").length()));
System.out.println("从客戶端发送的消息:"+body+" counter:"+ ++counter);
public class TimeServerHandler extends ChannelHandlerAdapter {
private int counter = 0;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
//System.getProperty("line.separator") 获取回车换行符
String body = new String(req,"utf-8").substring(0,req.length-(System.getProperty("line.separator").length()));
System.out.println("从客戶端发送的消息:"+body+" counter:"+ ++counter);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.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();
}
}
测试 先运行服务端,在运行客户端
结果会发现并没有我们想象的那样,客户端发送一百条请求,服务端会接受一百条请求并且打印这些请求,然后在响应客户端一百条响应,客户端打印一百条来自服务端响应的消息
出现这种的原因在于消息在TCP中发生了粘包和拆包的问题
在Netty中解决粘包和拆包问题需要使用解码器(Decoder),在Netty中提供了许多的解码器,比如LineBasedFrameDecoder,回车符解码器,FixedLengthFrameDecoder固定长度解码器。这些解码器的父类是ByteToMessageDecoder
我们以LineBasedFrameDecoder解码器来说明其流程
还是以前面的例子来介绍其流程(我们前面的例子在消息的结尾都加上了回车换行符)
首先我们在TimeServer类中ChlidChannelHandler 方法中在初始化Channel的时候加上LineBasedFrameDecoder(1024)其中1024表示这个数据包的大小,也就是编码规则(我们这里表示的是换行符)之间的消息大小,如果数据包大小超过就会报异常
我们还加上了StringDecoder()解码器,将消息变成String类型。这样我们就可以在TimeServerHandler()中不需要讲消息转变成String类型可以直接使用。
private class ChlidChannelHandler extends ChannelInitializer{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
/**
* LineBasedFrameDecoder() 换行符解码器
* StringDecoder()将接收到的对象转换成String对象
*/
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeServerHandler());
}
}
我们点开LineBasedFrameDecoder类或者StringDecoder类就可以看到decode方法中编写的解码规则。并且将解码后的消息放在一个list集合中,该消息会传到给下一个解码器或者处理器(下面代码为StringDecoder解码器中的decode方法)
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List
然后我们在TimeCilent类中加入LineBasedFrameDecoder类和StringDecoder类解码器。
private class ChlidChannelHandler extends ChannelInitializer{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
/**
* LineBasedFrameDecoder() 换行符解码器
* StringDecoder()将接收到的对象转换成String对象
*/
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeServerHandler());
}
}
最后 运行服务端,接着客户端