Netty的组件和设计

Netty组件

Netty的组件和设计_第1张图片

这些关系是:

  • 一个EventLoopGroup包含一个或者多个EventLoop
  • 一个EventLoop在它的生命周期内只和一个Thread绑定;
  • 所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理;
  • 一个Channel在它的生命周期内只注册于一个EventLoop
  • 一个EventLoop可能会被分配给一个或多个Channel

一个给定Channel的I/O操作都是由相同的Thread执行的,消除了对于同步的需要

Channel、EventLoop和ChannelFuture

Channel

Netty中所有的I/O操作都是异步的,我们需要在方法执行之后的某个时间点确定其结果,对此Netty提供了ChannelFuture接口,他的addListener()方法注册了一个ChannelFutureListener,可以实现监听,以便在某个操作完成时(无论是否成功)得到通知。

Channel的生命周期
状  态 描  述
ChannelUnregistered Channel已经被创建,但还未注册到EventLoop
ChannelRegistered Channel已经被注册到了EventLoop
ChannelActive Channel处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
ChannelInactive Channel没有连接到远程节点

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

Netty的组件和设计_第2张图片

EventLoop

ChannelFuture

ChannelHandler和ChannelPipeline

ChannelHandler

从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。

ChannelHandler的生命周期

下表列出了ChannelHandler接口定义的生命周期操作,在ChannelHandler被添加到ChannelPipeline中或者被从ChannelPipeline中移除时会调用这些操作。这些方法中的每一个都接受一个ChannelHandlerContext参数。

类  型 描  述
handlerAdded 当把ChannelHandler添加到ChannelPipeline中时被调用
handlerRemoved 当从ChannelPipeline中移除ChannelHandler时被调用
exceptionCaught 当处理过程中在ChannelPipeline中有错误产生时被调用

Netty定义了下面两个重要的ChannelHandler子接口:

  • ChannelInboundHandler——处理入站数据以及各种状态变化;
  • ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作。
ChannelInboundHandler接口

以下ChannelInboundHandler的生命周期方法。这些方法将会在数据被接收时或者与其对应的Channel状态发生改变时被调用。

类  型 描  述
channelRegistered Channel已经注册到它的EventLoop并且能够处理I/O时被调用
channelUnregistered Channel从它的EventLoop注销并且无法处理任何I/O时被调用
channelActive Channel处于活动状态时被调用;Channel已经连接/绑定并且已经就绪
channelInactive Channel离开活动状态并且不再连接它的远程节点时被调用
channelReadComplete Channel上的一个读操作完成时被调用[1]
channelRead 当从Channel读取数据时被调用
ChannelWritability - Changed Channel的可写状态发生改变时被调用。用户可以确保写操作不会完成得太快(以避免发生OutOfMemoryError)或者可以在Channel变为再次可写时恢复写入。可以通过调用ChannelisWritable()方法来检测Channel的可写性。与可写性相关的阈值可以通过Channel.config(). setWriteHighWaterMark()Channel.config().setWriteLowWater- Mark()方法来设置
userEventTriggered ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用,因为一个POJO被传经了ChannelPipeline
ChannelOutboundHandler接口

出站操作和数据将由ChannelOutboundHandler处理。他的一个强大功能是可以按需推迟操作或事件,这使得我们可以通过一些复杂的方法来处理请求。例如,如果写入到远程节点的操作被暂停了,那么你可以先推迟flush操作,并在之后合适的时候继续。

下表展示了所有由ChannelOutboundHandler本身所定义的方法(忽略了那些从ChannelHandler继承的方法)

类  型 描  述
bind(ChannelHandlerContext, SocketAddress,ChannelPromise) 当请求将Channel绑定到本地地址时被调用
connect(ChannelHandlerContext, SocketAddress,SocketAddress,ChannelPromise) 当请求将Channel连接到远程节点时被调用
disconnect(ChannelHandlerContext, ChannelPromise) 当请求将Channel从远程节点断开时被调用
close(ChannelHandlerContext,ChannelPromise) 当请求关闭Channel时被调用
deregister(ChannelHandlerContext, ChannelPromise) 当请求将Channel从它的EventLoop注销时被调用
read(ChannelHandlerContext) 当请求从Channel读取更多的数据时被调用
flush(ChannelHandlerContext) 当请求通过Channel将入队数据冲刷到远程节点时被调用
write(ChannelHandlerContext,Object,``ChannelPromise) 当请求通过Channel将数据写到远程节点时被调用

