netty-netty粘包和拆包
摘自<netty权威指南>
为什么会有粘包和拆包的问题
netty粘包和拆包的问题,本质上归结于TCP的粘包和拆包。
网络上发送一个完整的数据包时,可能会被TCP拆分成多个包进行发送,也可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP的粘包和拆包问题
TCP的粘包和拆包的情况
现有客户端发送两个数据包P1和P2给服务器端,由于服务端一次读取的字节数是不确定的
TCP的粘包和拆包的情况:
- 服务器端两次读到两个独立的包,分别是P1和P2,没有粘包和拆包的情况
- 服务器端一次接收两个数据包,P1和P2粘合在一起被称为TCP粘包
- 服务器端分两次读取到两个数据包,第一次读取到了完整的P1和P2包的部分内容(P2_1),第二次读取到了P2包的剩余内容(P2_2),称为TCP的拆包
- 服务器端分两次读取到两个数据包,第一次读取到了P1包的部分内容(P1_1),第二次读取到了P1包的剩余内容(P1_2)和完整的P2包,称为TCP的拆包
- 如果P1和P2包比较大,服务器端需要分多次才能将D1和D2包接收完全,期间会发生多次拆包
粘包问题解决方案
- 消息定长,如每个报文的大小为固定长度200字节,如果不够,空位补空格
- 在包尾增加回车换行进行分隔,如FTP协议
- 将消息分成消息头和消息体。消息头中包含消息总长度(或者消息长度)的字段。通常设计思路为消息头的第一个字段使用int32表示消息的长度
粘包例子
改造一下TimeServerHandler
:
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
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());
System.out.println("time server recv order [x]: "
+ body + ",the counter is : " + counter++ + ";\n");
String currentTime = body.startsWith("time") ?
new Date(System.currentTimeMillis()).toString(): "bad time ";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx)
throws Exception {
ctx.flush();
// ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
// .addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) throws Exception {
ctx.close();
}
}
改造一下TimeClientHandler
:
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private int counter;
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
ByteBuf message = null;
message = Unpooled.buffer();
message.writeBytes("time".getBytes());
ctx.writeAndFlush(message);
for (int i = 0; i < 100; i++) {
String body = " request order ";
body = body + i + System.getProperty("line.separator");
message = Unpooled.buffer(body.getBytes().length);
message.writeBytes(body.getBytes());
ctx.writeAndFlush(message);
}
}
@Override
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("client recv now is [x] :" + body
+ ",the counter is: " + counter++ + "\n");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) throws Exception {
ctx.close();
}
}
正常情况下服务器端接收到最后一条数据应该是这样的,即order为99,而counter为99:
time server recv order [x]: request order 99,the counter is : 99;
但是,实际可能接收到的是这样的结果:即order为99,而counter为43。客户端总共发送了100次请求,而服务器端只接收只用了43次
time server recv order [x]: request order 99,the counter is : 43;
client established success...
client recv now is [x] :bad time ... time ,the counter is: 0
client recv now is [x] :bad time ... time ,the counter is: 1
说明发生TCP粘包的情况
LineBasedFrameDecoder+StringDecoder解决粘包问题
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
LineBasedFrameDecoder原理
依次遍历ByteBuf字节,判断是否是以
\n
或者是\r\n
结尾,如果找到,则以此位置为结束位置。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略之前读到的异常码流
StringDecoder原理
将接收的对象直接转成string类型
LineBasedFrameDecoder+StringDecoder
组合就是换行切换的文本解码器