Netty的常用概念
我们先来看一段代码:
// Configure the server.
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(Config.threadNum);
try {
ServerBootstrap b = new ServerBootstrap();
b.option(ChannelOption.SO_BACKLOG, 1024);
b.option(ChannelOption.SO_REUSEADDR, true);
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new RiskServerInitializer(sslCtx));
Channel ch = b.bind(Config.PORT).sync().channel();
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
Channel, EventLoop, 以及ChannelFuture是Netty的三个基本组件。
Channel与Socket紧密关联, EventLoop负责控制流、多线程处理及并发等功能,而ChannelFuture则主要负责异步通知。
Channel接口
基本的I/O操作bing()、Connect()、read()和write()依赖于底层网络传输所提供的原语。在基于Java网络编程中,其基本构造是class Socket。Netty的Channel接口所提供的API,大大降低了直接使用Socket类的复杂性。此外,Channel也是拥有许多预定义的、专门化实现的广泛类层次结构的根。
方法名 | 描述 |
---|---|
eventLoop | 返回分配给Channel的EventLoop |
pipiline | 返回分配给Channel的ChannelPipeline |
isActive | 如果Channel是活动的,则返回true。活动的意义可能依赖于底层的传输。例如,一个Socket传输一旦连接到了远程节点便是活动的,而一个Datagram传输一旦被打开便是活动的 |
localAddress | 返回本地的SocketAddress |
remoteAdress | 返回远程的SocketAdress |
write | 将数据写到远程节点。这个数据将被传递给ChannelPipeline,并且排队直到它被冲刷 |
flush | 将之前已写的数据冲刷到底层传输,如一个Socket |
writeAndFlush | 一个简便的方法,等同于调用write()并接着调用flush() |
EventLoop接口
EventLoop定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件。
- 一个EventLoopGroup包含一个或多个EventLoop;
- 一个EventLoop在它的生命周期内只和一个Thread绑定;
- 所有由EventLoop处理的I/O事件都将在它专有的Tread上被处理;
- 一个Channel在它的生命周期内只注册于一个EventLoop;
- 一个EventLoop可能会被分配给一个或多个Channel。
ChannelFuture接口
可以看做是将来要执行的操作的结果的占位符。它究竟什么时候被执行则可能取决于若干的因素,因此具有不可预测性。此外,所有属于同一个Channel的操作都被保证其将以它们被调用的顺序被执行。
ChannelHandler接口
毫无疑问,这是Netty的主要组件,它充当了所有处理入站和出站数据的应用程序逻辑的容器。这事可行的,在ChannelHandler的方法是由网络事件触发的。事实上,ChannelHandler可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,或者处理转换过程中出现的异常。
ChannelInboundHandler接口是一个经常会实现的子接口。这种类型的ChannelHandler接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。你的应用程序的业务逻辑通常驻留在一个或多个ChannelInboundHandler中。
ChannelPipeline接口
ChannelPipeline为ChannelHandler链提供了容器,并定义了用于在该链上传播入站和出站事件流的API。当Channel被创建时,它会被自动分配懂啊它的专属的ChannelPipeline.
ChannelHandler安装到ChannelPipeline中的过程如下:
- 一个ChannelInitializer的实现被注册到了ServerBootstrap中;
- 当ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在ChannelPipeline中安装一组自定义的ChannelHandler;
- ChannelInitializer将它自己从ChannelPipeline中移除。
为了审查发送或者接收数据时将会发生什么,让我们来更加深入地研究ChannelPipeline与ChannelHandler之间的共生关系吧。
ChannelHandler是专为支持广泛的用途而设计的,可以将它看做 是处理往来ChannelPipeline事件(包括数据)的任何代码的通用容器。ChannelHandler派生出ChannelInboundHandler与ChannelOutboundHandler接口。
使得事件流经ChannelPipeline是ChannelHandler的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。实际上,被我们称为ChannelPipeline的是这些ChannelHandler编排顺序。
从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,反之则称为入站的。
入站和出站ChannelHandler可以被安装到同一个ChannelPipeline中。如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline的头部开始流动,并被传递给第一个ChannelInboundHandler。这个ChannelHandler不一定会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个ChannelInboundHandler。最终,数据将会到达ChannelPipeline的尾端,届时,所有处理就都结束了。
数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,这里显示为Socket。通常情下,这将触发一个写操作。
关于入站和出站ChannelHandler的更多讨论
通过使用作为参数传递到每个方法的ChannelHandlerContext,事件可以被传递给当前ChannelHandler链中的下一个ChannelHandler。因为你有时会忽略那些不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。通过调用ChannelHandlerContext 上的方法,每个都提供了简单地3将事件传递给下一个ChannelHandler的方法的实现。
鉴于出站操作和入站操作是不同的,你可能回想知道如果将两个类别的ChannelHandler都混合添加到同一个ChannelPipeline中会发生什么。虽然ChannelInboundHandler与ChannelOutboundHandler都扩展自ChannelHandler,但是Netty能区分ChannelInboundHandler与ChannelOutboundHandler实现,并确保数据只会在具有相同类型的两个ChannelHandler之间传递。
当ChannelHandler被添加到ChannelPipeline时,它将会被分配一个ChannelHandlerContext ,其代表了ChannelHandler与ChannelPipeline之间的绑定。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。
在Netty中,有两种发送消息的方式。你可以直接写到Channel中,也可以写到和ChannelHandler相关联的ChannelHandlerContext对象中。前一种方式将会导致消息从ChannelPipeline的尾端开始流动,而后者将导致消息从ChannelPipiline中下一个ChannelHandler开始流动。
更加深入地了解ChannelHandler
正如我们之前所说的,有许多不同类型的ChannelHandler,它们各自的功能主要取决于它们的超类。Netty的适配器类的形式提供了大量默认的ChannelHadler的实现,其旨在简化应用程序处理逻辑的开发过程。你已经看到了,ChannelPipeline中的每个ChannelHandler将负责把事件转发到链中的下一个ChannelHandler。这些适配器类(及它们的子类)将自动执行这个操作,所以你只可以重写那些你想要特殊处理的方法和事件。
编码器和解码器
当你通过Netty发送或者接收一个消息的时候,就将;会发生一次数据转换。入站消息会 被解码;也就是说,从字节转换为另一种格式,通常是一个Java对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节。
对应与特定的需要,Netty为编码器和解码器提供了不同类型的抽象类。例如,你的应用程序可能使用了一种中间格式,而不需要立即将消息转换字节。你将仍然需要一个编码器,但是它派生自一个不同的超类。为了确定合适的编码器类型,你可以应用一个简单地命名约定。
通常来说,这些基类的名称将类似于ByteTiMessageDecoder或MessageToByteEncoder。对于特殊的类型,你可能会发现类似于ProtobufEncoder和ProtobufDecoder这样的名称——预置的用来支持Google的ProtoBuffers。
严格的说,其他的处理器也可以完成编码器和解码器的功能,但是,正如有用来简化ChannelHandler的创建的适配器类一样,所有由Netty提供的编码器/解码器适配器类都实现了ChannelOutBoundHandler或者ChannelInBoiundHandler接口。
你会发现对于入站数来说,channelRead方法/事件已经被重写了。对于每个从入站Channel读取的消息,这个方法都将会被调用。随后,它将调用有预置解码器提供的decode()方法,并将已解码的字节转发给ChannelPipeline中的下一个ChannelInboundHadler。
出站消息的模式是相反的:编码器将消息转换为字节,并将它们转发给下一个ChannelOutboundHandler。
抽象类 SimpleChannelInboundHandler
最常见的情况是,你的应用程序会利用一个ChannnelHandler来接收解码消息,并对该数据应用业务逻辑。
要创建一个这样的ChannelHandler,你只需要扩展基类SimpleChannelInboundHandler<T>,其中,T是你要处理的消息的Java 类型。
这个ChannelHandler中,你将需要重写基类的一个或多个方法,并且获取一个到ChanelHandlerContext的引用,这个引用将作为输入参数传递给ChannelHandler的所有方法。
在这种类型的channelHandler中,最重要的方法是ChannelRead0(ChannelHandlerContect,T)。除了要求不要阻塞当前的I/O线程之外,其具体实现完全取决于你。
引导
Netty的引导类为应用程序的网络层配置提供了容器,这涉及将一个进程绑定到某个指定的端口,或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程。
通常来说,我们把前面的用例称作引导一个服务器,后面的用例称作为引导一个客户端。虽然这个术语简单方便,但是它略微掩盖了一个重要的事实,即“服务器”和“客户端”实际上表示了不同的网络行为;换句话说,是监听传入的连接还是建立到一个或者多个进程的连接。
因此,有两种类型的引导:一种用于客户端(简单地称为BootStrap),而另一种(ServerBootStrap)用于服务器。
无论你的应用程序使用哪种协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。
这两种类型的引导类之间的第一个区别:ServerBootStrap将绑定到一个端口,因为服务器必须要监听连接,而BootStrap则是由想要连接到远程节点的客户端应用程序锁使用的。
第二个区别可能更加明显。引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootStrap则需要两个(也可以是同一个实例)。因为服务器需要两组不同的Channel。
第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。
而第二组将包含所有已创建的用来处理传入客户端连接(对于每一个服务器已经接受的连接都有一个)的Channel。
与ServerChannel相关联的EventLoopGroup将分配一个负责为传入连接请求创建Channel的EventLoop。
一旦连接被接受,第二个EventLoopGroup就会给它的Channel分配一个EventLoop。