ChannelPromise与ChannelFuture

ChannelOutboundHandler中的大部分方法都需要一个ChannelPromise参数,以便在操作完成时得到通知。ChannelPromiseChannelFuture的一个子类,其定义了一些可写的方法,如setSuccess()setFailure(),从而使ChannelFuture不可变。

ChannelHandler适配器

可以使用ChannelInboundHandlerAdapterChannelOutboundHandlerAdapter类作为自己的ChannelHandler的起始点。这两个适配器分别提供了ChannelInboundHandlerChannelOutboundHandler的基本实现。

Netty的组件和设计_第3张图片

ChannelHandlerAdapter还提供了实用方法isSharable()。如果其对应的实现被标注为Sharable,那么这个方法将返回true,表示它可以被添加到多个ChannelPipeline中。

资源管理

Netty使用引用计数来处理池化的ByteBuf,每当通过调用ChannelInboundHandler.channelRead()或者ChannelOutboundHandler.write()方法来处理数据时,都需要确保没有任何的资源泄漏。所以在完全使用完某个ByteBuf后,调整其引用计数是很重要的。

为了诊断潜在的资源泄漏问题,Netty提供了ResourceLeakDetector这个类。它将对应用程序的缓冲区分配做大约1%的采样来检测内存泄露,相关的开销是非常小的。如果检测到了内存泄露,将会产生类似于下面的日志消息:

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable
advanced leak reporting to find out where the leak occurred. To enable
advanced leak reporting, specify the JVM option
'-Dio.netty.leakDetectionLevel=ADVANCED' or call
ResourceLeakDetector.setLevel().

Netty目前定义了4种泄漏检测级别:

级  别 描  述
DISABLED 禁用泄漏检测。只有在详尽的测试之后才应设置为这个值
SIMPLE 使用1%的默认采样率检测并报告任何发现的泄露。这是默认级别,适合绝大部分的情况
ADVANCED 使用默认的采样率,报告所发现的任何的泄露以及对应的消息被访问的位置
PARANOID 类似于ADVANCED,但是其将会对每次(对消息的)访问都进行采样。这对性能将会有很大的影响,应该只在调试阶段使用

泄露检测级别可以通过以下方式定义:

java -Dio.netty.leakDetectionLevel=ADVANCED

带着该JVM选项重新启动应用程序,将看到最近被泄漏的缓冲区被访问的位置。下面是一个典型的由单元测试产生的泄漏报告:

Running io.netty.handler.codec.xml.XmlFrameDecoderTest
15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK:
   ByteBuf.release() was not called before it's garbage-collected.
Recent access records: 1
#1: io.netty.buffer.AdvancedLeakAwareByteBuf.toString(
  AdvancedLeakAwareByteBuf.java:697)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(
  XmlFrameDecoderTest.java:157)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(
  XmlFrameDecoderTest.java:133)
...
如何防止资源泄漏

入站:当某个ChannelInboundHandler的实现重写channelRead()方法时,它必须显式地释放与池化的ByteBuf实例相关的内存。Netty为此提供了一个实用方法ReferenceCountUtil.release()

@Sharable
//继承自ChannelInboundHandlerAdapter
public class DiscardHandler extends ChannelInboundHandlerAdapter { 
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        //丢弃已接收的消息
        ReferenceCountUtil.release(msg); 
    }
}

Netty将使用WARN级别的日志消息记录未释放的资源,可以简单地发现代码中的违规实例。但是以这种方式管理资源很繁琐。因此Netty提供了一个更加简单的方式:使用SimpleChannelInboundHandler

@Sharable
//继承SimpleChannelInboundHandler
public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> {
    @Override
    public void channelRead0(ChannelHandlerContext ctx, Object msg) {
        //不需要任何显式的资源释放
        // No need to do anything special
    }
}

由于SimpleChannelInboundHandler会自动释放资源,所以所有的引用最后都将会失效。

出站:如果使用write()操作处理并丢弃了一个消息,那么你也应该负责释放它。

