简述TCP的粘包和拆包
TCP编程中无论是服务端还是客户端,读取和发送消息时都要考虑TCP底层的粘包和拆包机制,TCP是一个‘流’协议,数据是没有界限的,TCP底层不知道上层业务数据的含义,它会根据TCP缓冲区的实际情况进行包的划分,所以相对于业务来说,一个完整的包可能会被TCP拆分多个包进行发送 ,也有可能把许多小的包封装成一个大的数据包发送,这就是TCP的粘包和拆包的问题
粘包、拆包问题说明
现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,如下所示:
第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不考虑。
第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以接收端不知道如何处理。
第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。
TCP粘包拆包发生的原因有很多,主要包括如下:
粘包和拆包的解决策略
由于TCP无法知道上层业务数据,所以TCP底层无法保证数据包不会被拆分和重组,所以我们只能利用上层的应用协议栈设计来解决,归纳如下:
以上3种方式,客户端接受到包的时候就可以根据这些约束区分出来不同的包。
为了解决TCP中粘包、拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包,直接使用这些类库,TCP粘包拆包问题就变得非常容易
LineBasedFremeDecoder解决TCP粘包问题
LineBasedFremeDecoder改造服务端代码
public class NettyServer {
public void bind(int port){
//NioEventLoopGroup是一个线程组,包含一组NIO线程
EventLoopGroup bossGroup = new NioEventLoopGroup();//用于服务端接受客户端的连接
EventLoopGroup workerGroup = new NioEventLoopGroup();//用于SocketChannel的网络读写
try{
//ServerBootstrap对象是Netty用于启动NIO服务端的辅助启动类
ServerBootstrap bs = new ServerBootstrap();
bs.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)//设置创建的channel
.option(ChannelOption.SO_BACKLOG, 1024)//设置NioServerSocketChannel的TCP参数
.childHandler(new ChildChannelHandler());//绑定I/O事件处理类
ChannelFuture sync = bs.bind(port).sync();//绑定监听端口并调用同步阻塞方法等待绑定操作完成
sync.channel().closeFuture().sync();//等待服务器链路关闭之后main函数才退出
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
class ChildChannelHandler extends ChannelInitializer{
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new TimeServerHandler());
}
}
class TimeServerHandler extends ChannelHandlerAdapter{
private int counter;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String)msg;
System.out.println("The time server received order :" + body +";the counter is:" + ++counter);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
//响应的消息也添加回车换行符
currentTime = currentTime + System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
System.out.println("done" +currentTime);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//Netty把write方法并不直接将消息写入到SocketChannel中,调用write方法只是把待发送的消息放到缓冲数组中,
// 调用flush方法才将消息全部写道SocketChanel
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//释放相关句柄等资源
ctx.close();
}
}
public static void main(String[] args) {
new NettyServer().bind(9988);
}
}
LineBasedFremeDecoder改造客户端代码
public class NetttClient {
public void connect(String host, int port){
EventLoopGroup group = new NioEventLoopGroup();
try{
//创建客户端辅助启动类Bootstrap
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,Boolean.TRUE)
.handler(new ChannelInitializer() {
//创建NioSocketChannel成功之后,进行初始化
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new TimeServerHandler());
}
});
ChannelFuture sync = bootstrap.connect(host, port).sync();
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放NIO线程组资源
group.shutdownGracefully();
}
}
class TimeServerHandler extends ChannelHandlerAdapter {
//private final ByteBuf firstMessage;
private byte[] req;
private int counter;
public TimeServerHandler() {
//给消息添加回车换行符
req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();
}
//当客户端和服务端TCP链路建立成功之后,Netty的NI线程会调用channelActive方法,发送查询指定给服务端
@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 {
String body = (String) msg;
System.out.println("now is :" + body +";the couter is :" + ++counter);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("Unexpected exception frm downstream:" + cause.getMessage());
ctx.close();
}
}
public static void main(String[] args){
new NetttClient().connect("127.0.0.1",9988);
}
}
在改造的代码中,新增了2个解码器LineBasedFrameDecoder和StringDecoder,发送的带有回车换行符的消息在被接收后msg就是删除了回车换行符的消息,不需要再对消息进行编码解码。LineBasedFrameDecoder的工作原理就是一次遍历ByteBUF中可读字节,判断看是否有“\n”或者“\r\n”,如有,就以此位置为结束位置,这样可以读到一行一行的息,LineBasedFrameDecoder是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度,如果连续读取到最大长度仍然没有发现换行符就会抛出异常, 同时忽略之前读取的异常码流。StringDecoder的功能就是将接收到的对象转成字符串,然后继续调用Handler,LineBasedFrameDecoder+StringDecoder组合就是按行切换的文本解码器。
当然,基于LineBasedFrameDecoder+StringDecoder组合是针对回车换行符,如果消息没有回车换行符的消息就需要使用其他的半包解码器,Netty提供了支持多种TCP粘包/拆包的解码器,用来满足不同需求