Netty的TCP粘包半包处理

1 TCP为何会有粘包半包?

1.1 粘包

发送方每次写入数据 < 套接字缓冲区大小

接收方读取套接字缓冲区数据不够及时

1.2 半包

发送方写入数据 > 套接字缓冲区大小

发送的数据大于协议的MTU ( Maximum Transmission Unit,最大传输单元),必须拆包

而且

一个发送可能被多次接收,多个发送可能被一次接收

一个发送可能占用多个传输包,多个发送可能公用一个传输包

本质是因为 TCP 是流式协议,消息无边界。

而UDP就像快递,虽然一次运输多个,但每个包都有边界,一个个签收
所以无此类问题。

清楚了问题产生的本质,那么就知道如何避免了,即确定消息的边界

 解决方案

1、使用自定义协议,来实现TCP的粘包/拆包问题

2、使用protobuf 协议,可以看前一篇记录

Netty ByteBuf

ByteBuf的基本结构

ByteBuf由一段地址空间,一个read index和一个write index组成。两个index分别记录读写进度,省去了NIO中ByteBuffer手动调用flip和clear的烦恼。

    +-------------------+------------------+------------------+
      | discardable bytes |  readable bytes  |  writable bytes  |
      |                   |     (CONTENT)    |                  |
      +-------------------+------------------+------------------+
      |                   |                  |                  |
      0      <=      readerIndex   <=   writerIndex    <=    capacity

通过上图可以很好的理解ByteBuf的数据划分。writer index到capacity之间的部分是空闲区域,可以写入数据;reader index到writer index之间是已经写过还未读取的可读数据;0到reader index是已读过可以释放的区域。

三个index之间的关系是:reader index <= writer index <= capacity

存储空间

ByteBuf根据其数据存储空间不同有可以分为三种:基于JVM堆内的,基于直接内存的和组合的。

堆内受JVM垃圾收集器的管辖,使用上相对安全一些,不用每次手动释放。弊端是GC是会影响性能的;还有就是内存的拷贝带来的性能损耗(JVM进程到Socket)。

直接内存则不受JVM的管辖,省去了向JVM拷贝数据的麻烦。但是坏处就是别忘了释放内存,否则就会发生内存泄露。相比于堆内存,直接内存的的分配速度也比较慢。

最佳实践:在IO通信的线程中的读写Buffer使用DirectBuffer(省去内存拷贝的成本),在后端业务消息的处理使用HeapBuffer(不用担心内存泄露)。

通过hasArray检查一个ByteBuf heap based还是direct buffer。

创建ByteBuf

ByteBuf提供了两个工具类来创建ByteBuf,分别是支持池化的Pooled和普通的Unpooled。Pooled缓存了ByteBuf的实例,提高性能并且减少内存碎片。它使用Jemalloc来高效的分配内存。

如果在Channel中我们可以通过channel.alloc()来拿到ByteBufAllocator,具体它使用Pool还是Unpool,Directed还是Heap取决于程序的配置。

索引的标记与恢复

markReaderIndex和resetReaderIndex是一个成对的操作。markReaderIndex可以打一个标记,调用resetReaderIndex可以把readerIndex重置到原来打标记的位置。

空间释放

discardReadByte可以把读过的空间释放,这时buffer的readerIndex置为0,可写空间和writerIndex也会相应的改变。discardReadBytes在内存紧张的时候使用用,但是调用该方法会伴随buffer的内存整理的。这是一个expensive的操作。

clear是把readerIndex和writerIndex重置到0。但是,它不会进行内存整理,新写入的内容会覆盖掉原有的内容。

参考案例

1定义协议

https://www.cnblogs.com/sidesky/p/6913109.html

2代码

常量

 

 协议格式

 

 协议类

SmartCarProtocol
package com.netty.protobuf;

import com.netty.config.ConstantValue;

import java.util.Arrays;

/**
 * <pre>
 * 自己定义的协议
 *  数据包格式
 * +——----——+——-----——+——----——+
 * |协议开始标志|  长度             |   数据       |
 * +——----——+——-----——+——----——+
 * 1.协议开始标志head_data,为int类型的数据,16进制表示为0X76
 * 2.传输数据的长度contentLength,int类型
 * 3.要传输的数据
 * </pre>
 */
public class SmartCarProtocol {

    /**
     * 消息的开头的信息标志
     */
    private int head_data = ConstantValue.HEAD_DATA;
    /**
     * 消息的长度
     */
    private int contentLength;
    /**
     * 消息的内容
     */
    private byte[] content;

    /**
     * 用于初始化,SmartCarProtocol
     *
     * @param contentLength
     *            协议里面,消息数据的长度
     * @param content
     *            协议里面,消息的数据
     */
    public SmartCarProtocol(int contentLength, byte[] content) {
        this.contentLength = contentLength;
        this.content = content;
    }

    public int getHead_data() {
        return head_data;
    }

    public int getContentLength() {
        return contentLength;
    }

    public void setContentLength(int contentLength) {
        this.contentLength = contentLength;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "SmartCarProtocol [head_data=" + head_data + ", contentLength="
                + contentLength + ", content=" + Arrays.toString(content) + "]";
    }
}

编码器

package com.netty.protobuf;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
 * <pre>
 * 自己定义的协议
 *  数据包格式
 * +——----——+——-----——+——----——+
 * |协议开始标志|  长度             |   数据       |
 * +——----——+——-----——+——----——+
 * 1.协议开始标志head_data,为int类型的数据,16进制表示为0X76
 * 2.传输数据的长度contentLength,int类型
 * 3.要传输的数据
 * </pre>
 */