@Sharable
public class DiscardOutboundHandler
    extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx,
        Object msg, ChannelPromise promise) {
        //通过使用 ReferenceCountUtil.realse(...)方法释放资源
        ReferenceCountUtil.release(msg);
        //通知 ChannelPromise数据已经被处理了
        promise.setSuccess();
    }
}

对于ChannelOutboundHandler,不仅要释放资源,还要通知ChannelPromise。否则可能会出现ChannelFutureListener收不到某个消息已经被处理了的通知的情况。

总之,如果一个消息被消费或者丢弃了,并且没有传递给ChannelPipeline中的下一个ChannelOutboundHandler,那么用户就有责任调用ReferenceCountUtil.release()。如果消息到达了实际的传输层,那么当它被写入时或者Channel关闭时,都将被自动释放。

ChannelPipeline

ChannelPipelineChannelHandler链提供了容器,并定义了用于在该链上传播入站和出站事件流的API,当一个Channel被创建时,它会被自动地分配到它专属的ChannelPipeline

ChannelHandler安装到ChannelPipeline中的过程如下所示:

  • 一个ChannelInitializer的实现被注册到了ServerBootstrap
  • ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在ChannelPipeline中安装一组自定义的ChannelHandler
  • ChannelInitializer将它自己从ChannelPipeline中移除。

根据事件的来源,事件将会被ChannelInboundHandler或者ChannelOutboundHandler处理。随后,通过调用ChannelHandlerContext实现,它将被转发给同一超类的下一个ChannelHandler

Netty的组件和设计_第4张图片

修改ChannelPipeline

ChannelHandler可以通过添加、删除、替换其他的ChannelHandler或者是将它自己从ChannelPipeline中移除等操作,来实时地修改ChannelPipeline的布局。

以下是ChannelPipeline上的相关方法,由ChannelHandler用来修改ChannelPipeline的布局

名  称 描  述
addFirstaddBeforeaddAfteraddLast 将一个ChannelHandler添加到ChannelPipeline
remove 将一个ChannelHandlerChannelPipeline中移除
replace ChannelPipeline中的一个ChannelHandler替换为另一个ChannelHandler
public class ModifyChannelPipeline {
    private static final ChannelPipeline CHANNEL_PIPELINE_FROM_SOMEWHERE = ...;

    public static void modifyPipeline() {
        ChannelPipeline pipeline = CHANNEL_PIPELINE_FROM_SOMEWHERE; // get reference to pipeline;
        //创建一个 FirstHandler 的实例
        FirstHandler firstHandler = new FirstHandler();
        //将该实例作为"handler1"添加到ChannelPipeline 中
        pipeline.addLast("handler1", firstHandler);
        //将一个 SecondHandler的实例作为"handler2"添加到 ChannelPipeline的第一个槽中。这意味着它将被放置在已有的"handler1"之前
        pipeline.addFirst("handler2", new SecondHandler());
        //将一个 ThirdHandler 的实例作为"handler3"添加到 ChannelPipeline 的最后一个槽中
        pipeline.addLast("handler3", new ThirdHandler());
        //...
        //通过名称移除"handler3"
        pipeline.remove("handler3");
        //通过引用移除FirstHandler(它是唯一的,所以不需要它的名称)
        pipeline.remove(firstHandler);
        //将 SecondHandler("handler2")替换为 FourthHandler:"handler4"
        pipeline.replace("handler2", "handler4", new FourthHandler());
    }

    private static final class FirstHandler
        extends ChannelHandlerAdapter {

    }

    private static final class SecondHandler
        extends ChannelHandlerAdapter {

    }

    private static final class ThirdHandler
        extends ChannelHandlerAdapter {

    }

    private static final class FourthHandler
        extends ChannelHandlerAdapter {

    }
}

通常ChannelPipeline中的每一个ChannelHandler都是通过它的EventLoop(I/O线程)来处理传递给它的事件。所以不要阻塞这个线程,因为这会对整体的I/O处理产生负面的影响。

但有时可能需要与那些使用阻塞API的遗留代码进行交互。对于这种情况,ChannelPipeline有一些接受一个EventExecutorGroupadd()方法。如果一个事件被传递给一个自定义的EventExecutorGroup,它将被这个EventExecutorGroup中的某个EventExecutor处理,从而从该Channel本身的EventLoop中移除。对于这种用例,Netty提供了一个叫DefaultEventExecutorGroup的默认实现。

