Netty的编程模型

传统的BIO网络编程模型(线程池)

传统的网络编程模型,以Tomcat为代表,服务端的ServerSocketChannel个数取决于线程池中线程的数量,如图所示。
在这里插入图片描述
这种编程模型,客户端的连接数受限于线程池中线程的数量。

Netty的网络编程模型

在这里插入图片描述

按照场景去理解Netty

服务端的工作流程:

  • 阶段一:NioServerSocketChannel,实际是ServerSocketChannel注册到Selector多路复用器任务
    • 这个阶段会涉及两个线程:
      • 主程序线程,通常是main:创建注册Selector的任务
      • boss组中的nioEventLoop线程:执行上述注册任务,即register0。
        • register0干了什么事?
        1. k = ((AbstractSelector)sel).register(this, ops, att);

        2. 回调在AbstractBootstrap类中,向NioServerSocketChannel中的pipeline所添加的ChannelInitializer的initChannel方法。在回调过程中,创建能接收客户端连接的对象任务
          2.1 注意: 这里为什么是回调?因为,p.addLast(new ChannelInitializer<Channel>() {...}是创建对象时机,在main线程环境中;而回调initChannel方法,是执行时机,是在nioEventLoop线程环境中,这属于异步回调。
          2.2 探究一下这里使用回调的必要性以及恰当性,或者说为什么没有在用到的时候再创建ChannelInitializer该对象? 要解释这个问题,就要说Netty设计的初衷:简化JDK的NIO编程,向使用者屏蔽底层的实现细节。而简化的方式,就是将向使用者提供参数方式,而Netty将这些参数应用到NIO编程的流程中。这里,为了将参数应用到流程中,就需要提前创建一些对象,比如ChannelInitializer,但是使用是在未来使用这些对象。而解决这个流程,显然使用回调的方式。
          2.3 题外话,从4.0版本到4.1版本中,通过Netty处理回调的方式,不难发现,**优雅的代码都是不断通过重构演化而来。回调作为一个单独的过程,而不再是依附于channelRegister。
          Netty4.0:在这里插入图片描述
          在这里插入图片描述

          Netty4.1:
          在这里插入图片描述
          在这里插入图片描述

        3. 协调异步操作。safeSetSuccess(promise);以NioServerSocketChannel注册为例分析。
          main线程和boss线程共同操作DefaultChannelPromise对象,不同的是main线程是读取被boss线程更新后的共享状态变量result,因此,你会发现result用volatile修饰。
          这里是怎么协调的?
          3.1 我们知道,Netty创建任务跟执行任务大多是异步的,即在不同的线程环境中。
          3.2 Nio编程大体流程是,先将Channel注册到Selector上,然后再进行Channel的端口绑定。
          3.3 当main线程向bossGrope的loop实例中新增一个任务后,如果boss组的该实例没有线程,就开始启动一个线程,由新启动的线程执行该任务。此时,main线程会继续往下执行。
          由于线程不一致,会出现以下典型的case:
          i :表示if (channelFuture.isDone())
          s:表示safeSetSuccess(promise)
          a : 表示addListener中的isDone()
          之后解释时,都用标识代替。
          case 1: 如果新线程启动很快且在main线程执行到i之前,新线程已经开始执行到register0,且执行完s。此时,main线程开始执行i,结果为true(已经被新线程通过s给共享变量result赋值完),直接doBind0
          case 2: 如果新线程启动慢或者其他情况导致,并没有执行到s,此时,main线程开始执行i,结果为false,此时就需要向共享对象regFuture添加操作完成监听器,以便新线程通过s能够通知到main线程,添加绑定端口任务。
          case 3: 假如新线程没执行到s,此时main线程执行到i,main线程开始走添加操作完成监听器。假设,这时,新线程执行完s但此时的main线程还没向共享对象regFuture=DefaultChannelPromise添加监听器。换句话说,新线程执行完s也并没通知到main线程,因为此时的共享对象中的listeners并没有由main触发添加的监听器。所以,在addListener中,还需要一个额外的操作a来完成自己触发操作完成的动作。

        4. 注册事件

二:执行main线程所创建的boss组能接收客户端连接的对象任务
当第一阶段的任务执行完后,执行创建接收客户端连接的对象任务
三:当二中的任务执行完后,执行channel绑定端口任务。
在这一阶段,实际绑定完端口后,创建能将注册Channel时的操作位由0变更为16的任务。

	@Override
    public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
        assertEventLoop();

        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }

        // See: https://github.com/netty/netty/issues/576
        if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) &&
            localAddress instanceof InetSocketAddress &&
            !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() &&
            !PlatformDependent.isWindows() && !PlatformDependent.maybeSuperUser()) {
            // Warn a user about the fact that a non-root user can't receive a
            // broadcast packet on *nix if the socket is bound on non-wildcard address.
            logger.warn(
                    "A non-root user can't receive a broadcast packet if the socket " +
                    "is not bound to a wildcard address; binding to a non-wildcard " +
                    "address (" + localAddress + ") anyway as requested.");
        }

        boolean wasActive = isActive();
        try {
            doBind(localAddress);
        } catch (Throwable t) {
            safeSetFailure(promise, t);
            closeIfClosed();
            return;
        }

        if (!wasActive && isActive()) {
            invokeLater(new Runnable() {
                @Override
                public void run() {
                	// 触发操作位变更
                    pipeline.fireChannelActive();
                }
            });
        }

        safeSetSuccess(promise);
    }

四:执行变更channel操作位任务
当执行完操作位变更的任务后,此时服务端具备了能接收客户端连接请求,TCP通道的建立。

JDK原生的NIO编程

Netty在原生NIO编程的改进

  • 线程与服务类(NioEventLoop)分开。这里说的分开指的是,NioEventLoop本身并不是一个线程(依据就是没有extend Thread),但是它持有线程。
    • 这种设计方式,比之前单纯继承一个Thread,稍微要复杂点。因为每次使用该实例时,都要判断当前线程是否是该实例所持有的线程。 而且,还要保证实例中的操作都是顺序的,不能受线程的影响,为此,Netty为每个实例增加了一个任务队列,所有顺序的操作转变成任务队列以及线程间的通信(共享变量【状态变量】,一个写,一个读,事件通知等)。
    • 那么,这种设计方式有什么好处?将线程的作用域缩小到操作动作,方法级别上,而不再是整个实例类,充分利用多核CPU。
  • Netty对于我们来说,之所以使用简单,是因为内部统一了对象。客户端套接字的编程跟服务端套接字的编程做了抽象统一,比如AbstractNioChannel,因此,我们会发现,服务端编程和客户端编程基本相同。
    • 为了实现这种统一,Netty都做了哪些事情?
      • 套接字统一。我们知道jdk提供了ServerSocket和Socket这两种套接字,与之对应的ServerSocketChannel和SocketChannel,这是两套体系,即客户端编程和服务端编程。而Netty通过AbstractNioChannel的SelectableChannel将ServerSocketChannel和SocketChannel做到了有机统一,套接字模型统一。更重要的是,这里说明“少用继承,多用组合”的原则很重要,让功能更强大,这里的功能是面向使用人员,在开发服务端或者客户端编程时,十分简单,即传参就可以。
      • 流程统一。NioServerSocketChannel注册到Selector上跟NioSocketChannel注册到Selector上的过程一样,Netty将这个过程进行了统一,即AbstractUnsafe中的register0。(题外话,有些小伙伴一直纠结方法命名的问题,这里说一下。在doRegister()的上一级方法的命名是register0,而register0方法的上一级是register方法。
      • 操作即任务,任务统一。Netty将所有的操作或者动作转换成可执行的任务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值