Netty学习总结三(Netty启动流程和重要组件介绍)

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处理操作环节包括:从通道读数据包、数据包解码、业务处理、目标数据编码、把数据包写到通道,然后由通道发送到对端.
整个的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 {@Overridepublic void channelRead(ChannelHandlerContextctx,Object msg) throwsException {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<? >> extendsEventListener {//监听器的回调方法
            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高并发实战》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值