Netty学习七:编解码之自定义通信协议

一、自定义通信协议

通信协议:通信双方事先商量好的暗语。在TCP网络编程中,发送方和接收方的数据包格式都是二进制,发送方先将对象转化为二进制流发送给接收方,接收方获取二进制流后根据协议解析成对象。

当前通用的协议包括:HTTP、HTTPS、JSON-RPC、FTP、IMAP、Protobuf等。考虑到通信协议兼容性好、易于维护,在满足业务场景和性能需求时推荐采用通用协议,通信协议不满足要求时可以自定义通信协议。自定义通信协议的好处在于:

  • 极致性能:通用协议考虑了很多兼容性的问题,性能有所损失。
  • 扩展性:自定义协议更好扩展,更好满足业务需求。
  • 安全性:通信协议是公开的,很多漏洞已经被黑客攻破。对于自定义协议,黑客需要先破解协议内容。

一个完备的通信协议需要具备以下基本要素:

  • 魔数:通信的一个暗号,通常采用固定的几个字节表示。用于防止任何人随便向服务器的端口上发送数据。接收方接收到数据后会先解析出魔书,如果魔书和约定的不匹配,认为是非法数据,直接关闭连接或者采取其他措施以增强系统的安全防护
  • 协议版本号:随着业务需求的变化,协议可能需要对结构或者字段进行改动,不同版本的协议对应的解析方法也不同
  • 序列化算法:表示数据发送方如何将请求对象转化为二进制流,以及如何再将二进制流转为对象
  • 报文类型:不同的业务场景中,报文可能存在不同的类型。如,RPC框架中有请求、响应、心跳等类型的报文;IM即时通信中有登录、创建群聊、发送消息、接收消息、退出群聊等类型的报文
  • 长度域字段:代表请求数据的长度,接收方根据该字段获取完整的报文
  • 请求数据:序列化后的二进制流
  • 状态:表示请求是否正常。一般由调用方设置。如:一次RPC调用失败,状态字段可以被服务提供方设置为异常状态
  • 保留字段:为了应对协议升级的可能性,预留的若干字段

以下是一个较为通用的协议示例:

+---------------------------------------------------------------+
| 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte  |
+---------------------------------------------------------------+
| 状态 1byte |        保留字段 4byte     |      数据长度 4byte     | 
+---------------------------------------------------------------+
|                   数据内容 (长度不定)                          |
+---------------------------------------------------------------+

二、Netty中自定义通信协议

Netty提供了丰富的编解码抽象类,用于我们更为方便地基于这些抽象类扩展实现自定义通信协议。

Netty编码类抽象类:

  • MessageToByteEncoder类:将消息对象编码为二进制流
  • MessageToMessageEncoder类:将一种消息类型编码为另一种消息类型

Netty解码类抽象类:

  • ByteToMessageDecoder/ReplyingDecoder类:将二进制流解码为消息对象
  • MessageToMessageDecoder类:将一种消息类型编码为另一种消息类型

Netty中的编解码器分为一次编解码和二次编解码。以解码为例,一次解码器用于解决TCP拆包/粘包问题,解析得到字节数据。如果需要对解析后的字节数据做对象转换,需要使用二次解码器。同理,编码器是相反过程。

  • 一次编解码器:MessageToByteEncoder、ByteToMessageDecoder/ReplyingDecoder
  • 二次编解码器:MessageToMessageEncoder、MessageToMessageDecoder
抽象编码类
Drawing 0.png

抽象编码类是ChannelOutboundHandler的抽象实现,处理的是出站数据

MessageToByteEncoder类
MessageToByteEncoder重写了ChannelOutboundHandler的write()方法,该方法内的encode()抽象方法用于数据编码,仅需要继承MessageToByteEncoder类并实现encode()即可自定义编码逻辑。

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ByteBuf buf = null;
    try {
        if (acceptOutboundMessage(msg)) { // 1. 消息类型是否匹配
            @SuppressWarnings("unchecked")
            I cast = (I) msg;
            buf = allocateBuffer(ctx, cast, preferDirect); // 2. 分配 ByteBuf 资源
            try {
                encode(ctx, cast, buf); // 3. 执行 encode 方法完成数据编码
            } finally {
                ReferenceCountUtil.release(cast);
            }
            if (buf.isReadable()) {
                ctx.write(buf, promise); // 4. 向后传递写事件
            } else {
                buf.release();
                ctx.write(Unpooled.EMPTY_BUFFER, promise);
            }
            buf = null;
        } else {
            ctx.write(msg, promise);
        }
    } catch (EncoderException e) {
        throw e;
    } catch (Throwable e) {
        throw new EncoderException(e);
    } finally {
        if (buf != null) {
            buf.release();
        }
    }
}

