Tcp是个“流”协议,所谓流就是没有界限的一串数据。可以类比一下水流,没有分极限。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分。所以一个业务数据可能被TCP拆分成多个包进行发送,也有可能把多个小的数据包封装成一个大的数据包发送。这就是TCP的拆包和粘包。
出现TCP拆包/粘包的几个原因:
TCP拆包/粘包问题的解决策略:
重现问题,将上一篇文章中的代码进行修改。
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字符串。
分别在服务端和客户端的初始化时使用了两个解码器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() {
@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次消息。
LineBasedFrameDecoder是通过换行符来实现的拆包粘包,Netty中还有两种常见的解码器,分别是利用“分隔符”和“定长”的解码器。
文章最开始提到了TCP拆包/粘包问题的解决策略,大体分为4种,Netty分别对这四种进行了抽象。