除了这些操作,还有别的通过类型或者名称来访问ChannelHandler的方法。

名  称 描  述
get 通过类型或者名称返回ChannelHandler
context 返回和ChannelHandler绑定的ChannelHandlerContext
names 返回ChannelPipeline中所有ChannelHandler的名称
触发事件

下面列出了ChannelPipeline的入站操作,用于通知ChannelInboundHandlerChannelPipeline中所发生的事件。

方 法 名 称 描  述
fireChannelRegistered 调用ChannelPipeline中下一个ChannelInboundHandlerchannelRegistered(ChannelHandlerContext)方法
fireChannelUnregistered 调用ChannelPipeline中下一个ChannelInboundHandlerchannelUnregistered(ChannelHandlerContext)方法
fireChannelActive 调用ChannelPipeline中下一个ChannelInboundHandlerchannelActive(ChannelHandlerContext)方法
fireChannelInactive 调用ChannelPipeline中下一个ChannelInboundHandlerchannelInactive(ChannelHandlerContext)方法
fireExceptionCaught 调用ChannelPipeline中下一个ChannelInboundHandlerexceptionCaught(ChannelHandlerContext, Throwable)方法
fireUserEventTriggered 调用ChannelPipeline中下一个ChannelInboundHandleruserEventTriggered(ChannelHandlerContext, Object)方法
fireChannelRead 调用ChannelPipeline中下一个ChannelInboundHandlerchannelRead(ChannelHandlerContext, Object msg)方法
fireChannelReadComplete 调用ChannelPipeline中下一个ChannelInboundHandlerchannelReadComplete(ChannelHandlerContext)方法
fireChannelWritability Changed 调用ChannelPipeline中下一个ChannelInboundHandlerchannelWritabilityChanged(ChannelHandlerContext)方

在出站时,处理事件将会导致底层的Socket上发生一系列的动作。下面列出了ChannelPipeline API的出站操作:

方 法 名 称 描  述
bind Channel绑定到一个本地地址,这将调用ChannelPipeline中的下一个ChannelOutboundHandlerbind(ChannelHandlerContext, SocketAddress, ChannelPromise)方法
connect Channel连接到一个远程地址,这将调用ChannelPipeline中的下一个ChannelOutboundHandlerconnect(ChannelHandlerContext, Socket- Address, ChannelPromise)方法
disconnect Channel断开连接。这将调用ChannelPipeline中的下一个ChannelOutbound- Handlerdisconnect(ChannelHandlerContext, Channel Promise)方法
close Channel关闭。这将调用ChannelPipeline中的下一个ChannelOutbound- Handlerclose(ChannelHandlerContext, ChannelPromise)方法
deregister Channel从它先前所分配的EventExecutor(即EventLoop)中注销。这将调用ChannelPipeline中的下一个ChannelOutboundHandlerderegister (ChannelHandlerContext, ChannelPromise)方法
flush 冲刷Channel所有挂起的写入。这将调用ChannelPipeline中的下一个Channel- OutboundHandlerflush(ChannelHandlerContext)方法
write 将消息写入Channel。这将调用ChannelPipeline中的下一个Channel- OutboundHandlerwrite(ChannelHandlerContext, Object msg, Channel- Promise)方法。注意:这并不会将消息写入底层的Socket,而只会将它放入队列中。要将它写入Socket,需要调用flush()或者writeAndFlush()方法
writeAndFlush 这是一个先调用write()方法再接着调用flush()方法的便利方法
read 请求从Channel中读取更多的数据。这将调用ChannelPipeline中的下一个ChannelOutboundHandlerread(ChannelHandlerContext)方法

总结一下:

  • ChannelPipeline保存了与Channel相关联的ChannelHandler
  • 根据需要,可以通过添加或者删除ChannelHandler来动态地修改ChannelPipeline
  • ChannelPipeline有着丰富的API用以被调用,以响应入站和出站事件。

ChannelPipeline和ChannelHandler之间的关系

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

下面展示了一个典型的同时具有入站和出站ChannelHandlerChannelPipeline的布局,可以看出ChannelPipeline主要由一系列的ChannelHandler所组成的。从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,反之则称为入站的。

