java网络编程实战 - 要想全面玩转Netty实战,你需要了解或深入理解掌握这些

前言

怎样才能说自己懂Netty ? 如何将Netty了解到一定的深度 ? 如何全面玩转Netty的实战 ? 不妨沿着下面的路径,对Netty进行一个全面的认识和思考!


第一节、网络编程NIO的Reactor模式

第二节、Netty和选择Netty的理由

第三节、Netty入门中三个基本特性

第四节、核心概念和机制 - EventLoop、EventLoopGroup

第五节、主要组件ChannelHandler、ChannelHandlerContext和ChnnelPipeline

第六节、Netty支持的网络通讯传输模式

第七节、操作系统层ChannelOption详解

第八节、特有的缓冲封装ByteBuf详解

第九节、Netty中的重要机制引用计数

第十节、Netty如何解决粘包/半包问题

第十一节、Netty重量级组件编解码器

第十二节、Netty内置及引入外部序列和反序列化部件

第十三节、如何独立进行ChannelHandler的单元测试


全面认识Netty开始

第一节、网络编程NIO的Reactor模式

        Reactor反应器中的 "反应" 即倒置、控制反转的意思。具体事件处理程序不调用反应器,而向反应器注册一个事件处理器,表示自己对某些事件感兴趣,有事件来了,具体事件处理程序通过事件处理器对某个指定的事件发生做出反应;这种控制逆转又称为“好莱坞法则”(通俗点就是:你不要主动找我,有事我来找你)。

单线程Reactor模式

  服务器端的Reactor是一个线程对象,该线程会启动事件循环,并使用Selector(选择器)来实现IO的多路复用。注册一个Acceptor事件处理器到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)

客户端向服务器端发起一个连接请求,Reactor监听到了该ACCEPT事件的发生并将该ACCEPT事件派发给相应的Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将该连接所关注的READ事件以及对应的READ事件处理器注册到Reactor中,这样一来Reactor就会监听该连接的READ事件了;

Reactor监听到有读或者写事件发生时,将相关的事件派发给对应的处理器进行处理。比如,读处理器会通过SocketChannelread()方法读取数据,此时read()操作可以直接读取到数据,而不会堵塞与等待可读的数据到来;

每当处理完所有就绪的感兴趣的I/O事件后,Reactor线程会再次执行select()阻塞等待新的事件就绪并将其分派给对应处理器进行处理;

注意,Reactor的单线程模式的单线程主要是针对于I/O操作而言,也就是所有的I/Oaccept()read()write()以及connect()操作都在一个线程上完成的;

但在目前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上进行处理了,这可能会大大延迟I/O请求的响应。所以我们应该将非I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。


多线程Reactor模式

Reactor线程池中的每一Reactor线程都会有自己的Selector、线程和分发的事件循环逻辑。

mainReactor可以只有一个,但subReactor一般会有多个。mainReactor线程主要负责接收客户端的连接请求,然后将接收到的SocketChannel传递给subReactor,由subReactor来完成和客户端的通信。交互流程如下:

注册一个Acceptor事件处理器到mainReactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样mainReactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。启动mainReactor的事件循环;

客户端向服务器端发起一个连接请求,mainReactor监听到了该ACCEPT事件并将该ACCEPT事件派发给Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将这个SocketChannel传递给subReactor线程池;

subReactor线程池分配一个subReactor线程给这个SocketChannel,即,将SocketChannel关注的READ事件以及对应的READ事件处理器注册到subReactor线程中。当然你也注册WRITE事件以及WRITE事件处理器到subReactor线程中以完成I/O写操作。Reactor线程池中的每一Reactor线程都会有自己的Selector、线程和分发的循环逻辑;

当有I/O事件就绪时,相关的subReactor就将事件派发给响应的处理器处理。注意,这里subReactor线程只负责完成I/Oread()操作,在读取到数据后将业务逻辑的处理放入到线程池中完成,若完成业务逻辑后需要返回数据给客户端,则相关的I/Owrite操作还是会被提交回subReactor线程来完成;

注意,所以的I/O操作(包括,I/Oaccept()read()write()以及connect()操作)依旧还是在Reactor线程(mainReactor线程 subReactor线程)中完成的。Thread Pool(线程池)仅用来处理非I/O操作的逻辑;

