什么是编解码器?

  首先,我们回顾一下netty的组件设计:Netty的主要组件有Channel、EventLoop、ChannelFuture、ChannelHandler、ChannelPipe等。

ChannelHandler

  ChannelHandler充当了处理入站和出站数据的应用程序逻辑的容器。例如,实现ChannelInboundHandler接口(或ChannelInboundHandlerAdapter),你就可以接收入站事件和数据,这些数据随后会被你的应用程序的业务逻辑处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。你的业务逻辑通常写在一个或者多个ChannelInboundHandler中。ChannelOutboundHandler原理一样,只不过它是用来处理出站数据的。

ChannelPipeline

  ChannelPipeline提供了ChannelHandler链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler,并被这些Handler处理,反之则称为入站的

编码解码器

  当你通过Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java对象);如果是出站消息,它会被编码成字节。

  Netty提供了一系列实用的编码解码器,他们都实现了ChannelInboundHadnler或者ChannelOutcoundHandler接口。在这些类中,channelRead方法已经被重写了。以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由已知解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。

解码器

抽象基类ByteToMessageDecoder

由于你不可能知道远程节点是否会一次性发送一个完整的信息,tcp有可能出现粘包拆包的问题,这个类会对入站数据进行缓冲,直到它准备好被处理。

主要api有两个:

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<Object> out) throws Exception {
if (in.isReadable()) {
// Only call decode() if there is something left in the buffer to decode.
// See https://github.com/netty/netty/issues/4386
decodeRemovalReentryProtection(ctx, in, out);
}
}
}

decode方法:

  必须实现的方法,ByteBuf包含了传入数据,List用来添加解码后的消息。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该List,或者该ByteBuf中没有更多可读取的字节时为止。然后如果该List不会空,那么它的内容将会被传递给ChannelPipeline中的下一个ChannelInboundHandler。

decodeLast方法:

  当Channel的状态变成非活动时,这个方法将会被调用一次。

最简单的例子:

public class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() >= 4) {
out.add(in.readInt());
}
}
}

这个例子,每次入站从ByteBuf中读取4字节,将其解码为一个int,然后将它添加到下一个List中。当没有更多元素可以被添加到该List中时,它的内容将会被发送给下一个ChannelInboundHandler。int在被添加到List中时,会被自动装箱为Integer。在调用readInt()方法前必须验证所输入的ByteBuf是否具有足够的数据。

一个实用的例子:

public class MyDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 4) {
return;
}
     //在读取前标记readerIndex
in.markReaderIndex();
     //读取头部
int length = in.readInt();
if (in.readableBytes() < length) {
     //消息不完整,无法处理,将readerIndex复位
in.resetReaderIndex();
return;
}
out.add(in.readBytes(length).toString(CharsetUtil.UTF_8));
}
}

抽象类ReplayingDecoder

public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder

  ReplayingDecoder扩展了ByteToMessageDecoder类,使用这个类,我们不必调用readableBytes()方法。参数S指定了用户状态管理的类型,其中Void代表不需要状态管理。

以上代码可以简化为:

public class MySimpleDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//传入的ByteBuf是ReplayingDecoderByteBuf
//首先从入站ByteBuf中读取头部,得到消息体长度length,然后读取length个字节,
//并添加到解码消息的List中
out.add(in.readBytes(in.readInt()).toString(CharsetUtil.UTF_8));
}

如何实现的?

ReplayingDecoder在调用decode方法时,传入的是一个自定义的ByteBuf实现:

final class ReplayingDecoderByteBuf extends ByteBuf 

ReplayingDecoderByteBuf在读取数据前,会先检查是否有足够的字节可用,以readInt()为例:

final class ReplayingDecoderByteBuf extends ByteBuf {

    private static final Signal REPLAY = ReplayingDecoder.REPLAY;

    ......    

     @Override
public int readInt() {
checkReadableBytes(4);
return buffer.readInt();
} private void checkReadableBytes(int readableBytes) {
if (buffer.readableBytes() < readableBytes) {
throw REPLAY;
}
} ...... }

如果字节数量不够,会抛出一个Error(实际是一个Signal public final class Signal extends Error implements Constant<Signal> ),然后会在上层被捕获并处理,它会把ByteBuf中的ReadIndex恢复到读之前的位置,以供下次读取。当有更多数据可供读取时,该decode()方法将会被再次调用。最终结果和之前一样,从ByteBuf中提取的String将会被添加到List中。

虽然ReplayingDecoder使用方便,但它也有一些局限性:

1. 并不是所有的 ByteBuf 操作都被支持,如果调用了一个不被支持的方法,将会抛出一个 UnsupportedOperationException。

2. ReplayingDecoder 在某些情况下可能稍慢于 ByteToMessageDecoder,例如网络缓慢并且消息格式复杂时,消息被拆成了多个碎片,于是decode()方法会被多次调用反复地解析一个消息。