Netty的组件和设计_第5张图片

ChannelInboundHandler链为例,如果一个消息或者其他的入站事件被读取,那么它会从ChannelPipeline的头部开始流动,并被传递给第一个ChannelInboundHandler。最终,数据将会到达ChannelPipeline的尾端,届时,所有处理就都结束了;

数据的出站运动也是一样的,数据将从ChannelOutboundHandler链的尾端开始流动,直到它到达链的头部为止。

入站和出站ChannelHandler可以被安装到同一个ChannelPipeline中。虽然ChannelInboundHandleChannelOutboundHandle都扩展自ChannelHandler,但是Netty能区分ChannelInboundHandler实现和ChannelOutboundHandler实现,并确保数据只会在具有相同定向类型的两个ChannelHandler之间传递:在ChannelPipeline传播事件时,它会测试ChannelPipeline中的下一个ChannelHandler的类型是否和事件的运动方向相匹配。如果不匹配,ChannelPipeline将跳过该ChannelHandler并前进到下一个,直到它找到和该事件所期望的方向相匹配的为止。

ChannelPipeline相对论
从事件途经ChannelPipeline的角度来看,ChannelPipeline的头部和尾端取决于该事件是入站的还是出站的。然而Netty总是将ChannelPipeline的入站口(图6-3中的左侧)作为头部,而将出站口(该图的右侧)作为尾端。
当你完成了通过调用ChannelPipeline.add*()方法将入站处理器(ChannelInboundHandler)和出站处理器(ChannelOutboundHandler)混合添加到ChannelPipeline之后,每一个ChannelHandler从头部到尾端的顺序位置正如同我们方才所定义它们的一样。因此,如果你将图6-3中的处理器(ChannelHandler)从左到右进行编号,那么第一个被入站事件看到的ChannelHandler将是1,而第一个被出站事件看到的ChannelHandler将是5。

在Netty中,有两种发送消息的方式。你可以直接写到Channel中,也可以写到和Channel-Handler相关联的ChannelHandlerContext对象中。前一种方式将会导致消息从Channel-Pipeline的尾端开始流动,而后者将导致消息从ChannelPipeline中的下一个Channel- Handler开始流动。

Netty以适配器类的形式提供了大量默认的ChannelHandler实现,旨在简化应用程序处理逻辑的开发过程。ChannelPipeline中的每个ChannelHandler将负责把事件转发到链中的下一个ChannelHandler,这些适配器类(及它们的子类)将自动执行这个操作,所以你可以只重写那些你想要特殊处理的方法和事件。

下面这些是编写自定义ChannelHandler时经常会用到的适配器类:

  • ChannelHandlerAdapter
  • ChannelInboundHandlerAdapter
  • ChannelOutboundHandlerAdapter
  • ChannelDuplexHandler

ChannelHandlerContext接口

ChannelHandlerContext代表着ChannelHandlerChannelPipeline之间的关联,每当有ChannelHandler添加到ChannelPipeline中时,都会创建ChannelHandlerContext。他的主要功能是管理它关联的ChannelHandler和同ChannelPipeline中的其他ChannelHandler之间的交互。

ChannelHandlerContext有很多方法,其中一些方法也存在于ChannelChannelPipeline,但不同的是,如果调用Channel或者ChannelPipeline上的这些方法,它们将沿着整个ChannelPipeline传播,而调用位于ChannelHandlerContext上的相同方法,则将从其关联的ChannelHandler开始,并且只会传播给该ChannelPipeline中下一个能处理该事件的ChannelHandler

以下对ChannelHandlerContext API进行了总结:

