Netty源码分析之自定义编解码器
在日常的网络开发当中,协议解析都是必须的工作内容,Netty中虽然内置了基于长度、分隔符的编解码器,但在大部分场景中我们使用的都是自定义协议,所以Netty提供了 MessageToByteEncoder<I> 与 ByteToMessageDecoder 两个抽象类,通过继承重写其中的encode与decode方法实现私有协议的编解码。这篇文章我们就对Netty中的自定义编解码器进行实践与分析。
一、编解码器的使用
下面是MessageToByteEncoder与ByteToMessageDecoder使用的简单示例,其中不涉及具体的协议编解码。
创建一个sever端服务
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final CodecHandler codecHandler = new CodecHandler();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
//添加编解码handler
p.addLast(new MessagePacketDecoder(),new MessagePacketEncoder());
//添加自定义handler
p.addLast(codecHandler);
}
}); // Start the server.
ChannelFuture f = b.bind(PORT).sync();
继承MessageToByteEncoder并重写encode方法,实现编码功能
public class MessagePacketEncoder extends MessageToByteEncoder<byte[]> {
@Override
protected void encode(ChannelHandlerContext ctx, byte[] bytes, ByteBuf out) throws Exception {
//进行具体的编码处理 这里对字节数组进行打印
System.out.println("编码器收到数据:"+BytesUtils.toHexString(bytes));
//写入并传送数据
out.writeBytes(bytes);
}
}
继承ByteToMessageDecoder 并重写decode方法,实现解码功能
public class MessagePacketDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out){
try {
if (buffer.readableBytes() > 0) {
// 待处理的消息包
byte[] bytesReady = new byte[buffer.readableBytes()];
buffer.readBytes(bytesReady);
//进行具体的解码处理
System.out.println("解码器收到数据:"+ByteUtils.toHexString(bytesReady));
//这里不做过多处理直接把收到的消息放入链表中,并向后传递
out.add(bytesReady);
}
}catch(Exception ex) {
}
}
}
实现自定义的消息处理handler,到这里其实你拿到的已经是编解码后的数据
public class CodecHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("CodecHandler收到数据:"+ByteUtils.toHexString((byte[])msg));
byte[] sendBytes = new byte[] {0x7E,0x01,0x02,0x7e};
ctx.write(sendBytes);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
运行一个客户端模拟发送字节0x01,0x02,看一下输出的执行结果
解码器收到数据:0102
CodecHandler收到数据:0102
编码器收到数据:7E01027E
根据输出的结果可以看到消息的入站与出站会按照pipeline中自定义的顺序传递,同时通过重写encode与decode方法实现我们需要的具体协议编解码操作。
二、源码分析
通过上面的例子可以看到MessageToByteEncoder<I>与ByteToMessageDecoder分别继承了ChannelInboundHandlerAdapter与ChannelOutboundHandlerAdapter,所以它们也是channelHandler的具体实现,并在创建sever时被添加到pipeline中, 同时为了方便我们使用,netty在这两个抽象类中内置与封装了一些其操作;消息的出站和入站会分别触发write与channelRead事件方法,所以上面例子中我们重写的encode与decode方法,也都是在父类的write与channelRead方法中被调用,下面我们就别从这两个方法入手,对整个编解码的流程进行梳理与分析。
1、MessageToByteEncoder
编码需要操作的是出站数据,所以在MessageToByteEncoder的write方法中会调用我们重写的encode具体实现, 把我们内部定义的消息实体编码为最终要发送的字节流数据发送出去。
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = null;
try {
if (acceptOutboundMessage(msg)) {//判断传入的msg与你定义的类型是否一致
@SuppressWarnings("unchecked")
I cast = (I) msg;//转为你定义的消息类型
buf = allocateBuffer(ctx, cast, preferDirect);//包装成一个ByteBuf
try {
encode(ctx, cast, buf);//传入声明的ByteBuf,执行具体编码操作
} finally {
/**
* 如果你定义的类型就是ByteBuf 这里可以帮助你释放资源,不需要在自己释放
* 如果你定义的消息类型中包含ByteBuf,这里是没有作用,需要你自己主动释放
*/
ReferenceCountUtil.release(cast);//释放你传入的资源
} //发送buf
if (buf.isReadable()) {
ctx.write(buf, promise);
} else {
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER, promise);
}
buf = null;
} else {
//类型不一致的话,就直接发送不再执行encode方法,所以这里要注意如果你传递的消息与泛型类型不一致,其实是不会执行的
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
if (buf != null) {
buf.release();//释放资源
}
}
}
MessageToByteEncoder的write方法要实现的功能还是比较简单的,就是把你传入的数据类型进行转换和发送;这里有两点需要注意:
- 一般情况下,需要通过重写encode方法把定义的泛型类型转换为ByteBuf类型, write方法内部自动帮你执行传递或发送操作;
- 代码中虽然有通过ReferenceCountUtil.release(cast)释放你定义的类型资源,但如果定义的消息类中包含ByteBuf对象,仍需要主动释放该对象资源;
2、ByteToMessageDecoder
从命名上就可以看出ByteToMessageDecoder解码器的作用是把字节流数据编码转换为我们需要的数据格式
作为入站事件,解码操作的入口自然是channelRead方法
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {//如果消息是bytebuff
CodecOutputList out = CodecOutputList.newInstance();//实例化一个链表
try {
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) {
cumulation = data;
} else {
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
callDecode(ctx, cumulation, out);//开始解码
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
if (cumulation != null && !cumulation.isReadable()) {//不为空且没有可读数据,释放资源
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
// We did enough reads already try to discard some bytes so we not risk to see a OOME.
// See https://github.com/netty/netty/issues/4275
numReads = 0;
discardSomeReadBytes();
} int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);//向下传递消息
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
callDecode方法内部通过while循环的方式对ByteBuf数据进行解码,直到其中没有可读数据
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {//判断ByteBuf是还有可读数据
int outSize = out.size();//获取记录链表大小
if (outSize > 0) {//判断链表中是否已经有数据
fireChannelRead(ctx, out, outSize);//如果有数据继续向下传递
out.clear();//清空链表
// Check if this handler was removed before continuing with decoding.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See:
// - https://github.com/netty/netty/issues/4635
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
int oldInputLength = in.readableBytes();
decodeRemovalReentryProtection(ctx, in, out);//开始调用decode方法
// Check if this handler was removed before continuing the loop.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See https://github.com/netty/netty/issues/1664
if (ctx.isRemoved()) {
break;
}
//这里如果链表为空且bytebuf没有可读数据,就跳出循环
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes()) {
break;
} else {//有可读数据继续读取
continue;
}
}
if (oldInputLength == in.readableBytes()) {//beytebuf没有读取,但却进行了解码
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
if (isSingleDecode()) {//是否设置了每条入站数据只解码一次,默认false
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Exception cause) {
throw new DecoderException(cause);
}
}
decodeRemovalReentryProtection方法内部会调用我们重写的decode解码实现
final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
decodeState = STATE_CALLING_CHILD_DECODE;//标记状态
try {
decode(ctx, in, out);//调用我们重写的decode解码实现
} finally {
boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
decodeState = STATE_INIT;
if (removePending) {//这里判断标记,防止handlerRemoved事件与解码操作冲突
handlerRemoved(ctx);
}
}
}
channelRead方法中接受到数据经过一系列逻辑处理,最终会调用我们重写的decode方法实现具体的解码功能;在decode方法中我们只需要ByteBuf类型的数据解析为我们需要的数据格式直接放入 List<Object> out链表中即可,ByteToMessageDecoder会自动帮你向下传递消息。
三、总结
通过上面的讲解,我们可以对Netty中内置自定义编解码器MessageToByteEncoder与ByteToMessageDecoder有一定的了解,其实它们本质上是Netty封装的一组专门用于自定义编解码的channelHandler实现类。在实际开发当中基于这两个抽象类的实现非常具有实用性,所以在这里稍作分析, 其中如有不足与不正确的地方还望指出与海涵。
关注微信公众号,查看更多技术文章。

