Netty学习总结一(Netty优点场景、IO模型、JavaNIO)
Netty学习总结二(Reactor介绍和Netty线程模型)
Netty学习三(Netty启动流程和重要组件介绍)
一、Netty服务端启动流程例子
这里先对流程有个大概的认识,可以带着疑问去了解每个组件的作用
//创建一个服务器的启动器 ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
//创建反应器线程组,并赋值给ServerBootstrap启动器实例
NioEventLoopGroup bossLoopGroup =new NioEventLoopGroup(10);
NioEventLoopGroup workerLoopGroup =new NioEventLoopGroup();
try {
//第1步:配置线程组
serverBootstrap.group(bossLoopGroup,workerLoopGroup);
//第2步:设置通道的IO类型
serverBootstrap.channel(NioServerSocketChannel.class);
//第3步:设置监听端口
serverBootstrap.localAddress(new InetSocketAddress(8801));
//第4步:设置传输通道的配置选项(SO_BACKLOG表示服务器端接收连接的队列长度,如果队列已满,客户端连接将被拒绝。默认值,在Windows中为200,其他操作系统为128。)
serverBootstrap.option(ChannelOption.SO_BACKLOG,128);
//serverBootstrap.option(ChannelOption.ALLOCATOR.PooledByteBufAllocator)
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE,true);
//第5步:装配子通道的Pipeline流水线
serverBootstrap.childHandler(new ChannelInitializer<NioServerSocketChannel>() {
@Override
protected void initChannel(NioServerSocketChannel ch) throws Exception {
//在流水线中加入自己的业务处理器
ch.pipeline().addLast(myServerHandler);
}
});
//第6步:开始绑定服务器新连接的监听端口
ChannelFuture channelFuture = serverBootstrap.bind().sync();
//第7步:自我阻塞,直到通道关闭
channelFuture.channel().closeFuture().sync();
} finally {
//第8步:关闭EventLoopGroup
bossLoopGroup.shutdownGracefully();
workerLoopGroup.shutdownGracefully();
}
二、Netty组成
1.Bootstrap启动器类
ServerBootstrap serverBootstrap = new ServerBootstrap();
Bootstrap类是Netty提供的一个便利的工厂类,可以通过它来完成Netty的客户端或服务器端的Netty组件的组装,以及Netty程序的初始化配置。
当然,Netty的官方解释是,完全可以不用这个Bootstrap启动器。但是,一点点去手动创建通道、完成各种设置和启动、并且注册到EventLoop,这个过程会非常麻烦。通常情况下,还是使用这个便利的Bootstrap工具类会效率更高。
2.EventLoop接口
//创建反应器线程组,并赋值给ServerBootstrap启动器实例
NioEventLoopGroup bossLoopGroup =new NioEventLoopGroup(10);
NioEventLoopGroup workerLoopGroup =new NioEventLoopGroup();
在Netty中,一个EventLoop相当于一个子反应器(SubReactor)。大家已经知道,一个NioEventLoop子反应器拥有了一个线程,同时拥有一个Java NIO选择器。
Netty如何组织外层的反应器呢?
答案是使用EventLoopGroup
线程组。多个EventLoop线程组成一个EventLoopGroup线程组。
反过来说,Netty的EventLoopGroup线程组就是一个多线程版本的反应器。而其中的单个EventLoop线程对应于一个子反应器(SubReactor)。
Netty的程序开发不会直接使用单个EventLoop线程,而是使用EventLoopGroup线程组。EventLoopGroup的构造函数有一个参数,用于指定内部的线程数。在构造器初始化时,会按照传入的线程数量,在内部构造多个Thread线程和多个EventLoop子反应器(一个线程对应一个EventLoop子反应器),进行多线程的IO事件查询和分发。
如果使用EventLoopGroup的无参数的构造函数,没有传入线程数或者传入的线程数为0,那么EventLoopGroup内部的线程数到底是多少呢?
默认的EventLoopGroup内部线程数为最大可用的CPU处理器数量的2倍。假设电脑使用的是4核的CPU,那么在内部会启动8个EventLoop线程,相当8个子反应器(SubReactor)实例。
从前文可知,为了及时接受(Accept)到新连接,在服务器端,一般有两个独立的反应器,一个反应器负责新连接的监听和接受,另一个反应器负责IO事件处理。对应到Netty服务器程序中,则是设置两个EventLoopGroup线程组,一个EventLoopGroup负责新连接的监听和接受,一个EventLoopGroup负责IO事件处理。
那么,两个反应器如何分工呢?
负责新连接的监听和接受的EventLoopGroup线程组,查询父通道的IO事件,有点像负责招工的包工头,因此,可以形象地称为“包工头”(Boss)线程组。另一个EventLoopGroup线程组负责查询所有子通道的IO事件,并且执行Handler处理器中的业务处理——例如数据的输入和输出(有点儿像搬砖),这个线程组可以形象地称为“工人”(Worker)线程组。
3.Channel通道
在Netty中,通道是其中的核心概念之一,代表着网络连接。通道是通信的主题,由它负责同对端进行网络通信,可以写入数据到对端,也可以从对端读取数据。
Netty中的每一种协议的通道,都有NIO(异步IO)和OIO(阻塞式IO)两个版本。
对应于不同的协议,Netty中常见的通道类型如下:
· NioSocketChannel:异步非阻塞TCP Socket传输通道。
· NioServerSocketChannel:异步非阻塞TCP Socket服务器端监听通道。
· NioDatagramChannel:异步非阻塞的UDP传输通道。
· NioSctpChannel:异步非阻塞Sctp传输通道。
· NioSctpServerChannel:异步非阻塞Sctp服务器端监听通道。
· OioSocketChannel:同步阻塞式TCP Socket传输通道。
· OioServerSocketChannel:同步阻塞式TCP Socket服务器端监听通道。
· OioDatagramChannel:同步阻塞式UDP传输通道。
· OioSctpChannel:同步阻塞式Sctp传输通道。
· OioSctpServerChannel:同步阻塞式Sctp服务器端监听通道。
一般来说,服务器端编程用到最多的通信协议还是TCP协议。对应的传输通道类型为NioSocketChannel
类,服务器监听类为NioServerSocketChannel
AbstractChannel抽象类
几乎所有的通道实现类都继承了AbstractChannel抽象类。
public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(AbstractChannel.class);
private final Channel parent;
private final ChannelId id;
private final Unsafe unsafe;
private final DefaultChannelPipeline pipeline;
...
/**
* Creates a new instance.
*
* @param parent
* the parent of this channel. {@code null} if there's no parent.
*/
protected AbstractChannel(Channel parent) {
//父通道
this.parent = parent;
id = newId();
//底层的NIO通道,完成实际的IO操作
unsafe = newUnsafe();
//一条通道,拥有一条流水线
pipeline = newChannelPipeline();
}
...
AbstractChannel内部有一个pipeline属性,表示处理器的流水线。Netty在对通道进行初始化的时候,将pipeline属性初始化为DefaultChannelPipeline的实例。这段代码也表明,每个通道拥有一条ChannelPipeline处理器流水线。
AbstractChannel内部有一个parent属性,表示通道的父通道。对于连接监听通道(如NioServerSocketChannel实例)来说,其父亲通道为null;而对于每一条传输通道(如NioSocketChannel实例),其parent属性的值为接收到该连接的服务器连接监听通道。
AbstractChannel重要方法
再来看一下,在通道接口中所定义的几个重要方法:
方法1. ChannelFuture connect(SocketAddress address)
@Override
public ChannelFuture connect(SocketAddress remoteAddress) {
return pipeline.connect(remoteAddress);
}
@Override
public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
return pipeline.connect(remoteAddress, localAddress);
}
此方法的作用为:连接远程服务器。方法的参数为远程服务器的地址,调用后会立即返回,返回值为负责连接操作的异步任务ChannelFuture。此方法在客户端的传输通道使用。
方法2. ChannelFuture bind(SocketAddress address)
@Override
public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
return pipeline.bind(localAddress, promise);
}
此方法的作用为:绑定监听地址,开始监听新的客户端连接。此方法在服务器的新连接监听和接收通道使用。
方法3. ChannelFuture close()
@Override
public ChannelFuture close() {
return pipeline.close();
}
此方法的作用为:关闭通道连接,返回连接关闭的ChannelFuture异步任务。如果需要在连接正式关闭后执行其他操作,则需要为异步任务设置回调方法;或者调用ChannelFuture异步任务的sync( ) 方法来阻塞当前线程,一直等到通道关闭的异步任务执行完毕。
方法4. Channel read()
@Override
public Channel read() {
pipeline.read();
return this;
}
此方法的作用为:读取通道数据,并且启动入站处理。具体来说,从内部的Java NIO Channel通道读取数据,然后启动内部的Pipeline流水线,开启数据读取的入站处理。此方法的返回通道自身用于链式调用。
方法5. ChannelFuture write(Object o)
@Override
public ChannelFuture write(Object msg) {
return pipeline.write(msg);
}
@Override
public ChannelFuture write(Object msg, ChannelPromise promise) {
return pipeline.write(msg, promise);
}
此方法的作用为:启程出站流水处理,把处理后的最终数据写到底层Java NIO通道。此方法的返回值为出站处理的异步处理任务。
方法6. Channel flush()
@Override
public Channel flush() {
pipeline.flush();
return this;
}
此方法的作用为:将缓冲区中的数据立即写出到对端。并不是每一次write操作都是将数据直接写出到对端,write操作的作用在大部分情况下仅仅是写入到操作系统的缓冲区,操作系统会将根据缓冲区的情况,决定什么时候把数据写到对端。而执行flush()方法立即将缓冲区的数据写到对端。
上面的6种方法,仅仅是比较常见的方法。在Channel接口中以及各种通道的实现类中,还定义了大量的通道操作方法。在一般的日常的开发中,如果需要用到,请直接查阅Netty API文档或者Netty源代码。
父子通道
在Netty中,每一个NioSocketChannel通道所封装的是Java NIO通道,再往下就对应到了操作系统底层的socket描述符。理论上来说,操作系统底层的socket描述符分为两类:
· 连接监听类型。连接监听类型的socket描述符,放在服务器端,它负责接收客户端的套接字连接;在服务器端,一个“连接监听类型”的socket描述符可以接受(Accept)成千上万的传输类的socket描述符。
· 传输数据类型。数据传输类的socket描述符负责传输数据。同一条TCP的Socket传输链路,在服务器和客户端,都分别会有一个与之相对应的数据传输类型的socket描述符。
在Netty中,异步非阻塞的服务器端监听通道NioServerSocketChannel,封装在Linux底层的描述符,是“连接监听类型”socket描述符;而NioSocketChannel异步非阻塞TCP Socket传输通道,封装在底层Linux的描述符,是“数据传输类型”的socket描述符。
在Netty中,将有接收关系的NioServerSocketChannel和NioSocketChannel,叫作父子通道。其中,NioServerSocketChannel负责服务器连接监听和接收,也叫父通道(Parent Channel)。对应于每一个接收到的NioSocketChannel传输类通道,也叫子通道(Child Channel)。
4.Handler业务处理器
整个的IO处理操作环节包括:从通道读数据包、数据包解码、业务处理、目标数据编码、把数据包写到通道,然后由通道发送到对端.
前后两个环节,从通道读数据包和由通道发送到对端,由Netty的底层负责完成,不需要用户程序负责。
用户程序主要在Handler业务处理器中,Handler涉及的环节为:数据包解码、业务处理、目标数据编码、把数据包写到通道中。
从应用程序开发人员的角度来看,有入站和出站两种类型操作。
-
入站处理,触发的方向为:自底向上,Netty的内部(如通道)到
ChannelInboundHandler
入站处理器。 -
出站处理,触发的方向为:自顶向下,从
ChannelOutboundHandler
出站处理器到Netty的内部(如通道)。
按照这种方向来分,前面数据包解码、业务处理两个环节——属于入站处理器的工作;后面目标数据编码、把数据包写到通道中两个环节——属于出站处理器的工作。
ChannelInboundHandler通道入站处理器
当数据或者信息入站到Netty通道时,Netty将触发入站处理器ChannelInboundHandler所对应的入站API,进行入站操作处理。
在Netty中,它的默认实现为ChannelInboundHandlerAdapter
,在实际开发中,只需要继承这个ChannelInboundHandlerAdapter默认实现,重写自己需要的方法即可。
ChannelOutboundHandler通道出站处理器
当业务处理完成后,需要操作Java NIO底层通道时,通过一系列的ChannelOutboundHandler通道出站处理器,完成Netty通道到底层通道的操作。比方说建立底层连接、断开底层连接、写入底层Java NIO通道等。ChannelOutboundHandler接口定义了大部分的出站操作。
在Netty中,它的默认实现为ChannelOutboundHandlerAdapter
,在实际开发中,只需要继承这个ChannelOutboundHandlerAdapter默认实现,重写自己需要的方法即可。
ChannelInitializer通道初始化处理器
通道和Handler业务处理器的关系是:一条Netty的通道拥有一条Handler业务处理器流水线,负责装配自己的Handler业务处理器。装配Handler的工作,发生在通道开始工作之前。
如果向流水线中装配业务处理器呢?这就得借助通道的初始化类——ChannelInitializer
。
//第5步:装配子通道的Pipeline流水线
serverBootstrap.childHandler(new ChannelInitializer<NioServerSocketChannel>() {
@Override
protected void initChannel(NioServerSocketChannel ch) throws Exception {
//在流水线中加入自己的业务处理器
ch.pipeline().addLast(myServerHandler);
}
});
上面的ChannelInitializer也是通道初始化器,属于入站处理器的类型。在示例代码中,使用了ChannelInitializer的initChannel() 方法。它是何方神圣呢?
initChannel()方法是ChannelInitializer定义的一个抽象方法,这个抽象方法需要开发人员自己实现。在父通道调用initChannel()方法时,会将新接收的通道作为参数,传递给initChannel()方法。initChannel()方法内部大致的业务代码是:拿到新连接通道作为实际参数,往它的流水线中装配Handler业务处理器。
ChannelInitializer,在它的注册回调channelRegistered方法中,就使用了ctx.pipeline().remove(this),将自己从流水线中删除。
ChannelInitializer在完成了通道的初始化之后,为什么要将自己从流水线中删除呢?原因很简单,就是一条通道只需要做一次初始化的工作。
5.Pipeline流水线
一条Netty通道需要很多的Handler业务处理器来处理业务。每条通道内部都有一条流水线(Pipeline)将Handler装配起来。Netty的业务处理器流水线ChannelPipeline是基于责任链设计模式(Chain of Responsibility)来设计的,内部是一个双向链表结构,能够支持动态地添加和删除Handler业务处理器
ChannelHandlerContext上下文
不管我们定义的是哪种类型的Handler业务处理器,最终它们都是以双向链表的方式保存在流水线中。这里流水线的节点类型,并不是前面的Handler业务处理器基类,而是一个新的Netty类型:ChannelHandlerContext通道处理器上下文类。ChannelHandlerContext又是何方神圣呢?
在Handler业务处理器被添加到流水线中时,会创建一个通道处理器上下文ChannelHandlerContext,它代表了ChannelHandler通道处理器和ChannelPipeline通道流水线之间的关联。
ChannelHandlerContext中包含了有许多方法,主要可以分为两类:
第一类是获取上下文所关联的Netty组件实例,如所关联的通道、所关联的流水线、上下文内部Handler业务处理器实例等;
第二类是入站和出站处理方法。
在Channel、ChannelPipeline、ChannelHandlerContext三个类中,会有同样的出站和入站处理方法,同一个操作出现在不同的类中,功能有何不同呢?如果通过Channel或ChannelPipeline的实例来调用这些方法,它们就会在整条流水线中传播。然而,如果是通过ChannelHandlerContext通道处理器上下文进行调用,就只会从当前的节点开始执行Handler业务处理器,并传播到同类型处理器的下一站(节点)。
Channel、Handler、ChannelHandlerContext三者的关系为:Channel通道拥有一条ChannelPipeline通道流水线,每一个流水线节点为一个ChannelHandlerContext通道处理器上下文对象,每一个上下文中包裹了一个ChannelHandler通道处理器。在ChannelHandler通道处理器的入站/出站处理方法中,Netty都会传递一个Context上下文实例作为实际参数。通过Context实例的实参,在业务处理中,可以获取ChannelPipeline通道流水线的实例或者Channel通道的实例。
截断流水线的处理
在channelRead方法中,不再调用父类的channelRead入站方法
class SimpleInHandlerB2 extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContextctx,Object msg) throws
Exception {
Logger.info("入站处理器B: 被回调 ");
//不调用基类的channelRead, 终止流水线的执行
//super.channelRead(ctx, msg);
}
6.ChannelFuture通道IO异步
Netty继承和扩展了JDK Future系列异步回调的API,定义了自身的Future系列接口和类,实现了异步任务的监控、异步执行结果的获取。并且名称没有变,还是叫作Future接口,代码实现位于io.netty.util.concurrent包中。
和Guava的ListenableFuture一样,Netty的Future接口,扩展了一系列的方法,对执行的过程的进行监控,对异步回调完成事件进行监听(Listen)
总体来说,Netty对JavaFuture异步任务的扩展如下:
(1)继承Java的Future接口,得到了一个新的属于Netty自己的Future异步任务接口;该接口对原有的接口进行了增强,使得Netty异步任务能够以非阻塞的方式处理回调的结果;注意,Netty没有修改Future的名称,只是调整了所在的包名,Netty的Future类的包名和Java的Future接口的包名不同。
(2)引入了一个新接口——GenericFutureListener,用于表示异步执行完成的监听器。这个接口和Guava的FutureCallbak回调接口不同。Netty使用了监听器的模式,异步任务的执行完成后的回调逻辑抽象成了Listener监听器接口。可以将Netty的GenericFutureListener监听器接口加入Netty异步任务Future中,实现对异步任务执行状态的事件监听。
GenericFutureListener接口
Netty新增了一个接口来封装异步非阻塞回调的逻辑——它就是GenericFutureListener接口。
GenericFutureListener位于io.netty.util.concurrent包中,源代码如下:
package io.netty.util.concurrent;
import java.util.EventListener;
public interface GenericFutureListener<F extends Future<? >> extends
EventListener {
//监听器的回调方法
void operationComplete(F var1) throws Exception;
}
ChannelFuture接口
Netty的Future接口一般不会直接使用,而是会使用子接口。Netty有一系列的子接口,代表不同类型的异步任务,如ChannelFuture接口。ChannelFuture子接口表示通道IO操作的异步任务;如果在通道的异步IO操作完成后,需要执行回调操作,就需要使用到ChannelFuture接口。
ChannelFuture要么是未完成状态,要么是已完成状态。ChannelFuture提供了各种方法,可让您检查IO操作是否已完成,等待完成以及获取IO操作的结果。它还允许您添加ChannelFutureListener,以便在IO操作完成时得到通知。
public interface ChannelFuture extends Future<Void> {
//将指定的listener添加到Future。Future完成时,将通知指定的listener。如果Future已经完成,则立即通知指定的listener;
@Override
ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> listener);
@Override
ChannelFuture removeListener(GenericFutureListener<? extends Future<? super Void>> listener);
//等待Future直到其完成,如果这个Future失败,则抛出失败原因
@Override
ChannelFuture sync() throws InterruptedException;
//等待Future完成;
@Override
ChannelFuture await() throws InterruptedException;
...
}
参考文章
《Netty、Redis、Zookeeper高并发实战》