TCP粘包和拆包

1.什么是TCP粘包和拆包

粘 / 黏
TCP是一个字节流协议,所谓流,就像流水一样,是连成一片的,没有分割线,你没法知道什么时候开始,什么时候结束,也就是我们通过TCP传输的数据是一连串没有界限的数据,TCP底层并不了解上层要传输的业务数据的具体含义,TCP只会根据缓冲区的大小及实际情况进行数据包的分割,那么我们一个完整的业务数据,可能会被TCP拆分成多个包进行发送,也有可能业务上的多条完整数据被合并成一个包发送,这就是TCP的粘包和拆包问题;
一个TCP协议传输的过程:

在这里插入图片描述

发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由接收端获取;

在这里插入图片描述
1、第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象;
2、第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包,这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理;
3、第三种情况,这种情况有两种表现形式,接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包,这两种情况如果不加特殊处理,对于接收端同样是不好处理的;

粘包和拆包发生原因

1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;
2、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;
3、待发送数据大于最大报文长度,TCP在传输前将进行拆包;
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;

粘包和拆包解决策略

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,该问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:
消息定长:发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从缓冲区中读取固定长度的数据,这就自然而然的把每个数据包拆分开来;
设置消息边界:服务端从网络流中按消息边界分离出消息内容,比如在数据包末尾增加回车换行符进行分割;
将消息分为消息头和消息体:消息头中包含表示消息总长度(或者消息体长度)的字段,消息体是要读取的内容;
更复杂的应用层协议:比如Netty中实现的一些协议对粘包、拆包进行处理;

Netty粘包和拆包解决方案

Netty框架对于客户端和服务端之间的数据传输做了很好的处理,客户端在发送数据之前先对数据按一定的规则进行编码,服务端在接收到数据后按照相同的规则进行解码,这就是Netty解决粘包拆包问题的思路;
对于粘包拆包问题,Netty 已经为我们提供了很多不同的解码器,在无必要时不必重复发明轮子,我们可以直接使用Netty现成的解码器即可;
Netty中提供如下四种解码器用来解决粘包和拆包问题:
1、固定长度解码器FixedLengthFrameDecoder;
每个应用层数据包都拆分成都是固定长度的大小,比如1024字节;
对于使用固定长度的粘包和拆包场景,可以使用FixedLengthFrameDecoder,该解码器会每次读取固定长度的消息,如果当前读取到的消息不足指定长度,那么就会等待下一个消息到达后进行补足,其使用也比较简单,只需要在构造函数中指定每个消息的长度即可;
由于解码有可能需要等待下一个包进行补全,代码相对复杂,所以Netty框架帮我们提供了解码器,但是对于编码器,需要用户自行编写,因为编码时只需要将不足指定长度的部分进行补全即可,代码比较简单,所以Netty就没有帮我们实现编码器;
数据在编码发送的时候,以固定长度作为一条完整的消息,代码实现:
channelPipeline.addLast(new FixedLengthFrameDecoder(22));
这里面的22表示占用的字节个数,utf-8编码下,一个汉字占3个字节,一个英文字母占1个字节;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * 服务handler
 * 
 * @author liulei
 */
@Slf4j
@ChannelHandler.Sharable
public class EChoServerHandler extends ChannelInboundHandlerAdapter {

    public EChoServerHandler() {
        super();
        log.info("EChoServerHandler方法执行");
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        super.channelRegistered(ctx);
        log.info("channelRegistered方法执行");
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        super.channelUnregistered(ctx);
        log.info("channelUnregistered方法执行");
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        log.info("channelActive方法执行");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        log.info("channelInactive方法执行");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf)msg;
        System.out.println("服务端接受到消息:"+byteBuf.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        super.channelReadComplete(ctx);
        log.info("channelReadComplete方法执行");
        // 数据读完后回调用此方法
        // 读取完后关闭
        //ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        super.userEventTriggered(ctx, evt);
        log.info("userEventTriggered方法执行");
    }

    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
        super.channelWritabilityChanged(ctx);
        log.info("channelWritabilityChanged方法执行");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        log.info("exceptionCaught方法执行");
    }

    @Override
    public boolean isSharable() {
        log.info("isSharable方法执行");
        return super.isSharable();
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        super.handlerAdded(ctx);
        log.info("handlerAdded方法执行");
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        super.handlerRemoved(ctx);
        log.info("handlerRemoved方法执行");
    }

}
@Slf4j
public class EchoServerRun {

