TCP是一个“流”协议,TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小包封装成一个大的数据包进行发送,这就是TCP的粘包和拆包问题。
假设客户端分别发送了俩个数据包D1和D2到服务端。但服务端每次读到的字节数是不确定的,所以可能存在下面四种情况:
问题产生的原因有三点:
我们通过一个简单的Netty服务端和客户端例子来复现一下粘包和拆包的异常情况。这个例子中客户端会向服务端发送一个指定的字符串来查询当前时间,服务端接收客户端发送的字符串,如果是合法的查询字符串则返回当前的系统实现,否则返回错误提示。
package problem;
import java.util.logging.Logger;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import problem.handler.TimeServerHandler;
public class TimeServer {
private static final Logger logger = Logger.getLogger(TimeServer.class.getName());
public void run(int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// TODO Auto-generated method stub
ch.pipeline().addLast(new TimeServerHandler());
}
});
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
logger.warning(e.getLocalizedMessage());
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
new TimeServer().run(8080);
}
}
package problem.handler;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.logging.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = Logger.getLogger(TimeServerHandler.class.getName());
private int counter;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// TODO Auto-generated method stub
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, StandardCharsets.UTF_8).substring(0,
req.length - System.getProperty("line.separator").length());
System.out.println("The time server receive order : " + body + "; This counter is : " + (++counter));
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)
? new Date(System.currentTimeMillis()).toString() + System.getProperty("line.separator")
: "BAD ORDERE";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.warning(cause.getLocalizedMessage());
ctx.close();
}
}
package problem;
import java.util.logging.Logger;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import problem.handler.TimeClientHandler;
public class TimeClient {
private static final Logger logger = Logger.getLogger(TimeClient.class.getName());
public void run(String host, int port) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class).option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// TODO Auto-generated method stub
ch.pipeline().addLast(new TimeClientHandler());
}
});
ChannelFuture future = b.connect(host, port).sync();
future.channel().closeFuture().await(5000L);
} catch (InterruptedException e) {
logger.warning(e.getLocalizedMessage());
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) {
new TimeClient().run("127.0.0.1", 8080);
}
}
package problem.handler;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = Logger.getLogger(TimeClientHandler.class.getName());
private byte[] req;
private int counter;
public TimeClientHandler() {
req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client connect server successful!");
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 {
System.out.println("读取");
ByteBuf buf = (ByteBuf) msg;
byte[] receiveByte = new byte[buf.readableBytes()];
buf.readBytes(receiveByte);
String body = new String(receiveByte, StandardCharsets.UTF_8);
System.out.println("Now is " + body + "; counter:" + (++counter));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.err.println(cause.getLocalizedMessage());
ctx.close();
}
}
同样客户端应该收到两条回复才对,但是这里只有一条,也发生了粘包。由于我们程序中没有考虑粘包和拆包所以发生了上面的情况。
为了解决TCP粘包和拆包导致的半包读写问题,Netty提供了一些自带的编码器用来处理半包。下面的代码是对上面发生异常的代码做的修改。
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeServerHandler());
也就是在原有的基础上增加了LineBasedFrameDecoder
这个解码器和StringDecoder
解码器。
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// TODO Auto-generated method stub
// ByteBuf buf = (ByteBuf) msg;
// byte[] req = new byte[buf.readableBytes()];
// buf.readBytes(req);
// String body = new String(req, StandardCharsets.UTF_8).substring(0,
// req.length - System.getProperty("line.separator").length());
String body = (String) msg;
System.out.println("The time server receive order : " + body + "; This counter is : " + (++counter));
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)
? new Date(System.currentTimeMillis()).toString() + System.getProperty("line.separator")
: "BAD ORDERE";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
可以看到,将上面转化接收数据的过程注释掉,然后把msg强转为String即可(相当简洁)。
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeClientHandler());
修改方式和服务端一样。
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// System.out.println("读取");
// ByteBuf buf = (ByteBuf) msg;
// byte[] receiveByte = new byte[buf.readableBytes()];
// buf.readBytes(receiveByte);
// String body = new String(receiveByte, StandardCharsets.UTF_8);
String body = (String) msg;
System.out.println("Now is " + body + "; counter:" + (++counter));
}
同样和服务端一样。
此时就会发现粘包的问题已经解决了,使用Netty来解决对使用者来说是很方便的,只需要将支持半包的解码的handler添加到ChannelPipeline
中即可,不需要额外的代码。
LineBasedFrameDecoder
的工作原理是它依次遍历ByteBuf中的可读字节,判断是否有"\n"或者"\r\n",如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,同时支持配置单行最大长度,如果连续读取到最大长度后没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。
StringDecoder
的作用就是将接收到的对象转为字符串,然后再继续调用后面的handler。LineBasedFrameDecoder
+StringDecoder
组合就是按行切换的文本解码器。初次之外,Netty还有其他多种解码器,用来满足不同的需求。