一起来分解一个Netty应用

​前言

前面几篇博客主要介绍到了NIO针对网络IO场景相比较传统的Socket通信的优势,以及NIO在应用过程中线程模型的演化。从这篇博客开始我们一起来学习一个基于NIO实现的框架Netty,这是一个目前应用非常广泛的通信框架。Netty所使用的线程模型就是我们上一篇博客提到的主从Reactor模型。那么为什么在JDK中已经集成了NIO之后,还需要一个二次封装的Netty。这是因为NIO的使用门槛比较高,易用性比较低。具体可以参考我GIT上面NIO各种模式的Demo,可以看出实现一个通信模型的工作量还是比较大的,原生的NIO对用户来讲实现负担较重。Netty的出现就是为了帮助用户更多的专注在自己的业务逻辑上,降低实现成本。

介绍

我们结合一个简单的Demo来熟悉下Netty的主要API以及这些API背后的一些简单的架构设计理念。在这个过程中,我们会将Netty中的主要接口与之前的博客结合起来,看看Netty中的API是怎么与NIO的主从Reactor模式对应起来的。首先我们来一起看下一个Netty的Server端主要代码。

//主reactorEventLoopGroup acceptor = new NioEventLoopGroup();//从reactorEventLoopGroup worker =new NioEventLoopGroup();try {     ServerBootstrap bootstrap = new ServerBootstrap();     bootstrap.group(acceptor, worker);       bootstrap.option(ChannelOption.SO_BACKLOG, 1024);     bootstrap.channel(NioServerSocketChannel.class);     //主ractor处理器注册     bootstrap.handler(new LoggingHandler(LogLevel.INFO))               //从reactor处理器注册               .childHandler(newChannelInitializer<SocketChannel>() {                    @Override                    public void initChannel(SocketChannel ch) throws Exception {                        ch.pipeline().addLast(new DecodeHandler())                        .addLast(new ProcessHandler())                        .addLast(new DumpHandler());                    }                  });     ChannelFuture f = bootstrap.bind(port).sync();     f.channel().closeFuture().sync();} catch (InterruptedException e) {     e.printStackTrace();} finally {     acceptor.shutdownGracefully();     worker.shutdownGracefully();}

上面这段程序就是Netty Server的启动逻辑。基本的过程就是设置了一些Server的参数,设置事件监听对象,用户请求的处理逻辑等等。可以看到,整个实现过程很简洁,不需要关心任何关于NIO的细节。下面让我们一起来看下,每个接口都为我们做了哪些工作。

ServerBootstrap

ServerBootstrap从字面翻译过来是服务器引导程序的意思。这个类是Server端的主要类,可以看到关于服务端的所有配置和逻辑都是通过该类进行设置或者注册的。可以认为所有服务端的实现和配置都被包裹在ServerBootstrap之内。我们开发服务端代码的主要目的就是构建该对象,利用接口去配置相关的参数,向其中去注册我们的处理逻辑,最后启动ServerBootstrap即可。

EventLoopGroup

EventLoopGroup与我们前面博客中提到的Reactor是相对应的,一个EventLoopGroup就是对应着一个Reactor对象。从Demo中可以看到定义了两个group对象,acceptor和worker。这两个group分别对应着我们的主从reactor。回顾下我们上一篇博客中的内容,不难想到,这两个group都包含了可以独立运行的线程,每个group中都封装了NIO的Selector对象并运行独立的事件监听线程。其中acceptor负责客户端的连接请求事件,worker负责数据读取和发送事件。事实上也确实如此,EventLoopGroup的类继承关系如下:

(该图像摘自:https://www.cnblogs.com/duanxz/p/3724395.html)

从继承关系上可以看出每个group都是一个独立的线程池实现(该类的实现细节,后续在Netty源码分析的系列博客中还会进行详细介绍)。此处我们希望联系前面博客中的主从Reactor模型的验证Netty的实现与前期的理解是一致的。

ChannelHandler

有了事件分发器,接下来就是具体的事件处理逻辑。事件的处理逻辑跟我们的业务密切相关,每个业务都有其独特性,显然这部分逻辑不可能由框架来实现。框架需要做的就是提供给用户自定义的口子。这个口子就是ChannelHandler。ChannelHandler是框架提供的一个处理器基类,它定义了Event处理的接口标准,用户通过override相应的接口来实现自己的处理逻辑。这也是大部分框架进行业务逻辑解耦的一种惯用方式。用户自定义实现了Handler之后只需要将自己的Handler对象注册到Bootstrap中即可。

值得注意的是,我们的框架分为了主从Reactor来对不同的事件进行分发处理,自然Handler的注册也分为了两类。因此在Demo代码中可以看出,Handler的注册也是分两个不同的接口进行注册的。Handler接口注册的主Reactor的处理器,即负责处理accept事件,childHandler接口注册的是从Reactor的处理器,即负责处理read,write事件。

Pipeline的设计理念

通过上面几个核心API的串联,我们基本上完成了整个服务端的处理过程梳理。从服务端参数的配置,事件的监听分发和用户自定义处理器的添加,这些基本上是一个完整的通信程序的所有要素。那么为什么还要设计一个Pipeline那。

回答是功能复用和逻辑解耦。Pipeline的字面意思是管道,我更喜欢翻译成流水线。流水线是一种生产作业方式,我们所有的生产处理过程都可以通过组合不同的基础处理能力,以流水作业的方式来实施。而我们这里的Pipeline起到的作用就是这个。我们每个业务的处理逻辑都是以流水线的方式,串联一组handler组合形成的。框架为我们定义和实现了不同handler之间进行数据传输和流转的的标准。这种方式方便业务逻辑进行灵活的逻辑扩展,不需要把所有的逻辑堆砌在某一个handler中。具体来说好处有以下两点。

第一个好处是功能复用,虽然说每个业务的处理逻辑都是不一样的,有其独特的地方。但是差异中也是能发掘到共同点的,这些共同点就可以作为公用的Handler沉淀下来,其他业务在开发时就可以拿来即用(串接到Pipeline中)。甚至有些能够形成业界统一标准的通用处理逻辑可以集成到Netty框架中供所有人使用,这就所谓的功能复用。

第二个好处是逻辑解耦。在架构整洁之道一书中提到,架构设计本身就是一门划分边界的艺术。是的,我们常常称一个低耦合的软件架构为优秀的设计,而解耦的前提就是进行边界划分。将软件架构中不容易变化的部分和容易变化的部分的边界分析出来,然后再通过一个通用的轻量的标准纽带联系起来就能完成解耦。通过Pipeline用户就能够轻松的进行这种边界切割和逻辑解耦。当然,如果没有Pipeline,用户也能够自行实现一个解耦业务逻辑。但是在Pipeline的基础上,这种实现能够做到事半功倍。更重要的是这种实现模式向更多的人推广了边界划分的设计理念,增强了他们主动进行功能单元划分的意识。甚至还会长远的影响到一部分人的软件设计理念。

         Netty客户端的实现这里就不介绍了,基本上跟服务端的套路差不多。

后话

搭建一个简单的Netty应用并不难,网络上也大量的Demo程序可以参考。但是我写这一篇博客的初衷更多的还是希望能够将Netty中的API与我们前面博客中提到的NIO的概念和线程模型对应起来,从而形成上下连贯。

另外Pipeline这种插件式架构的设计理念也是值得我们在自行设计的软件中去落地实践的。最后向大家推荐一本上面提到的书:架构整洁之道。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章