Netty是一个Java NIO客户端/服务器框架。

Netty中的Reactor模式

Java Reactor模式中IO事件的处理流程:

Java-Reactor

Reactor模式中IO事件的处理流程大致分为4步:

1、通道注册。IO事件源于通道(Channel),IO是和通道(对应于底层连接而言)强相关的。一个IO事件一定属于某个通道。如果要查询通道的事件,首先就要将通道注册到选择器。

2、查询事件。在Reactor模式中,一个线程会负责一个反应器(或者SubReactor子反应器),不断地轮询,查询选择器中的IO事件(选择键)。

3、事件分发。如果查询到IO事件,则分发给与IO事件有绑定关系的Handler业务处理器。

4、完成真正的IO操作和业务处理,这一步由Handler业务处理器负责。

服务端编程一般用到最多的通信协议还是TCP,对应的Netty传输通道类型为NioSocketChannel类、Netty服务器监听通道类型为NioServerSocketChannel

在Netty的NioSocketChannel内部封装了一个Java NIO的SelectableChannel成员,通过对该内部的Java NIO通道的封装,对Netty的NioSocketChannel通道上的所有IO操作最终都会落地到Java NIO的SelectableChannel底层通道。

对应于NioSocketChannel通道,Netty的反应器类为NioEventLoop(NIO事件轮询)。

理论上来说,一个EventLoop反应器和NettyChannel通道是一对多的关系:一个反应器可以注册成千上万的通道。

Netty的Handler分为两大类:第一类是ChannelInboundHandler入站处理器;第二类是ChannelOutboundHandler出站处理器,二者都继承了ChannelHandler处理器接口。

Bootstrap

Netty提供了一系列辅助类,用于把上面的三个组件快速组装起来完成一个Netty应用,这个系列的类叫作引导类。服务端的引导类叫作ServerBootstrap类,客户端的引导类叫作Bootstrap类。

Bootstrap的启动流程也就是Netty组件的组装、配置,以及Netty服务器或者客户端的启动流程。

BootStrap启动流程

创建一个服务端的ServerBootstrap引导类。

1
2
//创建一个服务端的引导类
ServerBootstrap b = new ServerBootstrap();

1、创建反应器轮询组,并设置到ServerBootstrap引导类实例。

1
2
3
4
5
6
7
8
9
//1 设置reactor 线程组
//创建反应器轮询组
//boss轮询组
EventLoopGroup bossLoopGroup = new NioEventLoopGroup(1);
//worker轮询组
EventLoopGroup workerLoopGroup = new NioEventLoopGroup();
//..
//step1:为ServerBootstrap引导类实例设置反应器轮询组
b.group(bossLoopGroup, workerLoopGroup);

在设置反应器轮询组之前,创建了两个NioEventLoopGroup

  • bossLoopGroup:负责处理连接监听IO事件

  • workerLoopGroup:负责数据传输事件和处理

在两个轮询组创建完成后,就可以配置给ServerBootstrap引导类实例,它一次性地给引导类配置了两大轮询组。

2、设置通道的IO类型。

Netty不仅支持Java NIO,也支持阻塞式的OIO。下面配置的是Java NIO类型的通道类型:

1
2
//2 设置NIO类型的channel
b.channel(NioServerSocketChannel.class);

3、设置监听端口。

1
2
//3 设置监听端口
b.localAddress(serverPort);

4、设置传输通道的配置选项。

1
2
3
4
//4 设置通道的参数
b.option(ChannelOption.SO_KEEPALIVE, true);
b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
b.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

设置了一个底层TCP相关的选项ChannelOption.SO_KEEPALIVE。该选项表示是否开启TCP底层心跳机制,true为开启,false为关闭。启用该功能时,TCP会主动探测空闲连接的有效性。需要注意的是:默认的心跳间隔是7200秒,即2小时。Netty默认关闭该功能。

5、装配子通道的Pipeline。每一个通道都用一条ChannelPipeline流水线,它的内部有一个双向的链表。

1
2
3
4
5
6
7
8
9
//5 装配子通道流水线
b.childHandler(new ChannelInitializer<SocketChannel>() {
    //有连接到达时会创建一个channel
    protected void initChannel(SocketChannel ch) throws Exception {
        // pipeline管理子通道channel中的Handler
        // 向子channel流水线添加一个handler处理器
        ch.pipeline().addLast(new NettyDiscardHandler());
    }
});

