Netty源码分析之一感性认识

本贴最后更新于 3038 天前,其中的信息可能已经时移世改

基于Netty4.0的框架源码分析,侧重于框架设计技巧和原理解析;以Example模块中的一个获取世界时钟例子的源码开始,逐步分析服务端与客户端的工作流程,在这个过程中,让我们先对Netty框架有个感性认识。

什么是Netty

  • 提供异步的,事件驱动的网络应用程序开发框架和工具
  • 可以开发高性能和高可靠性的网络服务器和客户端应用程序
  • 提供丰富的协议编解码支持
  • 自定义buffer系统,减少复制带来的消耗
  • 整套channel的实现,channel是统一的异步I/O编程接口,抽象了所有点对点的通信工作

Netty4.0项目结构 

模块                           描述


netty                          project parent

common                   utility and logging

buffer                        buffer API

transport                   channel API and its core implementations

transport-rtrx           RTRX transport implementation

transport-sctp          SCTP transport implementation

transport-udt           UDT transport implementation

handler                     channel handlers

codec                        codec framework

codec-http                HTTP, Web Sockets, SPDY, and RTSP codec

codec-socks             Socks codec

example                   examples

all                              generates an all-in-one JAR

tarball                       generates a tarball distribution


Netty框架核心

  • BUFFER
  • CHANNEL       
  • EVENT MODEL

参考 

服务端启动过程

该实例为一个世界时钟的服务端启动过程,从他的主类WorldClockServer.java源码看起,该代码可以看作是启动服务端或者客户端的一个模板代码。

public void run() throws Exception {
        //线程数如果为空时,默认为8
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new WorldClockServerInitializer());
        b.bind(port).sync().channel().closeFuture().sync();
    } finally {
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }

}

多线程的任务调度器

JDK自1.5之后,增加了executor接口,给多线程的使用提供很大的便利空间,Netty框架也是基于executor设计了非常稳定的线程模型。任何实现了Executor接口的类即是一个多线程的任务调度器,当然,这个任务是在新线程中执行,还是在线程池,或者是在调用者的当前线程中执行,是由具体实现类来决定的。

在整个启动过程中,主要思路就是将channel在多线程任务调度器中进行注册,然后给该channel绑定端口以及增加监听器;绑定端口、注册以及增加监听器等这些任务是分别交由任务调度器统一完成的。
 
NioEventLoopGroup就是这样一个任务调度器,它实现了Executor接口。(主要功能是异步处理在其上注册过的Channel的所有I/O操作)
 
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

上面两行代码分别新建了2个任务调度器,一个用来执行boss任务,一个用来执行worker任务。注意:只是建立了任务调度器,但是并没有执行任务;其内部实现其实是建立线程任务处理器Executor,然后再建立单线程的事件处理器数组,其中存放的是具体实现子类NioEventLoop。

启动管理器

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
 .childHandler(new WorldClockServerInitializer());

ServerBootstrap是一个启动管理器,主要职责是将服务端启动时需要的各种资源进行装配和注册,并且可以启动服务,停止服务,释放资源等等相关事项。以上代码首先使用group方法将建立的2个任务调度器注册到启动管理器中,然后利用channel方法注册NioServerSocketChannel这个资源---这个资源可以看作就是一根管子,为基于socket的网络通信提供的一种载体。最后注册WorldClockServerInitializer这个ChannelHandler,通过childHandler方法,ChannelHandler就是用来处理在Channel上发生的事件的,比如像open,close,connect等等事件。

绑定端口及其他

以上就是将所有需要的资源在启动管理器中进行了注册,下面就可以开始启动进行一系列的初始化工作,并且启动服务了。

b.bind(port).sync().channel().closeFuture().sync();

首先看bind方法的逻辑。

  1. 先初始化以及开启NioServerSocketChannel
  2. 在任务调度器NioEventLoop(该调度器属于调度器组NioEventLoopGroup)中注册NioServerSocketChannel
  3. 将注册的NioServerSocketChannel绑定端口以及绑定监听器

也就是说这个任务调度器,对于NioServerSocketChannel这个管子中发生的所有的事件,都会进行处理,当然是以重新开启一个新线程的方式进行处理;我们看看bind方法的部分代码逻辑---调用了channel对应任务调度器的execute方法来处理该任务,该任务其实就是上面的逻辑3。

private static void doBind0(
    final ChannelFuture regFuture, final Channel channel,
    final SocketAddress localAddress, final ChannelPromise promise) {
 // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
 // the pipeline in its channelRegistered() implementation.
     
    channel.eventLoop().execute(new Runnable() {
    //在将来会执行该方法,是在一个新线程,还是在线程池里面取线程,或者是在当前调用线程执行,这个有具体实现来负责。
    @Override
    public void run() {
      if (regFuture.isSuccess()) {
         channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
      } else {
          promise.setFailure(regFuture.cause());
      }
    }
    });}

在回到原来的主线程执行逻辑中,继续sync以及其后续的几个方法,他们主要功能是等待future彻底完成,否则当前主线程会一直处于阻塞状态,当future彻底完成后,执行finally中的任务调度器清理逻辑,至此,主线程所有逻辑完成。那么在任务调度器中的所有任务,会被不停的执行下去,直到所有的任务都完成为止。

总结下过程,从线程角度,如下:

  • 随着主线程的开启,第一个交给任务管理器的任务就是注册channel(该过程其实就已经打开一个channel,但是没有绑定端口)
  • 第二个交给任务管理器的任务是给新打开的channel绑定端口和监听器,这2个任务都会被添加到任务队列taskQueue中去,在加入第一个任务的时候由任务调度器开启新线程来执行taskQueue中所有的任务队列,在执行每个任务的时候,也是单独开启新线程执行
  • 在该过程中,有逻辑调用任务管理器执行时,就将新任务加入到任务队列中即可,不在开启新线程;此时,主线程继续执行,直到完成
  • 在执行taskQueue的新线程中,会逐个执行该队列中所有的任务,直到全部完成

客户端启动过程

与服务端类似,首先在WorldClockClient中新建一个启动管理器Bootstrap对象,然后在该启动管理器中注册一个多线程的任务调度器(该任务调度器与服务端启动过程中的线程任务调度器是同一个类),同时将连接服务端的channel注册到该任务调度器中,然后将handler注册到channel上来处理各种channel上的操作;我们知道在Netty中,channel的操作不同,就有不同的handler与其对应。

channel的注册动作(与服务端的注册逻辑类似),也是交由线程任务调度器完成的;如果该channel中的所有的异步I/O都完成的话,就开始连接服务端。

开始调用channel上的pipeline进行服务端的连接操作,其实质也是交由线程任务调度器去完成该任务(即加入同一个任务队列中)。

连接服务端操作完成后,关闭连接及其他对象,主线程结束。

服务端启动序列图如下:

netty

  • Netty

    Netty 是一个基于 NIO 的客户端-服务器编程框架,使用 Netty 可以让你快速、简单地开发出一个可维护、高性能的网络应用,例如实现了某种协议的客户、服务端应用。

    49 引用 • 33 回帖 • 24 关注
  • channel
    2 引用
  • 任务
    2 引用 • 1 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...