Reactor线程模式将“接受客户端的连接请求”和“与该客户端的通信”分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,它不负责与客户端的通信,而是将建立好的连接转交给subReactor线程来完成与客户端的通信,这样一来就不会因为read()数据量太大而导致后面的客户端连接请求得不到即时处理的情况。并且多Reactor线程模式在海量的客户端并发请求的情况下,还可以通过实现subReactor线程池来将海量的连接分发给多个subReactor线程,在多核的操作系统中这能大大提升应用的负载和吞吐量,Netty服务端使用了多Reactor线程模式。


第二节、Netty和选择Netty的理由

  Netty是由jbss提供的一个java开源框架, 它提供异步的、事件驱动网络编程框架和工具;支撑开发快速、高性能、高稳定性的端到端(服务端网络端)程序。我们常用的Netty大版本是Netty4,而最新的Netty5严格地说,还是alpha版本,还有一些问题且并没经过实战。

选择Netty的理由

1、虽然JAVA NIO框架提供了 多路复用IO的支持,但是并没有提供上层“信息格式”的良好封装。例如前两者并没有提供针对 Protocol BufferJSON这些信息格式的封装,但是Netty框架提供了这些数据格式封装(基于责任链模式的编码和解码功能);

2NIO的类库和API相当复杂,使用它来开发,需要非常熟练地掌握SelectorByteBufferServerSocketChannelSocketChannel等,需要很多额外的编程技能来辅助使用NIO,例如,因为NIO涉及了Reactor线程模型,所以必须必须对多线程和网络编程非常熟悉才能写出高质量的NIO程序;

3、要编写一个可靠的、易维护的、高性能的NIO服务器应用。除了框架本身要兼容实现各类操作系统的实现外。更重要的是它应该还要处理很多上层特有服务,例如:客户端的权限、还有上面提到的信息格式封装、简单的数据读取,断连重连,半包读写,心跳等等,这些Netty框架都提供了响应的支持;

4JAVA NIO框架存在一个poll/epoll bugSelector doesnt block on Selector.select(timeout),不能block意味着CPU的使用率会变成100%(这是底层JNI的问题,上层要处理这个异常实际上也好办)。当然这个bug只有在Linux内核上才能重现;

这个问题在JDK 1.7版本中还没有被完全解决,但是Netty已经将这个bug进行了处理。

这个Bug与操作系统机制有关系的,JDK虽然仅仅是一个兼容各个操作系统平台的软件,但在JDK5JDK6最初的版本中(严格意义上来将,JDK部分版本都是),这个问题并没有解决,而将这个帽子抛给了操作系统方,这也就是这个bug最终一直到2013年才最终修复的原因(JDK7JDK8之间)


第三节、Netty入门中三个基本特性

事件和Channel

    Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。由入站数据或者相关的状态更改而触发的事件包括:连接被激活、非激活事件、数据读取、用户事件、异常事件等;出站事件是未来将会触发的某个动作的操作结果,包括:打开或关闭到远程节点的连接、将数据写到或者冲刷到套接字等;

 事件被分发给ChannelHandler 类中每个覆盖的方法中, 这些方法类似于回调函数。Netty 提供了大量开箱即用的ChannelHandler 实现,包括用于各种协议(如HTTP SSL/TLS)的ChannelHandler。


Channel 接口

基本的I/O 操作(bind()connect()read()write())依赖于底层网络传输所提供的原语。在基于Java 的网络编程中,其基本的构造是类SocketNetty Channel 接口所提供的API,被用于所有的I/O 操作。大大地降低了直接使用Socket 类的复杂性。此外,Channel 也是拥有许多预定义的、专门化实现的广泛类层次结构的根。

由于Channel 是独一无二的,所以为了保证顺序将Channel 声明为java.lang.Comparable 的一个子接口。因此,如果两个不同的Channel 实例都返回了相同的散列码,那么AbstractChannel 中的compareTo()方法的实现将会抛出一个Error

Channel 的生命周期状态

ChannelUnregistered Channel 已经被创建,但还未注册到EventLoop

ChannelRegistered Channel 已经被注册到了EventLoop

ChannelActive Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了

ChannelInactive Channel 没有连接到远程节点