在装配流水线时需要注意的是,ChannelInitializer处理器有一个泛型参数SocketChannel,它代表需要初始化的通道类型,这个类型需要和前面的引导类中设置的传输通道类型一一对应起来。

6、开始绑定服务器新连接的监听端口

1
2
3
4
5
//6 开始绑定server
//通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = b.bind().sync();
Logger.info(" 服务器启动成功,监听端口: " +
        channelFuture.channel().localAddress());

7、自我阻塞,直到监听通道关闭

1
2
3
4
//7 等待通道关闭的异步任务结束
//服务监听通道会一直等待通道关闭的异步任务结束
ChannelFuture closeFuture = channelFuture.channel().closeFuture();
closeFuture.sync();

8、关闭EventLoopGroup

1
2
3
4
//8 优雅关闭EventLoopGroup,
//释放掉所有资源包括创建的线程
workerLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();

关闭反应器轮询组,同时会关闭内部的子反应器线程,也会关闭内部的选择器、内部的轮询线程以及负责查询的所有子通道。在子通道关闭后,会释放掉底层的资源,如Socket文件描述符等。

Channel

Netty通道的抽象类AbstractChannel的构造函数:

1
2
3
4
5
6
protected AbstractChannel(Channel parent) {
    this.parent = parent;
    this.id = this.newId();
    this.unsafe = this.newUnsafe();
    this.pipeline = this.newChannelPipeline();
}

AbstractChannel内部有一个parent父通道属性,保持通道的父通道。

  • 连接监听通道(如NioServerSocketChannel)parent属性的值为null;

  • 传输通道(如NioSocketChannel)parent属性的值为接收到该连接的监听通道。

在Netty的实际开发中,底层通信传输的基础工作Netty已经替大家完成。实际上,更多的工作是设计和开发ChannelHandler业务处理器。处理器开发完成后,需要投入单元测试。一般单元测试的大致流程是:先将Handler业务处理器加入到通道的Pipeline流水线中,接下来先后启动Netty服务器、客户端程序,相互发送消息,测试业务处理器的效果。这些复杂的工序存在一个问题:如果每开发一个业务处理器都进行服务器和客户端的重复启动,那么整个的过程是非常烦琐和浪费时间的。如何解决这种徒劳、低效的重复工作呢?Netty提供了一个专用通道,即EmbeddedChannel(嵌入式通道)。

EmbeddedChannel仅仅是模拟入站与出站的操作,底层不进行实际传输,不需要启动Netty服务器和客户端。

Handler

在Reactor经典模型中,反应器查询到IO事件后会分发到Handler业务处理器,由Handler完成IO操作和业务处理。

整个IO处理操作环节大致包括:

  • 从通道读数据包
  • 数据包解码
  • 业务处理
  • 目标数据编码
  • 把数据包写到通道
  • 由通道发送到对端。

整个的IO处理操作环节的前后两个环节

  • 从通道读数据包

  • 由通道发送到对端

由Netty的底层负责完成,不需要用户程序负责。

用户程序主要涉及的Handler环节为

  • 数据包解码:入站处理

  • 业务处理:入站处理

  • 目标数据编码:出站处理

  • 把数据包写到通道中:出站处理

从应用程序开发人员的角度来看有入站和出站两种类型操作。

  • 入站处理触发的方向为自底向上,从Netty的内部(如通道)到ChannelInboundHandler入站处理器。
  • 出站处理触发的方向为自顶向下,从ChannelOutboundHandler出站处理器到Netty的内部(如通道)。

PipeLine

Netty的业务处理器流水线ChannelPipeline是基于责任链设计模式(Chain of Responsibility)来设计的,内部是一个双向链表结构,能够支持动态地添加和删除业务处理器。

在Netty的设计中Handler是无状态的,不保存和Channel有关的信息。Handler的目标是将自己的处理逻辑做得很通用,可以给不同的Channel使用。

与Handler不同的是,Pipeline是有状态的,保存了Channel的关系。

于是,Handler和Pipeline之间通过一个中间角色ChannelHandlerContext(通道处理器上下文)将它们联系起来。

ByteBuf

Netty提供了ByteBuf缓冲区组件来替代Java NIO的ByteBuffer缓冲区组件,以便更加快捷和高效地操纵内存缓冲区。