public class SmartCarEncoder extends MessageToByteEncoder<SmartCarProtocol> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, SmartCarProtocol msg, ByteBuf out) throws Exception {
        // 写入消息SmartCar的具体内容
        // 1.写入消息的开头的信息标志(int类型)
        out.writeInt(msg.getHead_data());
        // 2.写入消息的长度(int 类型)
        out.writeInt(msg.getContentLength());
        // 3.写入消息的内容(byte[]类型)
        out.writeBytes(msg.getContent());
     
    }
}

解码器

package com.netty.protobuf;

import com.netty.config.ConstantValue;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

/**
 * <pre>
 * 自己定义的协议
 *  数据包格式
 * +——----——+——-----——+——----——+
 * |协议开始标志|  长度             |   数据       |
 * +——----——+——-----——+——----——+
 * 1.协议开始标志head_data,为int类型的数据,16进制表示为0X76
 * 2.传输数据的长度contentLength,int类型
 * 3.要传输的数据,长度不应该超过2048,防止socket流的攻击
 * </pre>
 */
public class SmartCarDecoder extends ByteToMessageDecoder {

    /**
     * <pre>
     * 协议开始的标准head_data,int类型,占据4个字节.
     * 表示数据的长度contentLength,int类型,占据4个字节.
     * </pre>
     */
    public final int BASE_LENGTH = 4 + 4;
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {

        // 可读长度必须大于基本长度
        if (buffer.readableBytes() >= BASE_LENGTH) {   // 记录包头开始的index
            int beginReader;

            while (true) {
                // 获取包头开始的index
                beginReader = buffer.readerIndex();
                // 标记包头开始的index
                buffer.markReaderIndex();
                // 读到了协议的开始标志,结束while循环
                if (buffer.readInt() == ConstantValue.HEAD_DATA) {
                    break;
                }

                // 未读到包头,略过一个字节
                // 每次略过,一个字节,去读取,包头信息的开始标记
                buffer.resetReaderIndex();
                buffer.readByte();

                // 当略过,一个字节之后,
                // 数据包的长度,又变得不满足
                // 此时,应该结束。等待后面的数据到达
                if (buffer.readableBytes() < BASE_LENGTH) {
                    return;
                }
            }

            // 消息的长度

            int length = buffer.readInt();
            // 判断请求数据包数据是否到齐
            if (buffer.readableBytes() < length) {
                // 还原读指针
                buffer.readerIndex(beginReader);
                return;
            }

            // 读取data数据
            byte[] data = new byte[length];
            buffer.readBytes(data);

            SmartCarProtocol protocol = new SmartCarProtocol(data.length, data);
            out.add(protocol);
        }
    }
}

客户端

    //传输自定义协议数据
    pipeline.addLast(new SmartCarEncoder());
    pipeline.addLast(new SmartCarDecoder());
    pipeline.addLast(new ClientChannelHandler());

服务端

    //传输自定义协议数据
    pipeline.addLast(new SmartCarEncoder());
    pipeline.addLast(new SmartCarDecoder());
    //自定义Handler
    pipeline.addLast(new ServerChannelHandler());

客户端发送数据

 //传输自定义协议数据
    @Async
    public String sendMsgSmartCarProtocol(Msg msg){
        // 发送SmartCar协议的消息
        // 要发送的信息
        String data = GsonUtil.GsonString(msg);
        Channel channel = channelContainer.getChannel();
        String socketString = channel.localAddress().toString();
        // 获得要发送信息的字节数组
        byte[] content = data.getBytes(Charset.defaultCharset());
        // 要发送信息的长度
        int contentLength = content.length;
        SmartCarProtocol protocol = new SmartCarProtocol(contentLength, content);
        channel.writeAndFlush(protocol);
        return socketString;
    }

服务端接收数据

    private String beat = "hello";
    private int count = 0;
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object obj) throws Exception {
        //传输自定义协议数据
        // 用于获取客户端发来的数据信息
        SmartCarProtocol body = (SmartCarProtocol) obj;
        byte[] content = body.getContent();
        if(beat.equals(new String(content))){
            System.out.println("Server接受的客户端的信息 :"+ new String(body.getContent()));
            // 会写数据给客户端
            String str = "ok";
            byte[] bytes = str.getBytes(Charset.defaultCharset());
            SmartCarProtocol response = new SmartCarProtocol(bytes.length,
                    bytes);
            ctx.writeAndFlush(response);
        }else{
            count ++;
            System.out.println("Server接受的客户端的信息 :"+count+","+ new String(body.getContent()));
        }

        //传输字符窜数据
//        String socketString = ctx.channel().remoteAddress().toString();
//        if(beat.equals(obj.toString())){
//            ctx.writeAndFlush("ok");
//        }else{
//            System.out.println("client:"+socketString+":"+obj.toString());
//        }
        //传输实体类数据
//        MessageProto.Message message = (MessageProto.Message) obj;
//        String socketString = ctx.channel().remoteAddress().toString();
//        if(beat.equals(message.getContent())){
//            System.out.println("client:"+socketString+":"+message.getContent());
//            MessageProto.Message pong = MessageProto.Message.newBuilder().setContent("ok").build();
//            ctx.writeAndFlush(pong);
//        }else {
//            count ++;
//            System.out.println("服务端接收客户端"+socketString+"的数据:");
//            System.out.println(count+","+message.getId()+","+message.getContent());
//            String content = message.getContent();
//            System.out.println(content);
//            Msg msg = GsonUtil.GsonToBean(content, Msg.class);
//            msg.setRemoteaddress(socketString);
//            TaskService taskService = SpringUtil.getBean(TaskService.class);
//            taskService.insertMsg(msg);
//        }
    }

 

以上测试代码,仅供参考

posted @ 2021-11-04 14:12  一心二念  阅读(520)  评论(0编辑  收藏  举报