    public static final int port = 8888;

    public static void main(String[] args) {
        EchoServerRun echoServerRun = new EchoServerRun();
        echoServerRun.run();
    }

    public void run(){
        final EChoServerHandler eChoServerHandler = new EChoServerHandler();
        // 创建线程池
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        // 创建服务启动类
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        // 给启动引导类进行配置
        serverBootstrap.group(eventLoopGroup).channel(NioServerSocketChannel.class)

                .childHandler(new ChannelInitializer<SocketChannel>() {

                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(eChoServerHandler);
                    }
                });
        try {
            // 端口绑定
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            // 优雅关闭
            eventLoopGroup.shutdownGracefully();
        }
    }


}
/**
 * @author liulei
 **/
@Slf4j
public class EChoClientHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        super.channelRegistered(ctx);
    }

    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        super.channelUnregistered(ctx);
    }

    /**
     * 连接通道建立
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String message = "hello,netty.";
        for (int i = 0; i < 100; i++) {
            ctx.writeAndFlush(Unpooled.copiedBuffer(message.getBytes(CharsetUtil.UTF_8)));
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
    }

    /**
     * 数据连接被建立
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        super.channelReadComplete(ctx);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
        super.channelWritabilityChanged(ctx);
    }

    /**
     * 捕获一个异常时调用
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
    }
}
/**
 * @author liulei
 **/
public class EChoClientRun {

    public static void main(String[] args) {
        EChoClientRun eChoClientRun = new EChoClientRun();
        eChoClientRun.run();
    }

    public void run() {
        final EChoClientHandler eChoServerHandler = new EChoClientHandler();
        // 创建线程池
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        // 启动引导类
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(eChoServerHandler);
                    }
                });
        //绑定一个端口,返回未来的通道 .bind() --> udp
        try {
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888).sync();
            Channel channel = channelFuture.channel();
            channel.writeAndFlush(Unpooled.copiedBuffer("123", CharsetUtil.UTF_8));
            // 当channel被关闭的时候会通知此处关闭chanel(closeFuture方法)
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
            eventLoopGroup.shutdownGracefully();
        }

    }


}

客户端发送了100个hello,netty.服务端接受结果为:

在这里插入图片描述
发生了粘包现象我们修改用固定长度解码器FixedLengthFrameDecoder
在这里插入图片描述
客户端和服务端均要加上字节长度。

在这里插入图片描述

2、行解码器LineBasedFrameDecoder
每个应用层数据包,都以换行符作为分隔符(\r\n或者\n),进行分割拆分;
数据在编码发送的时候,会以换行符作为一条完整的消息;
也没有提供编码器;
代码实现:

 pipeline.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
 String message = "hello,netty.";
        for (int i = 0; i < 100; i++) {
            ctx.writeAndFlush(Unpooled.copiedBuffer(message.concat("\n").getBytes(CharsetUtil.UTF_8)));
        }

3、分隔符解码器DelimiterBasedFrameDecoder
每个应用层数据包,通过自定义的分隔符进行分割拆分,该解码器与LineBasedFrameDecoder本质上是一样的,都是使用分隔符对数据包进行拆分,只是可以指定自己的分割符;
数据在编码发送的时候,会以一个自定义的分隔符作为一条完整的消息;
也没有提供编码器;
代码实现:

channelPipeline.addLast(
        new DelimiterBasedFrameDecoder(Integer.MAX_VALUE,
        Unpooled.copiedBuffer("$", CharsetUtil.UTF_8))
);

4、基于数据包长度的解码器 LengthFieldBasedFrameDecoder(重要)