当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给ChannelPipeline 中的ChannelHandler,其可以随后对它们做出响应。


第四节、核心概念和机制 - EventLoop、EventLoopGroup

image.png


在内部,当提交任务到如果当前)调用线程正是支撑EventLoop 的线程,那么所提交的代码块将会被(直接)执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件。

服务于Channel I/O 和事件的EventLoop 则包含在EventLoopGroup 中。

异步传输实现只使用了少量的EventLoop(以及和它们相关联的Thread),而且在当前的线程模型中,它们可能会被多个Channel 所共享。这使得可以通过尽可能少量的Thread 来支撑大量的Channel,而不是每个Channel 分配一个ThreadEventLoopGroup 负责为每个新创建的Channel 分配一个EventLoop。在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,并且相同的EventLoop可能会被分配给多个Channel

一旦一个Channel 被分配给一个EventLoop,它将在它的整个生命周期中都使用这个EventLoop(以及相关联的Thread)。请牢记这一点,因为它可以使你从担忧你的ChannelHandler 实现中的线程安全和同步问题中解脱出来。

image.png

需要注意EventLoop 的分配方式对ThreadLocal 的使用的影响。因为一个EventLoop 通常会被用于支撑多个Channel,所以对于所有相关联的Channel 来说,ThreadLocal 都将是一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。然而,在一些无状态的上下文中,它仍然可以被用于在多个Channel 之间共享一些重度的或者代价昂贵的对象,甚至是事件。


第五节、主要组件ChannelHandler、ChannelHandlerContext和ChnnelPipeline

ChannelHandler接口

Netty 的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 的方法是由网络事件触发的。

Netty 定义了下面两个重要的ChannelHandler 子接口:ChannelInboundHandler——处理入站数据以及各种状态变化;ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作。

ChannelInboundHandler接口的生命周期中重要的方法:

channelActive: 当Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪

channelReadComplete: Channel上的一个读操作完成时被调用

channelRead: 当从Channel 读取数据时被调用

userEventTriggered: 当ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用

ChannelOutboundHandler接口的生命周期中重要的方法:

bind:当请求将Channel 绑定到本地地址时被调用

connect:当请求将Channel 连接到远程节点时被调用

close:当请求关闭Channel 时被调用

read: 当请求从Channel 读取更多的数据时被调用

flush: 当请求通过Channel 将入队数据冲刷到远程节点时被调用

write:当请求通过Channel 将数据写到远程节点时被调用

ChannelHandlerContext常用API

bind: 绑定到给定的SocketAddress,并返回ChannelFuture

channel: 返回绑定到这个实例的Channel

close: 关闭Channel,并返回ChannelFuture

connect: 连接给定的SocketAddress,并返回ChannelFuture 

fireChannelActive: 触发对下一个ChannelInboundHandler 上的channelActive()方法(已连接)的调用 

fireChannelRead: 触发对下一个ChannelInboundHandler 上的channelRead()方法(已接收的消息)的调用

fireChannelReadComplete: 触发对下一个ChannelInboundHandler 上的channelReadComplete()方法的调用

fireExceptionCaught: 触发对下一个ChannelInboundHandler 上的fireExceptionCaught(Throwable)方法的调用

fireUserEventTriggered: 触发对下一个ChannelInboundHandler 上的fireUserEventTriggered(Object evt)方法的调用

handler: 返回绑定到这个实例的ChannelHandler

pipeline: 返回这个实例所关联的ChannelPipeline

read 将数据从Channel读取到第一个入站缓冲区;如果读取成功则触发一个channelRead事件,并(在最后一个消息被读取完成后)通知ChannelInboundHandler 的channelReadComplete

ChnnelPipeline接口

    Channel 被创建时,它将会被自动地分配一个新的ChannelPipeline。这项关联是永久性的;Channel 既不能附加另外一个ChannelPipeline,也不能分离其当前的。在Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。

image.png

Channel 被创建时,它将会被自动地分配一个新的ChannelPipeline。这项关联是永久性的;Channel 既不能附加另外一个ChannelPipeline,也不能分离其当前的。在Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。

使得事件流经ChannelPipeline ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。

入站和出站ChannelHandler 可以被安装到同一个ChannelPipeline中。如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline 的头部开始流动,最终,数据将会到达ChannelPipeline 的尾端,届时,所有处理就都结束了。