write()方法的主要逻辑如下:

  • acceptOutboundMessage判断是否有匹配的消息类型,如果匹配需要执行编码,如果不匹配直接传递给下一个ChannelOutboundHandler
  • 分配ByteBuf资源,默认使用堆外内存
  • 调用子类的encode()方法进行编码,编码成功,会通过调用ReferenceCountUtil.release(cast)自动释放
  • 如果ByteBuf可读,说明成功编码得到数据,然后写入下一个ChannelOutboundHandler;否则释放ByteBuf资源,向下传递空的ByteBuf对象

编码器无需关注拆包/粘包问题,以下为一个自定义编码类:

public class StringToByteEncoder extends MessageToByteEncoder<String> {
        @Override
        protected void encode(ChannelHandlerContext channelHandlerContext, String data, ByteBuf byteBuf) throws Exception {
            byteBuf.writeBytes(data.getBytes());
        }
}

MessageToMessageEncoder类
与MessageToByteEncoder类似,同样可以继承MessageToMessageEncoder类并实现encode()方法实现自定义编码。但是MessageToMessageEncoder用于将一个格式的消息转换为另一个格式的消息,这里的第二个Message可以是任意一个对象,如果该对象是ByteBuf类型,MessageToMessageEncoder的实现逻辑基本上与MessageToByteEncoder一致

MessageToMessageEncoder的实现类包括:StringEncoder、LineEncoder、Base64Encoder等。以StringEncoder类为例,查看encode()方法:将CharSequence类型转换为ByteBuf类型,结合StringDecoder可以实现String类型的编解码。

@Override
protected void encode(ChannelHandlerContext ctx, CharSequence msg, List<Object> out) throws Exception {
    if (msg.length() == 0) {
        return;
    }
    out.add(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), charset));
}

抽象解码类
Drawing 1.png

抽象解码类是ChannelInboundHandler的抽象类实现,处理的是入站数据。解码器需要考虑拆包/粘包问题。由于接收方可能没有收到完整消息,解码框架需要对入站的数据做缓冲操作,直至收到完整的消息。

ByteToMessageDecoder类

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
    protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
    protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List&lt;Object&gt; out) throws Exception {
        if (in.isReadable()) {
            decodeRemovalReentryProtection(ctx, in, out);
        }
    }
}

ByteToMessageDecoder类中包含decode()抽象方法,可以通过继承ByteToMessageDecoder并实现该方法完成自定义解码。decode()在调用时需要传入ByteBuf以及用来添加解码后消息的List列表。由于拆包/粘包问题,ByteBuf中可能包含多个有效报文,或者不够一个完整的报文,因此Netty会重复会调用decode()方法,直到解码出新的完整报文可以添加到List中,或者ByteBuf中没有更多可读的数据为止。如果List不为空,将会传递给下一个ChannelInboundHandler。

ByteToMessageDecoder中还有一个decodeLast()方法,该方法会在Channel关闭后被调用一次,主要用于处理ByteBuf最后剩余的字节数据。Netty中decodeLast()方法默认只是简单调用一下decode()方法,有特殊也无需求也可以重写decodeLast()。

ByteToMessageDecoder有一个抽象子类ReplyingDecoder,他封装了缓冲区的管理,读取缓冲区时无需再对字节长度进行校验。因为如果没有足够长度的字节数据,ReplyingDecoder会终止解码操作。一般不推荐使用ReplyingDecoder类,因为其性能相较直接使用ByteToMessageDecoder要慢。