将应用层数据包的长度,作为接收端应用层数据包的拆分依据,按照应用层数据包的大小解码,这个解码器要求应用层协议中包含数据包的长度,这里的长度是动态长度; abcd sduihfd32 09werferjgvlk
一般是LengthFieldBasedFrameDecoderLengthFieldPrepender配合起来使用,前者是解码,后者是编码,它们处理粘拆包的主要思想是在生成的数据包中添加一个长度字段,用于记录当前数据包的长度,
LengthFieldBasedFrameDecoder会按照参数指定的包长度对接收到的数据进行解码,从而得到目标消息体数据,而LengthFieldPrepender则会在响应的数据前面添加指定的包长度,这个包长度保存了当前消息体的整体字节数据长度;
数据在编码发送的时候,会指定当前这条消息的长度;
在这里插入图片描述
使用前,对其构造函数参数进行说明:
maxFrameLength:指定了每个包所能传递的最大数据包大小;

lengthFieldOffset:指定了长度字段在字节码中的偏移量;

lengthFieldLength:指定了长度字段所占用的字节长度;

lengthFieldOffset和lengthFieldLength可以去掉长度字段

lengthAdjustment:对一些不仅包含有消息头和消息体的数据进行消息头的长度的调整,这样就可以只得到消息体的数据,这里的lengthAdjustment指定的就是消息头的长度;

initialBytesToStrip:对于长度字段在消息头中间的情况,可以通过
initialBytesToStrip忽略掉消息头以及长度字段占用的字节,从而得到消息体的内容和lengthFieldLength长度字节保存一致;

LengthFieldBasedFrameDecoder内部实现原理:
首先对长度字段进行设置,如果需要包含消息长度自身,则在原来长度的基础之上再加上lengthFieldLength的长度;
如果调整后的消息长度小于0,则抛出参数非法异常,对消息长度自身所占的字节数进行判断,以便采用正确的方法将长度字段写入到ByteBuf中,共有以下6种可能:

  1. 长度字段所占字节为1:如果使用1个Byte字节代表消息长度,则最大长度需要小于256个字节。对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeByte将长度值写入到ByteBuf中;
  2. 长度字段所占字节为2:如果使用2个Byte字节代表消息长度,则最大长度需要小于65536个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeShort将长度值写入到ByteBuf中;
  3. 长度字段所占字节为3:如果使用3个Byte字节代表消息长度,则最大长度需要小于16777216个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeMedium将长度值写入到ByteBuf中;
  4. 长度字段所占字节为4:创建新的ByteBuf,并通过writeInt将长度值写入到ByteBuf中;
  5. 长度字段所占字节为8:创建新的ByteBuf,并通过writeLong将长度值写入到ByteBuf中;
  6. 其它长度值:直接抛出Error;
channelPipeline.addLast(new LengthFieldBasedFrameDecoder(
        1024, 0, 4, 0, 4));
channelPipeline.addLast(new LengthFieldPrepender(4));
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Netty中的TCP粘包拆包问题是由于底层的TCP协议无法理解上层的业务数据而导致的。为了解决这个问题,Netty提供了几种解决方案。其中,常用的解决方案有四种[1]: 1. 固定长度的拆包器(FixedLengthFrameDecoder):将每个应用层数据包拆分成固定长度的大小。这种拆包器适用于应用层数据包长度固定的情况。 2. 行拆包器(LineBasedFrameDecoder):将每个应用层数据包以换行符作为分隔符进行分割拆分。这种拆包器适用于应用层数据包以换行符作为结束符的情况。 3. 分隔符拆包器(DelimiterBasedFrameDecoder):将每个应用层数据包通过自定义的分隔符进行分割拆分。这种拆包器适用于应用层数据包以特定分隔符作为结束标志的情况。 4. 基于数据包长度的拆包器(LengthFieldBasedFrameDecoder):将应用层数据包的长度作为接收端应用层数据包的拆分依据。根据应用层协议中包含的数据包长度进行拆包。这种拆包器适用于应用层协议中包含数据包长度的情况。 除了使用这些拆包器,还可以根据业界主流协议的解决方案来解决粘包拆包问题[3]: 1. 消息长度固定:累计读取到长度和为定长LEN的报文后,就认为读取到了一个完整的信息。 2. 使用特殊的分隔符:将换行符或其他特殊的分隔符作为消息的结束标志。 3. 在消息头中定义长度字段:通过在消息头中定义长度字段来标识消息的总长度。 综上所述,Netty提供了多种解决方案来解决TCP粘包拆包问题,可以根据具体的业务需求选择合适的解决方案[1][3]。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值