数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,这里显示为Socket。通常情况下,这将触发一个写操作。

如果将两个类别的ChannelHandler都混合添加到同一个ChannelPipeline 中会发生什么。虽然ChannelInboundHandle ChannelOutboundHandle 都扩展自ChannelHandler,但是Netty 能区分ChannelInboundHandler实现和ChannelOutboundHandler 实现,并确保数据只会在具有相同定向类型的两个ChannelHandler 之间传递。

ChannelPipeline上的重要的方法:addFirst、addBefore、addAfter、addLast。


第六节、Netty支持的网络通讯传输模式

NIO: 使用java.nio.channels 包作为基础——基于选择器的方式;

Epoll:  JNI 驱动的 epoll()和非阻塞 IO。这个传输支持只有在Linux 上可用的多种特性,如SO_REUSEPORT,比NIO 传输更快,而且是完全非阻塞的。将NioEventLoopGroup替换为      EpollEventLoopGroup 并且将NioServerSocketChannel.class 替换为EpollServerSocketChannel.class 即可

OIO: 使用java.net 包作为基础——使用阻塞流

Local:可以在VM 内部通过管道进行通信的本地传输

Embedded: 传输允许使用ChannelHandler 而又不需要一个真正的基于网络的传输。在测试ChannelHandler 实现时非常有用


第七节、操作系统层ChannelOption详解

ChannelOption 的各种属性在套接字选项中都有对应。

ChannelOption.SO_BACKLOG

ChannelOption.SO_BACKLOG 对应的是 tcp/ip 协议 listen 函数中的 backlog 参数,函数 listen(int socketfd,int backlog)用来初始化服务端可连接队列, 服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多 个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参 数指定了队列的大小。

ChannelOption.SO_REUSEADDR 

ChanneOption.SO_REUSEADDR 对应于套接字选项中的 SO_REUSEADDR,这个参数表示允 许重复使用本地地址和端口, 比如,某个服务器进程占用了 TCP 的 80 端口进行监听,此时再次监听该端口就会返回 错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使 用,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程 使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置 SO_REUSEADDR 就无法正常使用该端口。 

ChannelOption.SO_KEEPALIVE 

Channeloption.SO_KEEPALIVE 参数对应于套接字选项中的 SO_KEEPALIVE,该参数用于设 置 TCP 连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数 据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP 会自动发送一 个活动探测数据报文。 

ChannelOption.SO_SNDBUF 和 ChannelOption.SO_RCVBUF 

ChannelOption.SO_SNDBUF 参数对应于套接字选项中的 SO_SNDBUF, ChannelOption.SO_RCVBUF 参数对应于套接字选项中的 SO_RCVBUF 这两个参数用于操作接 收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程 序读取成功,发送缓冲区用于保存发送数据,直到发送成功。 

ChannelOption.SO_LINGER 

ChannelOption.SO_LINGER 参数对应于套接字选项中的 SO_LINGER,Linux 内核默认的处理 方式是当用户调用 close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不 一定保证会发生剩余的数据,造成了数据的不确定性,使用 SO_LINGER 可以阻塞 close()的调 用时间,直到数据完全发送 。

ChannelOption.TCP_NODELAY 

ChannelOption.TCP_NODELAY 参数对应于套接字选项中的 TCP_NODELAY,该参数的使用 与 Nagle 算法有关,Nagle 算法是将小的数据包组装为更大的帧然后进行发送,而不是输入 一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发 送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使 用 Nagle 算法,使用于小数据即时传输,于 TCP_NODELAY 相对应的是 TCP_CORK,该选项是 需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。


第八节、特有的缓冲封装ByteBuf详解

ByteBuf是Netty在ByteBuffer基础上做的二次封装和扩展。它的API 具有以下优点: 

1. 它可以被用户自定义的缓冲区类型扩展; 

2. 通过内置的复合缓冲区类型实现了透明的零拷贝;

3. 容量可以按需增长(类似于 JDK 的 StringBuilder); 

4. 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法; 

5. 读和写使用了不同的索引; 

6. 支持方法的链式调用; 

7. 支持引用计数; 支持池化;