方 法 名 称 描  述
alloc 返回和这个实例相关联的Channel所配置的ByteBufAllocator
bind 绑定到给定的SocketAddress,并返回ChannelFuture
channel 返回绑定到这个实例的Channel
close 关闭Channel,并返回ChannelFuture
connect 连接给定的SocketAddress,并返回ChannelFuture
deregister 从之前分配的EventExecutor注销,并返回ChannelFuture
disconnect 从远程节点断开,并返回ChannelFuture
executor 返回调度事件的EventExecutor
fireChannelActive 触发对下一个ChannelInboundHandler上的channelActive()方法(已连接)的调用
fireChannelInactive 触发对下一个ChannelInboundHandler上的channelInactive()方法(已关闭)的调用
fireChannelRead 触发对下一个ChannelInboundHandler上的channelRead()方法(已接收的消息)的调用
fireChannelReadComplete 触发对下一个ChannelInboundHandler上的channelReadComplete()方法的调用
fireChannelRegistered 触发对下一个ChannelInboundHandler上的fireChannelRegistered()方法的调用
fireChannelUnregistered 触发对下一个ChannelInboundHandler上的fireChannelUnregistered()方法的调用
fireChannelWritabilityChanged 触发对下一个ChannelInboundHandler上的fireChannelWritabilityChanged()方法的调用
fireExceptionCaught 触发对下一个ChannelInboundHandler上的fireExceptionCaught(Throwable)方法的调用
fireUserEventTriggered 触发对下一个ChannelInboundHandler上的fireUserEventTriggered(Object evt)方法的调用
handler 返回绑定到这个实例的ChannelHandler
isRemoved 如果所关联的ChannelHandler已经被从ChannelPipeline中移除则返回true
name 返回这个实例的唯一名称
pipeline 返回这个实例所关联的ChannelPipeline
read 将数据从Channel读取到第一个入站缓冲区;如果读取成功则触发[5]一个channelRead事件,并(在最后一个消息被读取完成后)通知ChannelInboundHandler的channelReadComplete (ChannelHandlerContext)方法
write 通过这个实例写入消息并经过ChannelPipeline
writeAndFlush 通过这个实例写入并冲刷消息并经过ChannelPipeline

当使用ChannelHandlerContext的API的时候,请牢记以下两点:

  • ChannelHandlerContextChannelHandler之间的绑定是永远不会改变的,所以缓存对它的引用是安全的;
  • 相对于其他类的同名方法,ChannelHandlerContext的方法产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。
使用ChannelHandlerContext

ChannelChannelPipelineChannelHandler以及ChannelHandlerContext之间的关系:

Netty的组件和设计_第6张图片

通过ChannelHandlerContext分别获取到ChannelChannelPipeline的引用,并调用他们的write()方法

//ChannelHandlerContext 访问 Channel
public static void writeViaChannel() {
    ChannelHandlerContext ctx = CHANNEL_HANDLER_CONTEXT_FROM_SOMEWHERE; //get reference form somewhere
    //获取到与 ChannelHandlerContext相关联的 Channel 的引用
    Channel channel = ctx.channel();
    //通过 Channel 写入缓冲区
    channel.write(Unpooled.copiedBuffer("Netty in Action",
                                        CharsetUtil.UTF_8));

}

//ChannelHandlerContext 访问 ChannelPipeline
public static void writeViaChannelPipeline() {
    ChannelHandlerContext ctx = CHANNEL_HANDLER_CONTEXT_FROM_SOMEWHERE; //get reference form somewhere
    //获取到与 ChannelHandlerContext相关联的 ChannelPipeline 的引用
    ChannelPipeline pipeline = ctx.pipeline(); //get reference form somewhere
    //通过 ChannelPipeline写入缓冲区
    pipeline.write(Unpooled.copiedBuffer("Netty in Action",
                                         CharsetUtil.UTF_8));

}

以上两种方式的事件流是一样的。但是要注意,虽然调用的ChannelChannelPipeline上的write()方法事件将在整个ChannelPipeline传播,但是在ChannelHandler的级别上,事件从一个ChannelHandler到下一个ChannelHandler的移动是由ChannelHandlerContext上的调用完成的。

Netty的组件和设计_第7张图片

为什么会想要从ChannelPipeline中的某个特定点开始传播事件呢?

  • 为了减少事件传经对它不感兴趣的ChannelHandler所带来的开销。
  • 为了避免将事件传经那些可能会对它感兴趣的ChannelHandler

要想从某个特定的ChannelHandler开始处理,必须获取到在ChannelPipeline中该ChannelHandler之前的ChannelHandler所关联的ChannelHandlerContext。这个ChannelHandlerContext将调用它关联的ChannelHandler之后的ChannelHandler,此时消息将从下一个ChannelHandler开始在ChannelPipeline中传播,绕过了前面的ChannelHandler