3. 你需要时刻注意decode()方法在同一个消息上可能被多次调用.。

错误用法:

一个简单的echo服务,客户端在连接建立时,向服务端发送消息(两个1)。服务端需要一次拿到两个Integer,并做处理。

EchoServerHandler

public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("msg from client: " + msg);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

EchoClientHandler

public class EchoClientHandler extends ChannelInboundHandlerAdapter {

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("sent to server: 11");
ctx.writeAndFlush(1); Thread.sleep(1000);
ctx.writeAndFlush(1);
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

解码器

public class MyReplayingDecoder extends ReplayingDecoder<Void> {

    private final Queue<Integer> values = new LinkedList<>();

    @Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
values.add(in.readInt());
values.add(in.readInt()); assert values.size() == 2; out.add(values.poll() + values.poll());
}
}

运行程序,就会发现断言失败。

我们通过在decode()方法中打印日志或者打断点的方式,可以看到,decode()方法是被调用了两次的,分别在服务端两次接受到消息的时候:

第一次调用时,由于缓冲区中只有四个字节,在第二句 values.add(in.readInt()) 中抛出了异常REPLAY,在ReplayingDecoder中被捕获,并复位ReadIndex。此时values.size() = 1。

第二次调用时,从头开始读取到两个Integer并放入values,因此values.size() = 3。

正确用法:

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//清空队列
values.clear();
values.add(in.readInt());
values.add(in.readInt()); assert values.size() == 2; out.add(values.poll() + values.poll());
}

如何提高ReplayingDecoder的性能?如上所说,使用ReplayingDecoder存在对一个消息多次重复解码的问题,我们可以通过Netty提供的状态控制来解决这个问题。

首先我们将消息结构设计为:header(4个字节,存放消息体长度),body(消息体)

根据消息的结构,我们定义两个状态:

public enum MyDecoderState {
/**
* 未读头部
*/
READ_LENGTH, /**
* 未读内容
*/
READ_CONTENT;
}

EchoClientHandler

public class EchoClientHandler extends ChannelInboundHandlerAdapter {

    @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println("sent to server: msg" + i);
ctx.writeAndFlush("msg" + i);
}
} @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

EchoServerHandler

public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("msg from client: " +
((ByteBuf) msg).toString(CharsetUtil.UTF_8));
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

解码器

public class IntegerHeaderFrameDecoder extends ReplayingDecoder<MyDecoderState> {

    private int length;

    public IntegerHeaderFrameDecoder() {
// Set the initial state.
super(MyDecoderState.READ_LENGTH);
} @Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { switch (state()) {
case READ_LENGTH:
length = in.readInt();
checkpoint(MyDecoderState.READ_CONTENT);
case READ_CONTENT:
ByteBuf frame = in.readBytes(length);
checkpoint(MyDecoderState.READ_LENGTH);
out.add(frame);
break;
default:
throw new Error("Shouldn't reach here.");
}
}
}

编码器

public class MyEncoder extends MessageToByteEncoder<String> {
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
byte[] b = msg.getBytes();
int length = b.length; //write length of msg
out.writeInt(length); //write msg
out.writeBytes(b);
}
}

当头部被成功读取到时,我们调用 checkpoint(MyDecoderState.READ_CONTENT) 设置状态为“未读消息”,相当于设置一个标志位,如果在后续读取时抛出异常,那么readIndex会被复位到上一次你调用checkpoint()方法的地方。下一次接收到消息,再次调用decode()方法时,就能够从checkpoint处开始读取,避免了又从头开始读。

更多解码器:

LineBasedFrameDecoder

这个类在Netty内部也有使用,它使用行尾控制字符(\n或者\r\n)作为分隔符来解析数据。

DelimiterBasedFrameDecoder

使用自定义的特殊字符作为消息的分隔符。

HttpObjectDecoder

一个HTTP数据的解码器。

这些解码器也非常实用,下次更新关于这些解码器的原理和详细使用。

更多详细内容参见《netty in action》 或者netty源码的英文注释。