ByteBuf 维护了两个不同的索引,名称以 read 或者 write 开头的 ByteBuf 方法,将会推进其对应的索引,而名称以 set 或者 get 开头的操作则不会。

image.png


第九节、Netty中的重要机制引用计数

      当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它要负责显式地释放 与池化的 ByteBuf 实例相关的内存。Netty 为此提供了一个实用方法 ReferenceCountUtil.release() Netty 将使用 WARN 级别的日志消息记录未释放的资源,使得可以非常简单地在代码 中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用 SimpleChannelInboundHandler,SimpleChannelInboundHandler 会自动释放资源。

 1、对于入站请求,Netty 的 EventLoo 在处理 Channel 的读操作时进行分配 ByteBuf,对 于这类 ByteBuf,需要我们自行进行释放,有三种方式,或者使用 SimpleChannelInboundHandler,或者在重写 channelRead()方法使用 ReferenceCountUtil.release()或者使用 ctx.fireChannelRead 继续向后传递; 

2、对于出站请求,不管 ByteBuf 是否由我们的业务创建的,当调用了 write 或者 writeAndFlush 方法后,Netty 会自动替我们释放,不需要我们业务代码自行释放。


第十节、Netty如何解决粘包/半包问题

粘包半包

image.png

假设客户端分别发送了两个数据包 D1 和 D2 给服务端,由于服务端一次读取到的字节 数是不确定的,故可能存在以下 4 种情况。 

1. 服务端分两次读取到了两个独立的数据包,分别是 D1 和 D2,没有粘包和拆包; 

2. 服务端一次接收到了两个数据包,D1 和 D2 粘合在一起,被称为 TCP 粘包; 

3. 服务端分两次读取到了两个数据包,第一次读取到了完整的 D1 包和 D2 包的部分 内容,第二次读取到了 D2 包的剩余内容,这被称为 TCP 拆包; 

4. 服务端分两次读取到了两个数据包,第一次读取到了 D1 包的部分内容 D1_1,第 二次读取到了 D1 包的剩余内容 D1_2 和 D2 包的整包。 

    如果此时服务端 TCP 接收滑窗非常小,而数据包 D1 和 D2 比较大,很有可能会发生第 五种可能,即服务端分多次才能将 D1 和 D2 包接收完全,期间发生多次拆包;

粘包半包发生的原因分析

        由于 TCP 协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会 维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发 往服务器,但是如果发送的网络数据包太小,那么他本身会启用 Nagle 算法(可配置是否启 用)对较小的数据包进行合并(基于此,TCP 的网络延迟要 UDP 的高些)然后再发送(超 时或者包大小足够)。

        那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪 些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲 区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个 数据包的情况,造成粘包现象。

粘包半包解决办法

由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案, 可以归纳如下:

1. 在包尾增加分割符,比如回车换行符进行分割,例如 FTP 协议; 参见 cn.enjoyedu.nettybasic.splicing.linebase 和 cn.enjoyedu.nettybasic.splicing.delimiter 下的代码;

2. 消息定长,例如每个报文的大小为固定长度 200 字节,如果不够,空位补空格; 参见 cn.enjoyedu.nettybasic.splicing.fixed 下的代码 ;

3. 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度) 的字段,通常设计思路为消息头的第一个字段使用 int32 来表示消息的总长度LengthFieldBasedFrameDecoder。


第十一节、Netty重量级组件编解码器

将字节解码为消息

抽象类 ByteToMessageDecoder 

将字节解码为消息(或者另一个字节序列)是一项如此常见的任务,以至于 Netty 为它 提供了一个抽象的基类:ByteToMessageDecoder。由于你不可能知道远程节点是否会一次性 地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理。

decode(ChannelHandlerContext ctx,ByteBuf in,List out)

这是你必须实现的唯一抽象方法。decode()方法被调用时将会传入一个包含了传入数据 的 ByteBuf,以及一个用来添加解码消息的 List。

将一种消息类型解码为另一种

decode(ChannelHandlerContext ctx,I msg,List out)

对于每个需要被解码为另一种格式的入站消息来说,该方法都将会被调用。解码消息随 后会被传递给 ChannelPipeline 中的下一个 ChannelInboundHandler

