Netty 4.x自定义协议实现与C/C++通信

713 阅读3分钟

本文使用场景:Java作为客户端和C++系统进行数据交换,以得到satellite设备上的数据(temperature,current,voltage)及指令(upstream,downstream)。 为了实现以上目标,我们从以下几方面考虑:

  1. 通信协议制订
  2. 使用大端还是小端字节序

经讨论最终结果为:使用小端字节序的自定义通信协议,其中包体部分使用protobuf进行编码。 自定义协议如下: MagicNumber,Length(数据包的总长度,包括包头和包体),version(版本号,为后续协议升级做准备),kind(数据包类型),serialize(序列化类型),guid(数据包标识,用于包追踪),包体(业务实体序列化数据)

image.png

image.png 以上问题解决以后就是编码实现了. 源代码见此处:github.com/gisonwin/ne…

  • 小端实现
               //读包体长度 LE是小端 little endian
            int length = in.readIntLE();
            log.info("package length =" + length);
            //读取主版本号
            short majorVersion = in.readShortLE();
            //读取次版本号
            short minorVersion = in.readShortLE();
            //读包类型
            byte kind = in.readByte();
            //读取序列化类型,默认为protobuf
            byte serialize = in.readByte();

byte就不区分大小端了.

  • 编解码
public class MessageDecoder extends ReplayingDecoder<Void> {
        protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception {
        //......
                int BASE_LENGTH = 4 + 4; //HEADER+length 4+4
        int readableBytes = in.readableBytes();
        if (readableBytes > BASE_LENGTH) {  //HEADER+length 4+4 ,可读字节要大于header和包体长度才会解码
                        //读取Header
            int beginReader;
            //一直读数据,直到讲到的数据是header以后,再向后读取数据
            int header;
            while (true) {
                beginReader = in.readerIndex();
                in.markReaderIndex();//标记包头开始的位置
                int readInt = in.readIntLE();

                if ((header = readInt) == MAGIC_NUMBER) {
                    //读到了header, 开始读后面的数据
                    break;
                }
                in.resetReaderIndex();//重置reader index
                in.readByte();
                // 未读到包头,略过一个字节
                // 每次略过,一个字节,去读取,包头信息的开始标记
                // 当略过,一个字节之后,
                // 数据包的长度,又变得不满足
                // 此时,应该结束。等待后面的数据到达
                if (in.readableBytes() < BASE_LENGTH) {
                    return;
                }
            }
            ......
           int dataLength = length - BASE_LENGTH;//后续数据的长度
            // 判断请求数据包数据是否到齐
            int readableBytes1 = in.readableBytes();
            if (readableBytes1 < dataLength) {
                // 还原读指针
                in.readerIndex(beginReader);
                log.error("数据包不完整,{},{}", readableBytes1, dataLength);
                return;
            }     
        }}
#编码器
public class MessageEncoder extends MessageToByteEncoder<NettyMessage> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, NettyMessage msg, ByteBuf out) throws Exception {
        out.writeIntLE(msg.getMagicNumber());//同步字
        out.writeIntLE(msg.getLength());//长度
        out.writeShortLE(msg.getMajorVersion());//主版本号
        out.writeShortLE(msg.getMinorVersion());//次版本号
        out.writeByte(msg.getKind());//包类型
        out.writeByte(msg.getSerialize());//序列化方式
        //将guid byte array 修改为小端 写出去
        byte[] data = EndianUtils.BigEndian16BytesToLittleEndianBytes(msg.getGuid());
        out.writeBytes(data);//guid little endian
        out.writeBytes(msg.getByteData());//写的proto buff的字节流
    }
}
  • 客户端
public class NettyClientBootstrap {
    //......
        private ChannelFuture start() throws Exception {

        Bootstrap bootstrap = new Bootstrap();
        InetSocketAddress address = new InetSocketAddress(host, port);
        bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .handler(new ClientInitializeHandler());
                //.......

主要注意LoggingHandler里的日志级别为DEBUG.该级别会在通信时打印出READ WRITE的详细信息

image.png

  • 服务端
@Component
public class NettyTcpServerBootstrap {
    @Value("${tcp.server.port}")
    int port;
    //......
    @PostConstruct
    //@PostConstruct是随着springboot启动时 调用该标识标注的方法.
    public void start() {
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                    //设置线程队列得到连接个数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    //设置保持活动连接状态
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    //设置NoDelay禁用Nagel,消息会立即发送出去,不用等到一定数量才发送出去
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    //这里的log是boss group的日志级别
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline p = ch.pipeline();
                            //这里log handler是跟随worker group的日志级别
                            p.addLast(new LoggingHandler(LogLevel.DEBUG));
                            //解码器
                            p.addLast(new MessageEncoder());
                            //编码器
                            p.addLast(new MessageDecoder());
                            //心跳包
//                            p.addLast(new IdleStateHandler(0, 0, 6, TimeUnit.SECONDS));
                            //处理器
                            p.addLast(nettyQueryHandler);
                        }
                    });
                    //......
  • 启动 本文在open jdk 11 && jetbrains IDEA 社区版 测试通过.如果大家有疑问或建议,请留言.