netty解码器详解(小白也能看懂!)的更多相关文章

  1. 搭建分布式事务组件 seata 的Server 端和Client 端详解(小白都能看懂)

    一,server 端的存储模式为:Server 端 存 储 模 式 (store-mode) 支 持 三 种 : file: ( 默 认 ) 单 机 模 式 , 全 局 事 务 会 话 信 息 内 存 ...

  2. Oracle Statspack报告中各项指标含义详解~~学习性能必看!!!

    Oracle Statspack报告中各项指标含义详解~~学习性能必看!!! Data Buffer Hit Ratio#<#90# 数据块在数据缓冲区中的命中率,通常应该在90%以上,否则考虑 ...

  3. 图文并茂VLAN详解,让你看一遍就理解VLAN

    一.为什么需要VLAN 1.1.什么是VLAN? VLAN(Virtual LAN),翻译成中文是“虚拟局域网”.LAN可以是由少数几台家用计算机构成的网络,也可以是数以百计的计算机构成的企业网络.V ...

  4. 小白也能看懂的插件化DroidPlugin原理(二)-- 反射机制和Hook入门

    前言:在上一篇博文<小白也能看懂的插件化DroidPlugin原理(一)-- 动态代理>中详细介绍了 DroidPlugin 原理中涉及到的动态代理模式,看完上篇博文后你就会发现原来动态代 ...

  5. 小白也能看懂的插件化DroidPlugin原理(三)-- 如何拦截startActivity方法

    前言:在前两篇文章中分别介绍了动态代理.反射机制和Hook机制,如果对这些还不太了解的童鞋建议先去参考一下前两篇文章.经过了前面两篇文章的铺垫,终于可以玩点真刀实弹的了,本篇将会通过 Hook 掉 s ...

  6. 小白也能看懂的Redis教学基础篇——朋友面试被Skiplist跳跃表拦住了

    各位看官大大们,双节快乐 !!! 这是本系列博客的第二篇,主要讲的是Redis基础数据结构中ZSet(有序集合)底层实现之一的Skiplist跳跃表. 不知道那些是Redis基础数据结构的看官们,可以 ...

  7. 【vscode高级玩家】Visual Studio Code❤️安装教程(最新版🎉教程小白也能看懂!)

    目录 如果您在浏览过程中发现文章内容有误,请点此链接查看该文章的完整纯净版 下载 Linux Mac OS 安装 运行安装程序 同意使用协议 选择附加任务 准备安装 开始安装 安装完成 如果您在浏览过 ...

  8. 小白也能看懂的Redis教学基础篇——做一个时间窗限流就是这么简单

    不知道ZSet(有序集合)的看官们,可以翻阅我的上一篇文章: 小白也能看懂的REDIS教学基础篇--朋友面试被SKIPLIST跳跃表拦住了 书接上回,话说我朋友小A童鞋,终于面世通过加入了一家公司.这 ...

  9. 【最短路径Floyd算法详解推导过程】看完这篇,你还能不懂Floyd算法?还不会?

    简介 Floyd-Warshall算法(Floyd-Warshall algorithm),是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似.该算法名称以 ...

随机推荐

  1. 创建作业(JOB)

    在SQL Server日常需求处理中,会遇到定时执行或统计数据的需求,这时我们可以通过作业(JOB)来处理,从而通过代理的方式来实现数据的自动处理.一下为SQL Server中创建作业的脚本,供大家参 ...

  2. 20175312 2018-2019-2 《Java程序设计》第8周学习总结

    20175312 2018-2019-2 <Java程序设计>第8周学习总结 教材学习内容总结 已依照蓝墨云班课的要求完成了第十章的学习,主要的学习渠道是PPT,和书的课后习题. 总结如下 ...

  3. jQuery拼接HTML标签元素

    1. append & appendTo 的功能均为:在被选元素结尾(仍在元素内部)插入指定内容,但是内容和选择器的位置不同 (1) append()方法: //在id为element元素内部 ...

  4. 《Clean Code》阅读笔记

    Chapter 2  命名 命名要表现意图 避免歧义和误导,增强区分 命名可读性:便于发音,增强印象,便于交流 命名可查性:增强区分,便于搜索 类和对象的命名:名词或名词短语 方法的命名:动词或动词短 ...

  5. 爬虫学习笔记(1)-- 利用Python从网页抓取数据

    最近想从一个网站上下载资源,懒得一个个的点击下载了,想写一个爬虫把程序全部下载下来,在这里做一个简单的记录 Python的基础语法在这里就不多做叙述了,黑马程序员上有一个基础的视频教学,可以跟着学习一 ...

  6. leecode第二百一十七题(存在重复元素)

    class Solution { public: bool containsDuplicate(vector<int>& nums) { set<int> s; for ...

  7. Jquery封装的Ajax

    $.get方法 语法: $.get(url,data,function(e){ //e就是服务器返回的数据 },dataType); 四个参数: url: 请求的服务器地址 data: 发送给服务器的 ...

  8. python多线程采集

    import requests import json import threading Default_Header = { #具体请求头自己去弄 } _session=requests.sessi ...

  9. vuex 改变状态值得命名问题

    今天在做vuex的状态的时候 发现了个奇葩的问题,后面解决了,在改变状态的值得时候 传值的名称 不要和定义的状态的名称值相同,要不然会报错,如图所示 也就是password的名称不能相同,要不监测不到 ...

  10. Linux三种网络连接模式

    https://www.cnblogs.com/linjiaxin/p/6476480.html 三种模式的区别:https://www.cnblogs.com/itxiaok/p/10358055. ...