TooLongFrameException

    由于 Netty 是一个异步框架,所以需要在字节可以解码之前在内存中缓冲它们。因此, 不能让解码器缓冲大量的数据以至于耗尽可用的内存。为了解除这个常见的顾虑,Netty 提 供了 TooLongFrameException 类,其将由解码器在帧超出指定的大小限制时抛出。


第十二节、Netty内置及引入外部序列和反序列化部件

序列化的问题

Java 序列化的目的主要有两个:

1.网络传输 

2.对象持久化 

    当选行远程跨迸程服务调用时,需要把被传输的 Java 对象编码为字节数组或者 ByteBuffer 对象。而当远程服务读取到 ByteBuffer 对象或者字节数组时,需要将其解码为发 送时的 Java 对象。这被称为 Java 对象编解码技术。 

    Java 序列化仅仅是 Java 编解码技术的一种,由于它的种种缺陷,衍生出了多种编解码 技术和框架;


Java 序列化的缺点

1. 无法跨语言 

对于跨进程的服务调用,服务提供者可能会使用 C 十+或者其他语言开发,当我们需要 和异构语言进程交互时 Java 序列化就难以胜任。由于 Java 序列化技术是 Java 语言内部的私 有协议,其他语言并不支持,对于用户来说它完全是黑盒。对于 Java 序列化后的字节数组, 别的语言无法进行反序列化,这就严重阻碍了它的应用。 

2. 序列化后的码流太大 

通过很多验证代码证明:序列化后的码流确实很大;

3. 序列化性能太低 

无论是序列化后的码流大小,还是序列化的性能,JDK 默认的序列化机制表现得都很差。 

因此,我们边常不会选择 Java 序列化作为远程跨节点调用的编解码框架。

Netty内置的对象序列和反序列化组件是:Protocol Buffers

外部业界比较高效地序列和反序列化的部件有:

protostuff 、 kryo 、 fast-serialization 、msgpack-databird、 hessian 等,尤其是前三甲;


第十三节、如何独立进行ChannelHandler的单元测试

EmbeddedChannel---ChannelHandler独立单元测试工具

        将入站数据或者出站数据写入到 EmbeddedChannel 中,然后检查是否有任何东西到达 了 ChannelPipeline 的尾端。以这种方式,你便可以确定消息是否已经被编码或者被解码过 了,以及是否触发了任何的 ChannelHandler 动作。

writeInbound(Object... msgs)

将入站消息写到 EmbeddedChannel 中。如果可以通过 readInbound()方法从 EmbeddedChannel 中读取数据,则返回 true。

readInbound()

从 EmbeddedChannel 中读取一个入站消息。任何返回的东西都穿越了整个 ChannelPipeline。如果没有任何可供读取的,则返回 null。

writeOutbound(Object... msgs)

将出站消息写到 EmbeddedChannel 中。如果现在可以通过 readOutbound()方法从 EmbeddedChannel 中读取到什么东西,则返回 true。

readOutbound()

从 EmbeddedChannel 中读取一个出站消息。任何返回的东西都穿越了整个 ChannelPipeline。如果没有任何可供读取的,则返回 null。

finish()

将 EmbeddedChannel 标记为完成,并且如果有可被读取的入站数据或者出站数 据,则返回 true。这个方法还将会调用 EmbeddedChannel 上的 close()方法。

入站数据由 ChannelInboundHandler 处理,代表从远程节点读取的数据。出站数据由 ChannelOutboundHandler 处理,代表将要写到远程节点的数据。

使用 writeOutbound()方法将消息写到 Channel 中,并通过 ChannelPipeline 沿着出站的 方向传递。随后,你可以使用 readOutbound()方法来读取已被处理过的消息,以确定结果是 否和预期一样。 类似地,对于入站数据,你需要使用 writeInbound()和 readInbound()方法。

image.png



总结

我们应该尽量对以上Netty相关知识进行理解,尤其是EventLoop(Group)、ChannelHandler、ChannelHandlerContext、ChannelPipeline、ByteBuf、Netty编解码器、序列化和反序列化等做深入理解。那么有了这些知识储备后,我们就可以基于一些业务来设计实现一个Netty端到端的网络IO实战了。下次我们将运用以上知识或机制,逐步深入给大家展示Netty的实战相关课题。


猜你喜欢

转载自blog.51cto.com/14815984/2506523