//调用 ChannelHandlerContext 的 write()方法
public static void writeViaChannelHandlerContext() {
    //获取到 ChannelHandlerContext 的引用
    ChannelHandlerContext ctx = CHANNEL_HANDLER_CONTEXT_FROM_SOMEWHERE; //get reference form somewhere;
    //write()方法将把缓冲区数据发送到下一个 ChannelHandler
    ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));
}

Netty的组件和设计_第8张图片

ChannelHandler和ChannelHandlerContext的高级用法

为了收集跨越多个Channel的统计信息,我们可以在多个ChannelPipeline中共享同一个ChannelHandler:一个ChannelHandler可以从属于多个ChannelPipeline(此时ChannelHandler必须要使用@Sharable注解标注),所以它可以绑定多个ChannelHandlerContext实例,为了安全地被用于多个并发的Channel,使用@Sharable注解时需要确保你的ChannelHandler是线程安全的。

//可共享的 ChannelHandler使用注解@Sharable标注
@Sharable
public class SharableHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        System.out.println("channel read message " + msg);
        //记录方法调用,并转发给下一个 ChannelHandler
        ctx.fireChannelRead(msg);
    }
}

以上实现的ChannelHandler可以正确加入到多个ChannelPipeline,因为它使用了注解@Sharable标注,并且也不持有任何的状态。

//使用注解@Sharable标注
@Sharable
public class UnsharableHandler extends ChannelInboundHandlerAdapter {
    private int count;
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        //将 count 字段的值加 1
        count++;
        //记录方法调用,并转发给下一个ChannelHandler
        System.out.println("inboundBufferUpdated(...) called the "
                + count + " time");
        ctx.fireChannelRead(msg);
    }
}

这段代码的问题在于它拥有状态,即用于跟踪方法调用次数的实例变量count。将该类的一个实例添加到ChannelPipeline,极有可能在它被多个并发的Channel访问时出现问题。

编码器和解码器

当你通过Netty发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式,通常是一个Java对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节

Netty为编码器和解码器提供了不同类型的抽象类。严格地说,其他的处理器也可以完成编码器和解码器的功能。但是,正如有用来简化ChannelHandler的创建的适配器类一样,所有由Netty提供的编码器/解码器适配器类都实现了ChannelOutboundHandler或者ChannelInboundHandler接口。

对于入站数据来说,channelRead方法被重写,对于每个从入站Channel读取的消息,这个方法都将会被调用。随后,它将调用由预置解码器所提供的decode()方法,并将已解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler

出站消息则相反:编码器将消息转换为字节,并将它们转发给下一个ChannelOutboundHandler

SimpleChannelInboundHandler

最常见的情况是我们的应用程序需要一个ChannelHandler来接收解码消息,并对该数据进行一些业务逻辑上的处理。要创建一个这样的ChannelHandler,只需要扩展基类SimpleChannelInboundHandler,其中T是你要处理的消息的Java类型。在这个ChannelHandler中,需要重写基类的一个或者多个方法,并且获取一个到ChannelHandlerContext的引用,这个引用将作为输入参数传递给ChannelHandler的所有方法。

SimpleChannelInboundHandler中,最重要的方法是channelRead0(ChannelHandlerContext,T)。除了要求不要阻塞当前的I/O线程之外,其具体实现完全取决于你。

引导(Bootstrap)

Netty的引导类为应用程序的网络层配置提供了容器。

  • 引导一个客户端:将一个进程连接到另一个运行在某个指定主机的指定端口上的进程。

  • 引导一个服务器:将一个进程绑定到某个指定的端口;

分别两种类型的引导:一种用于客户端(Bootstrap),而另一种用于服务器(ServerBootstrap)。无论你的应用程序使用哪种协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。

类  别 Bootstrap ServerBootstrap
网络编程中的作用 连接到远程主机和端口 绑定到一个本地端口
EventLoopGroup的数目 1 2

服务器需要两组不同的Channel。第一组只包含一个ServerChannel,代表服务器自身已绑定到某个本地端口的Socket。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel

Netty的组件和设计_第9张图片

ServerChannel相关联的EventLoopGroup将分配一个负责为传入连接请求创建ChannelEventLoop。一旦连接被接受,第二个EventLoopGroup就会给它的Channel分配一个EventLoop

你可能感兴趣的:(Java,Netty,java,后端)