MessageToMessageDecoder类
该类的作用与ByteToMessageDecoder类似,都是将一种消息解码为另一种消息,但是第一个Message可以是任意一个对象,而且MessageToMessageDecoder不会对数据报文进行缓存。推荐先使用ByteToMessageDecoder解析TCP协议,解决拆包/粘包问题,然后用MessageToMessageDecoder做数据对象的转换。
Lark20201109-102121.png

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第1讲:学习的要义 第2讲:Netty宏观理解 第3讲:Netty课程大纲深度解读 第4讲:项目环境搭建与Gradle配置 第5讲:Netty执行流程分析与重要组件介绍 第6讲:Netty回调与Channel执行流程分析 第7讲:Netty的Socket编程详解 第8讲:Netty多客户端连接与通信 第9讲:Netty读写检测机制与长连接要素 第10讲:NettyWebSocket的支援 第11讲:Netty实现服务器端与客户端的长连接通信 第12讲:Google Protobuf详解 第13讲:定义Protobuf文件及消息详解 第14讲:Protobuf完整实例详解 第15讲:Protobuf集成Netty与多协议消息传递 第16讲:Protobuf多协议消息支援与工程最佳实践 第17讲:Protobuf使用最佳实践与Apache Thrift介绍 第18讲:Apache Thrift应用详解与实例剖析 第19讲:Apache Thrift原理与架构解析 第20讲:通过Apache Thrift实现Java与Python的RPC调用 第21讲:gRPC深入详解 第22讲:gRPC实践 第23讲:Gradle Wrapper在Gradle项目构建中的最佳实践 第24讲:gRPC整合Gradle与代码生成 第25讲:gRPC通信示例与JVM回调钩子 第26讲:gRPC服务器流式调用实现 第27讲:gRPC双向流式数据通信详解 第28讲:gRPC与Gradle流畅整合及问题解决的完整过程与思考 第29讲:Gradle插件问题解决方案与Nodejs环境搭建 第30讲:通过gRPC实现Java与Nodejs异构平台的RPC调用 第31讲:gRPC在Nodejs领域中的静态代码生成及与Java之间的RPC调用 第32讲:IO体系架构系统回顾与装饰模式的具体应用 第33讲:Java NIO深入详解与体系分析 第34讲:Buffer中各重要状态属性的含义与关系图解 第35讲:Java NIO核心类源码解读与分析 第36讲:文件通道用法详解 第37讲:Buffer深入详解 第38讲:NIO堆外内存与零拷贝深入讲解 第39讲:NIO中Scattering与Gathering深度解析 第40讲:Selector源码深入分析 第41讲:NIO网络访问模式分析 第42讲:NIO网络编程实例剖析 第43讲:NIO网络编程深度解析 第44讲:NIO网络客户端编写详解 第45讲:深入探索Java字符集编解码 第46讲:字符集编解码全方位解析 第47讲:Netty服务器与客户端编码模式回顾及源码分析准备 第48讲:Netty与NIO系统总结及NIO与Netty之间的关联关系分析 第49讲:零拷贝深入剖析及用户空间与内核空间切换方式 第50讲:零拷贝实例深度剖析 第51讲:NIO零拷贝彻底分析与Gather操作在零拷贝中的作用详解 第52讲:NioEventLoopGroup源码分析与线程数设定 第53讲:Netty对Executor的实现机制源码分析 第54讲:Netty服务端初始化过程与反射在其中的应用分析 第55讲:Netty提供的Future与ChannelFuture优势分析与源码讲解 第56讲:Netty服务器地址绑定底层源码分析 第57讲:Reactor模式透彻理解及其在Netty中的应用 第58讲:Reactor模式与Netty之间的关系详解 第59讲:Acceptor与Dispatcher角色分析 第60讲:Netty的自适应缓冲区分配策略与堆外内存创建方式 第61讲:Reactor模式5大角色彻底分析 第62讲:Reactor模式组件调用关系全景分析 第63讲:Reactor模式与Netty组件对比及Acceptor组件的作用分析 第64讲:Channel与ChannelPipeline关联关系及模式运用 第65讲:ChannelPipeline创建时机与高级拦截过滤器模式的运用 第66讲:Netty常量池实现及ChannelOption与Attribute作用分析 第67讲:Channel与ChannelHandler及ChannelHandlerContext之间的关系分析 第68讲:Netty核心四大组件关系与构建方式深度解读 第69讲:Netty初始化流程总结及Channel与ChannelHandlerContext作用域分析 第70讲:Channel注册流程深度解读 第71讲:Channel选择器工厂与轮询算法及注册底层实现 第72讲:Netty线程模型深度解读与架构设计原则 第73讲:Netty底层架构系统总结与应用实践 第74讲:Netty对于异步读写操作的架构思想与观察者模式的重要应用 第75讲:适配器模式与模板方法模式在入站处理器中的应用 第76讲:Netty项目开发过程中常见且重要事项分析 第77讲:Java NIO Buffer总结回顾与难点拓展 第78讲:Netty数据容器ByteBuf底层数据结构深度剖析 第79讲:Netty的ByteBuf底层实现大揭秘 第80讲:Netty复合缓冲区详解与3种缓冲区适用场景分析 第81讲:Netty引用计数的实现机制与自旋锁的使用技巧 第82讲:Netty引用计数原子更新揭秘与AtomicIntegerFieldUpdater深度剖析 第83讲:AtomicIntegerFieldUpdater实例演练与volatile关键字分析 第84讲:Netty引用计数注意事项与内存泄露检测方式 第85讲:Netty编解码器剖析与入站出站处理器详解 第86讲:Netty自定义编解码器与TCP粘包拆包问题 第87讲:Netty编解码器执行流程深入分析 第88讲:ReplayingDecoder源码分析与特性解读 第89讲:Netty常见且重要编解码器详解 第90讲:TCP粘包与拆包实例演示及分析 第91讲:Netty自定义协议与TCP粘包拆包问题解决之道 第92讲:精通并发与Netty课程总结与展望
93个netty高并发全面的教学视频下载,每个视频在400-700M,一到两个小时时长的视频,无机器码和解压密码,下载下来的就是MP4格式视频。点击即可观看学习。下载txt文档,里面有永久分享的连接。包括01_学习的要义;02_Netty宏观理解;03_Netty课程大纲深度解读;04_项目环境搭建与Gradle配置;05_Netty执行流程分析与重要组件介绍;06_Netty回调与Channel执行流程分析;07_Netty的Socket编程详解;08_Netty多客户端连接与通信,09_Netty读写检测机制与长连接要素,10_NettyWebSocket的支援;11_Netty实现服务器端与客户端的长连接通信;12_Google Protobuf详解;13_定义Protobuf文件及消息详解;14_Protobuf完整实例详解;15_Protobuf集成Netty与多协议消息传递;16_Protobuf多协议消息支援与工程最佳实践;17_Protobuf使用最佳实践与Apache Thrift介绍;18_Apache Thrift应用详解与实例剖析;19_Apache Thrift原理与架构解析;20_通过Apache Thrift实现Java与Python的RPC调用;21_gRPC深入详解 ;22_gRPC实践 ;23_Gradle Wrapper在Gradle项目构建中的最佳实践;24_gRPC整合Gradle与代码生成············82_Netty引用计数原子更新揭秘与AtomicIntegerFieldUpdater深度剖析;83_AtomicIntegerFieldUpdater实例演练与volatile关键字分析;84_Netty引用计数注意事项与内存泄露检测方式;85_Netty编解码器剖析与入站出站处理器详解;86_Netty自定义编解码器与TCP粘包拆包问题;87_Netty编解码器执行流程深入分析;88_ReplayingDecoder源码分析与特性解读;89_Netty常见且重要编解码器详解;90_TCP粘包与拆包实例演示及分析;91_Netty自定义协议与TCP粘包拆包问题解决之道;92_精通并发与Netty课程总结与展望
以下是一个简单的Netty自定义协议的示例代码,该协议由消息头和消息体组成,消息头包含消息的长度信息,消息体包含具体的消息内容。 1. 定义消息类 ```java public class MyMessage { private int length; private String content; // getter and setter methods } ``` 2. 编写编解码器 ```java public class MyMessageCodec extends MessageToByteEncoder<MyMessage> { @Override protected void encode(ChannelHandlerContext ctx, MyMessage msg, ByteBuf out) throws Exception { byte[] contentBytes = msg.getContent().getBytes(CharsetUtil.UTF_8); out.writeInt(contentBytes.length); out.writeBytes(contentBytes); } } public class MyMessageDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { if (in.readableBytes() < 4) { return; } in.markReaderIndex(); int length = in.readInt(); if (in.readableBytes() < length) { in.resetReaderIndex(); return; } byte[] contentBytes = new byte[length]; in.readBytes(contentBytes); String content = new String(contentBytes, CharsetUtil.UTF_8); MyMessage message = new MyMessage(); message.setLength(length); message.setContent(content); out.add(message); } } ``` 3. 编写服务端和客户端代码 服务端代码: ```java public class MyServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyMessageDecoder()); pipeline.addLast(new MyServerHandler()); pipeline.addLast(new MyMessageCodec()); } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture f = b.bind(8888).sync(); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } } public class MyServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { MyMessage message = (MyMessage) msg; System.out.println("Server receive message: " + message.getContent()); message.setContent("Hello, " + message.getContent() + "!"); ctx.writeAndFlush(message); } } ``` 客户端代码: ```java public class MyClient { public static void main(String[] args) throws Exception { EventLoopGroup workerGroup = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(workerGroup) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyMessageCodec()); pipeline.addLast(new MyMessageDecoder()); pipeline.addLast(new MyClientHandler()); } }); ChannelFuture f = b.connect("localhost", 8888).sync(); for (int i = 0; i < 10; i++) { MyMessage message = new MyMessage(); message.setContent("world-" + i); f.channel().writeAndFlush(message); Thread.sleep(1000); } f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); } } } public class MyClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { MyMessage message = (MyMessage) msg; System.out.println("Client receive message: " + message.getContent()); } } ``` 以上代码演示了如何使用Netty实现自定义协议,其中MyMessageCodec和MyMessageDecoder负责编解码,MyServer和MyServerHandler负责服务端逻辑,MyClient和MyClientHandler负责客户端逻辑。在本示例中,自定义协议包含消息头和消息体,消息头包含消息长度信息,消息体包含具体的消息内容。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值