这些关系是:
EventLoopGroup
包含一个或者多个EventLoop
;EventLoop
在它的生命周期内只和一个Thread
绑定;EventLoop
处理的I/O事件都将在它专有的Thread
上被处理;Channel
在它的生命周期内只注册于一个EventLoop
;EventLoop
可能会被分配给一个或多个Channel
。一个给定Channel
的I/O操作都是由相同的Thread
执行的,消除了对于同步的需要
Netty中所有的I/O操作都是异步的,我们需要在方法执行之后的某个时间点确定其结果,对此Netty提供了ChannelFuture
接口,他的addListener()
方法注册了一个ChannelFutureListener
,可以实现监听,以便在某个操作完成时(无论是否成功)得到通知。
状 态 | 描 述 |
---|---|
ChannelUnregistered |
Channel 已经被创建,但还未注册到EventLoop |
ChannelRegistered |
Channel 已经被注册到了EventLoop |
ChannelActive |
Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了 |
ChannelInactive |
Channel 没有连接到远程节点 |
当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给ChannelPipeline
中的ChannelHandler
,其可以随后对它们做出响应。
从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler
,它充当了所有处理入站和出站数据的应用程序逻辑的容器。
下表列出了ChannelHandler
接口定义的生命周期操作,在ChannelHandler
被添加到ChannelPipeline
中或者被从ChannelPipeline
中移除时会调用这些操作。这些方法中的每一个都接受一个ChannelHandlerContext
参数。
类 型 | 描 述 |
---|---|
handlerAdded |
当把ChannelHandler 添加到ChannelPipeline 中时被调用 |
handlerRemoved |
当从ChannelPipeline 中移除ChannelHandler 时被调用 |
exceptionCaught |
当处理过程中在ChannelPipeline 中有错误产生时被调用 |
Netty定义了下面两个重要的ChannelHandler
子接口:
ChannelInboundHandler
——处理入站数据以及各种状态变化;ChannelOutboundHandler
——处理出站数据并且允许拦截所有的操作。以下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 变为再次可写时恢复写入。可以通过调用Channel 的isWritable() 方法来检测Channel 的可写性。与可写性相关的阈值可以通过Channel.config(). setWriteHighWaterMark() 和Channel.config().setWriteLowWater- Mark() 方法来设置 |
userEventTriggered |
当ChannelnboundHandler.fireUserEventTriggered() 方法被调用时被调用,因为一个POJO被传经了ChannelPipeline |
出站操作和数据将由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
参数,以便在操作完成时得到通知。ChannelPromise
是ChannelFuture
的一个子类,其定义了一些可写的方法,如setSuccess()
和setFailure()
,从而使ChannelFuture
不可变。
可以使用ChannelInboundHandlerAdapter
和ChannelOutboundHandlerAdapter
类作为自己的ChannelHandler
的起始点。这两个适配器分别提供了ChannelInboundHandler
和ChannelOutboundHandler
的基本实现。
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
为ChannelHandler
链提供了容器,并定义了用于在该链上传播入站和出站事件流的API,当一个Channel
被创建时,它会被自动地分配到它专属的ChannelPipeline
。
ChannelHandler
安装到ChannelPipeline
中的过程如下所示:
ChannelInitializer
的实现被注册到了ServerBootstrap
中ChannelInitializer.initChannel()
方法被调用时,ChannelInitializer
将在ChannelPipeline
中安装一组自定义的ChannelHandler
;ChannelInitializer
将它自己从ChannelPipeline
中移除。根据事件的来源,事件将会被ChannelInboundHandler
或者ChannelOutboundHandler
处理。随后,通过调用ChannelHandlerContext
实现,它将被转发给同一超类的下一个ChannelHandler
。
ChannelHandler
可以通过添加、删除、替换其他的ChannelHandler
或者是将它自己从ChannelPipeline
中移除等操作,来实时地修改ChannelPipeline
的布局。
以下是ChannelPipeline
上的相关方法,由ChannelHandler
用来修改ChannelPipeline
的布局:
名 称 | 描 述 |
---|---|
addFirstaddBeforeaddAfteraddLast |
将一个ChannelHandler 添加到ChannelPipeline 中 |
remove |
将一个ChannelHandler 从ChannelPipeline 中移除 |
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
有一些接受一个EventExecutorGroup
的add()
方法。如果一个事件被传递给一个自定义的EventExecutorGroup
,它将被这个EventExecutorGroup
中的某个EventExecutor
处理,从而从该Channel
本身的EventLoop
中移除。对于这种用例,Netty提供了一个叫DefaultEventExecutorGroup
的默认实现。
除了这些操作,还有别的通过类型或者名称来访问ChannelHandler
的方法。
名 称 | 描 述 |
---|---|
get |
通过类型或者名称返回ChannelHandler |
context |
返回和ChannelHandler 绑定的ChannelHandlerContext |
names |
返回ChannelPipeline 中所有ChannelHandler 的名称 |
下面列出了ChannelPipeline
的入站操作,用于通知ChannelInboundHandler
在ChannelPipeline
中所发生的事件。
方 法 名 称 | 描 述 |
---|---|
fireChannelRegistered |
调用ChannelPipeline 中下一个ChannelInboundHandler 的channelRegistered(ChannelHandlerContext) 方法 |
fireChannelUnregistered |
调用ChannelPipeline 中下一个ChannelInboundHandler 的channelUnregistered(ChannelHandlerContext) 方法 |
fireChannelActive |
调用ChannelPipeline 中下一个ChannelInboundHandler 的channelActive(ChannelHandlerContext) 方法 |
fireChannelInactive |
调用ChannelPipeline 中下一个ChannelInboundHandler 的channelInactive(ChannelHandlerContext) 方法 |
fireExceptionCaught |
调用ChannelPipeline 中下一个ChannelInboundHandler 的exceptionCaught(ChannelHandlerContext, Throwable) 方法 |
fireUserEventTriggered |
调用ChannelPipeline 中下一个ChannelInboundHandler 的userEventTriggered(ChannelHandlerContext, Object) 方法 |
fireChannelRead |
调用ChannelPipeline 中下一个ChannelInboundHandler 的channelRead(ChannelHandlerContext, Object msg) 方法 |
fireChannelReadComplete |
调用ChannelPipeline 中下一个ChannelInboundHandler 的channelReadComplete(ChannelHandlerContext) 方法 |
fireChannelWritability Changed |
调用ChannelPipeline 中下一个ChannelInboundHandler 的channelWritabilityChanged(ChannelHandlerContext)方 法 |
在出站时,处理事件将会导致底层的Socket
上发生一系列的动作。下面列出了ChannelPipeline
API的出站操作:
方 法 名 称 | 描 述 |
---|---|
bind |
将Channel 绑定到一个本地地址,这将调用ChannelPipeline 中的下一个ChannelOutboundHandler 的bind(ChannelHandlerContext, SocketAddress, ChannelPromise) 方法 |
connect |
将Channel 连接到一个远程地址,这将调用ChannelPipeline 中的下一个ChannelOutboundHandler 的connect(ChannelHandlerContext, Socket- Address, ChannelPromise) 方法 |
disconnect |
将Channel 断开连接。这将调用ChannelPipeline 中的下一个ChannelOutbound- Handler 的disconnect(ChannelHandlerContext, Channel Promise) 方法 |
close |
将Channel 关闭。这将调用ChannelPipeline 中的下一个ChannelOutbound- Handler 的close(ChannelHandlerContext, ChannelPromise) 方法 |
deregister |
将Channel 从它先前所分配的EventExecutor (即EventLoop )中注销。这将调用ChannelPipeline 中的下一个ChannelOutboundHandler 的deregister (ChannelHandlerContext, ChannelPromise) 方法 |
flush |
冲刷Channel 所有挂起的写入。这将调用ChannelPipeline 中的下一个Channel- OutboundHandler 的flush(ChannelHandlerContext) 方法 |
write |
将消息写入Channel 。这将调用ChannelPipeline 中的下一个Channel- OutboundHandler 的write(ChannelHandlerContext, Object msg, Channel- Promise) 方法。注意:这并不会将消息写入底层的Socket ,而只会将它放入队列中。要将它写入Socket ,需要调用flush() 或者writeAndFlush() 方法 |
writeAndFlush |
这是一个先调用write() 方法再接着调用flush() 方法的便利方法 |
read |
请求从Channel 中读取更多的数据。这将调用ChannelPipeline 中的下一个ChannelOutboundHandler 的read(ChannelHandlerContext) 方法 |
总结一下:
ChannelPipeline
保存了与Channel
相关联的ChannelHandler
;ChannelHandler
来动态地修改ChannelPipeline
;ChannelPipeline
有着丰富的API用以被调用,以响应入站和出站事件。ChannelHandler
的工作是使得事件流经ChannelPipeline
。ChannelHandler
是在应用程序的初始化或者引导阶段被安装的,这些对象接收事件、执行它们要实现的业务逻辑,并将数据传递给链中的下一个ChannelHandler
。它们的执行顺序是由它们被添加的顺序所决定的。
下面展示了一个典型的同时具有入站和出站ChannelHandler
的ChannelPipeline
的布局,可以看出ChannelPipeline
主要由一系列的ChannelHandler
所组成的。从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,反之则称为入站的。
以ChannelInboundHandler
链为例,如果一个消息或者其他的入站事件被读取,那么它会从ChannelPipeline
的头部开始流动,并被传递给第一个ChannelInboundHandler
。最终,数据将会到达ChannelPipeline
的尾端,届时,所有处理就都结束了;
数据的出站运动也是一样的,数据将从ChannelOutboundHandler
链的尾端开始流动,直到它到达链的头部为止。
入站和出站ChannelHandler
可以被安装到同一个ChannelPipeline
中。虽然ChannelInboundHandle
和ChannelOutboundHandle
都扩展自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
代表着ChannelHandler
和ChannelPipeline
之间的关联,每当有ChannelHandler
添加到ChannelPipeline
中时,都会创建ChannelHandlerContext
。他的主要功能是管理它关联的ChannelHandler
和同ChannelPipeline
中的其他ChannelHandler
之间的交互。
ChannelHandlerContext
有很多方法,其中一些方法也存在于Channel
和ChannelPipeline
,但不同的是,如果调用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的时候,请牢记以下两点:
ChannelHandlerContext
和ChannelHandler
之间的绑定是永远不会改变的,所以缓存对它的引用是安全的;ChannelHandlerContext
的方法产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。Channel
、ChannelPipeline
、ChannelHandler
以及ChannelHandlerContext
之间的关系:
通过ChannelHandlerContext
分别获取到Channel
和ChannelPipeline
的引用,并调用他们的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));
}
以上两种方式的事件流是一样的。但是要注意,虽然调用的Channel
或ChannelPipeline
上的write()
方法事件将在整个ChannelPipeline
传播,但是在ChannelHandler
的级别上,事件从一个ChannelHandler
到下一个ChannelHandler
的移动是由ChannelHandlerContext
上的调用完成的。
为什么会想要从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));
}
为了收集跨越多个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
。
最常见的情况是我们的应用程序需要一个ChannelHandler
来接收解码消息,并对该数据进行一些业务逻辑上的处理。要创建一个这样的ChannelHandler
,只需要扩展基类SimpleChannelInboundHandler
,其中T
是你要处理的消息的Java类型。在这个ChannelHandler
中,需要重写基类的一个或者多个方法,并且获取一个到ChannelHandlerContext
的引用,这个引用将作为输入参数传递给ChannelHandler
的所有方法。
在SimpleChannelInboundHandler
中,最重要的方法是channelRead0(ChannelHandlerContext,T)
。除了要求不要阻塞当前的I/O线程之外,其具体实现完全取决于你。
Netty的引导类为应用程序的网络层配置提供了容器。
引导一个客户端:将一个进程连接到另一个运行在某个指定主机的指定端口上的进程。
引导一个服务器:将一个进程绑定到某个指定的端口;
分别两种类型的引导:一种用于客户端(Bootstrap
),而另一种用于服务器(ServerBootstrap
)。无论你的应用程序使用哪种协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。
类 别 | Bootstrap |
ServerBootstrap |
---|---|---|
网络编程中的作用 | 连接到远程主机和端口 | 绑定到一个本地端口 |
EventLoopGroup 的数目 |
1 | 2 |
服务器需要两组不同的Channel
。第一组只包含一个ServerChannel
,代表服务器自身已绑定到某个本地端口的Socket
。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel
。
与ServerChannel
相关联的EventLoopGroup
将分配一个负责为传入连接请求创建Channel
的EventLoop
。一旦连接被接受,第二个EventLoopGroup
就会给它的Channel
分配一个EventLoop
。