转载说明:未经授权不得转载,授权后务必注明来源(注明:来源于公众号:架构空间, 作者:大凡)
Netty源码分析之自定义编解码器的更多相关文章
- Netty源码分析第4章(pipeline)---->第2节: handler的添加
Netty源码分析第四章: pipeline 第二节: Handler的添加 添加handler, 我们以用户代码为例进行剖析: .childHandler(new ChannelInitialize ...
- Netty源码分析第6章(解码器)---->第1节: ByteToMessageDecoder
Netty源码分析第六章: 解码器 概述: 在我们上一个章节遗留过一个问题, 就是如果Server在读取客户端的数据的时候, 如果一次读取不完整, 就触发channelRead事件, 那么Netty是 ...
- netty源码分析之揭开reactor线程的面纱(二)
如果你对netty的reactor线程不了解,建议先看下上一篇文章netty源码分析之揭开reactor线程的面纱(一),这里再把reactor中的三个步骤的图贴一下 reactor线程 我们已经了解 ...
- Netty源码分析(前言, 概述及目录)
Netty源码分析(完整版) 前言 前段时间公司准备改造redis的客户端, 原生的客户端是阻塞式链接, 并且链接池初始化的链接数并不高, 高并发场景会有获取不到连接的尴尬, 所以考虑了用netty长 ...
- Netty源码分析第6章(解码器)---->第4节: 分隔符解码器
Netty源码分析第六章: 解码器 第四节: 分隔符解码器 基于分隔符解码器DelimiterBasedFrameDecoder, 是按照指定分隔符进行解码的解码器, 通过分隔符, 可以将二进制流拆分 ...
- Netty源码分析 (三)----- 服务端启动源码分析
本文接着前两篇文章来讲,主要讲服务端类剩下的部分,我们还是来先看看服务端的代码 /** * Created by chenhao on 2019/9/4. */ public final class ...
- Netty源码分析 (七)----- read过程 源码分析
在上一篇文章中,我们分析了processSelectedKey这个方法中的accept过程,本文将分析一下work线程中的read过程. private static void processSele ...
- Netty源码分析之NioEventLoop(三)—NioEventLoop的执行
前面两篇文章Netty源码分析之NioEventLoop(一)—NioEventLoop的创建与Netty源码分析之NioEventLoop(二)—NioEventLoop的启动中我们对NioEven ...
- Netty 源码分析——ChannelPipeline
Netty 源码分析--ChannelPipeline 通过前面的两章我们分析了客户端和服务端的流程代码,其中在初始化 Channel 的时候一定会看到一个 ChannelPipeline.所以在 N ...
随机推荐
- java内部类简单用法
package innerClass; /** * 特点 * 1:增强封装性,通过把内部类隐藏在外部类的里面,使得其他类不能访问外部类. * 2:增强可维护性. * 3:内部类可以访问外部的成员. * ...
- Java实现 LeetCode 784 字母大小写全排列(DFS)
784. 字母大小写全排列 给定一个字符串S,通过将字符串S中的每个字母转变大小写,我们可以获得一个新的字符串.返回所有可能得到的字符串集合. 示例: 输入: S = "a1b2" ...
- Java实现 LeetCode 743 网络延迟时间(Dijkstra经典例题)
743. 网络延迟时间 有 N 个网络节点,标记为 1 到 N. 给定一个列表 times,表示信号经过有向边的传递时间. times[i] = (u, v, w),其中 u 是源节点,v 是目标节点 ...
- Java实现蓝桥杯模拟树的叶结点数量
问题描述 一棵包含有2019个结点的树,最多包含多少个叶结点? 答案提交 这是一道结果填空的题,你只需要算出结果后提交即可.本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分. ...
- Java实现蓝桥杯打印图形
标题:打印图形 如下的程序会在控制台绘制分形图(就是整体与局部自相似的图形). 当n=1,2,3的时候,输出如下: 请仔细分析程序,并填写划线部分缺少的代码. n=1时: o ooo o n=2时: ...
- Python 网络爬虫基本概念篇
爬虫的概念 网络爬虫(又称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本.这是百度百科对爬虫的定义,其实,说简单点,爬虫 ...
- lambda操作DataTable进阶版
using System;using System.Collections.Generic;using System.Data;using System.Linq;using System.Text; ...
- k8s学习-安全
4.8.安全 4.8.1.概念 一些内容可参考4.6.2.Secret的内容 说明 Kubernetes 作为一个分布式集群的管理工具,保证集群的安全性是其一个重要的任务.API Server 是集群 ...
- MyBatis运行流程及入门第一个程序
1. mybatis是什么? MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并 ...
- 学而思Java开发岗位面试
去学而思培优面试了. 有四道笔试题,后面会整理做法. 1.给一个文件夹,用递归的方式统计这个目录及其子目录不同文件类型的个数. 如,输出:jpg:几个文件,txt:几个文件... 2.不适用加减乘除, ...