TCP粘包/拆包
Tcp是个“流”协议,所谓流就是没有界限的一串数据。可以类比一下水流,没有分极限。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分。所以一个业务数据可能被TCP拆分成多个包进行发送,也有可能把多个小的数据包封装成一个大的数据包发送。这就是TCP的拆包和粘包。
出现TCP拆包/粘包的几个原因:
- 程序write写入的字节大小大于套接口发送缓冲区大小。
- 进行MSS(TCP传输时的最大报文段长度)大小的TCP分段。
- 以太网帧的payload(封装后的不含头和尾的数据包部分)大于MTU( 最大传输单元()进行IP分片。
TCP拆包/粘包问题的解决策略:
- 消息定长,例如,每个报文的大小固定长度为200字节,如果不够,空位补空格。
- 在包尾增加回车换行符进行分割,例如FTP协议。
- 将特殊字符作为消息结束的标志,回车换行符只是其中的一种。
- 将消息分为消息头和消息体,消息头中包含表示消息长度的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度。
Netty对于粘包/拆包问题重现
重现问题,将上一篇文章中的代码进行修改。
1.服务端修改NettyServerHandler类,加一个计数器:
public class NettyServerHandler extends ChannelHandlerAdapter {
private int count = 0;//记录客户端请求次数。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws UnsupportedEncodingException {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8");
System.out.println("收到客户端消息:" + body + ";次数是:" + count++);
String currentTime = "query".equalsIgnoreCase(body)?new java.util.Date(System.currentTimeMillis()).toString():"error";
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();
}
}
2.客户端修改NettyClientHandler类,向服务端写100次数据。
public class NettyClientHandler extends ChannelHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf firstMessage = null;
//此处做了修改。
byte[] req = ("query"+System.getProperty("line.separator")).getBytes();
for(int j=0; j<100; j++){
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
ctx.writeAndFlush(firstMessage);
}
}
@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");
System.out.println("Now is : " + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
服务端的运行结果如下,次数并不是100次,而是发生了粘包,如果把客户端的字符串换成一个更长的字符串,效果会更明显。
客户端的运行结果如下,客户端在接收消息时也发生了粘包,收到了两个连在一起的error字符串。
Netty对于粘包/拆包问题的初步解决
分别在服务端和客户端的初始化时使用了两个解码器LineBasedFrameDecoder和StringDecoder,这两个解码器是通过换行符来配合实现的拆包粘包。
服务端修改内容如下
//修改了NettyServerInit类
public class NettyServerInit extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
//添加了下面两行。
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}
//NettyServerHandler修改如下:
public class NettyServerHandler extends ChannelHandlerAdapter {
private int count = 0;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws UnsupportedEncodingException {
//直接强转了消息,不用再处理
String body = (String) msg;
System.out.println("收到客户端消息:" + body + "次数是:" + ++count);
String currentTime = ("query".equalsIgnoreCase(body)?new java.util.Date(System.currentTimeMillis()).toString():"error") +System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
客户端端修改内容如下
public class NettyClient {
public void connect(int port,String host) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
public void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
//在此处加了两个解码器。
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new NettyClientHandler());
}
});
ChannelFuture f = b.connect("192.168.1.104",port).sync();
f.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new NettyClient().connect(55557,"127.0.0.1");
}
}
public class NettyClientHandler extends ChannelHandlerAdapter {
private int count = 0;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf firstMessage = null;
byte[] req = ("query"+System.getProperty("line.separator")).getBytes();
for(int j=0; j<100; j++){
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
ctx.writeAndFlush(firstMessage);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//此处不用再处理消息,直接强转即可。
String body = (String) msg;
System.out.println("第" + ++count + "次收到时间N : " + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
服务端运行结果如下:
客户端运行结果如下:
可以发现,服务端和客户端都达到了预期效果,客户端发送了100次消息,服务端响应了100次消息。
Netty对于粘包/拆包问题其他解决方案
LineBasedFrameDecoder是通过换行符来实现的拆包粘包,Netty中还有两种常见的解码器,分别是利用“分隔符”和“定长”的解码器。
文章最开始提到了TCP拆包/粘包问题的解决策略,大体分为4种,Netty分别对这四种进行了抽象。
- DelimiterBasedFrameDecoder自定义消息分隔符解码器。
- FixedLengthFrameDecoder固定长度解码器。