初步探索高性能网络应用框架Netty

Netty 是一个高性能的网络框架,应用很是广泛,目前在Java 领域,Netty 基本上成为网络程序的标配了,Netty 框架功能丰富,也很是复杂。今天主要分析Netty 框架中的线程模型,而线程模型直接影响着网络程序的性能。算法

在介绍Netty 的线程模型以前,咱们首先搞清楚网络编程性能的瓶颈在哪里,而后再看Netty 的线程模型是如何解决这个问题的。编程

网络编程性能的瓶颈

传统的BIO 编程模型里, 全部的read() 操做和 write() 操做都会阻塞当前线程的, 若是客户端和服务端已经创建了一个链接,而迟迟不发送数据,那么服务端的 read() 操做会一直阻塞, 因此使用BIO 模型, 通常都会为每一个socket 分配一个独立的线程,这样就不会由于线程阻塞在一个socket 上而影响对其余socket 的读写。服务器

BIO 的线程模型以下图所示:每一个socket 对应一个独立的线程。为了不频繁建立消耗线程,能够采用线程池,可是socket 和线程之间的对应关系不会变化。网络

初步探索高性能网络应用框架Netty

BIO 这种线程模型,适用于socket 链接不是不少的场景。可是如今的互联网场景,每每须要服务器可以支撑十万甚至百万链接,而建立十万甚至百万链接显然不现实,因此BIO 线程模型没法解决百万链接的问题。若是仔细观察,你会发现互联网场景中,虽然链接不少,可是每一个链接的请求并不频繁,因此线程大部分时间都在等待I/O 就绪,也就是说线程大部分时间都阻塞在那里,这彻底是浪费,若是咱们可以解决这个问题,那就不须要这么多线程了。多线程

顺着这个思路,咱们能够将线程的模型优化为下图这个样子,用一个线程来处理多个链接,这样利用率就上来了,同时所须要的线程数量也降下来了。但是使用 BIO 相关的 API 是没法实现的, 为何呢?由于 BIO 相关的 socket 读写操做都是阻塞式的,而一旦调用了阻塞式 API,在 I/O 就绪前,调用线程会一直阻塞,也就没法处理其余的 socket 链接了。并发

初步探索高性能网络应用框架Netty

好在 Java 里还提供了非阻塞式(NIO)API, 利用非阻塞API 就可以实现一个线程处理多个链接了。 那具体如何实现呢?如今广泛采用的都是Reactor 模式, 包括Netty 的实现,因此先让咱们了解如下 Reactor 模式。负载均衡

Reactor 模式

下面是 Reactor 模式的类结构图,其中 Handle 指的是 I/O 句柄,在 Java 网络编程里,它本质上就是一个网络链接。Event Handler 很容易理解,就是一个事件处理器,其中 handle_event() 方法处理 I/O 事件,也就是每一个 Event Handler 处理一个 I/O Handle;get_handle() 方法能够返回这个 I/O 的 Handle。Synchronous Event Demultiplexer 能够理解为操做系统提供的 I/O 多路复用 API,例如 POSIX 标准里的 select() 以及 Linux 里面的 epoll()。框架

初步探索高性能网络应用框架Netty

Reactor 模式的核心天然是 Reactor 这个类,其中 register_handler() 和 remove_handler() 这两个方法能够注册和删除一个事件处理器;handle_events() 方式是核心,也是 Reactor 模式的发动机,这个方法的核心逻辑以下:首先经过同步事件多路选择器提供的 select() 方法监听网络事件,当有网络事件就绪后,就遍历事件处理器来处理该网络事件。因为网络事件是源源不断的,因此在主程序中启动 Reactor 模式,须要以 while(true){} 的方式调用 handle_events() 方法。异步

void Reactor::handle_events(){
  //经过同步事件多路选择器提供的
  //select()方法监听网络事件
  select(handlers);
  //处理网络事件
  for(h in handlers){
    h.handle_event();
  }
}
// 在主程序中启动事件循环
while (true) {
  handle_events();

Netty 中的线程模型

Netty 的实现虽然参考了 Reactor 模式,可是并无彻底照搬,Netty 中最核心的概念是事件循环(EventLoop),其实也就是 Reactor 模式中的 Reactor,负责监听网络事件并调用事件处理器进行处理。在 4.x 版本的 Netty 中,网络链接和 EventLoop 是稳定的多对 1 关系,而 EventLoop 和 Java 线程是 1 对 1 关系,这里的稳定指的是关系一旦肯定就再也不发生变化。也就是说一个网络链接只会对应惟一的一个 EventLoop,而一个 EventLoop 也只会对应到一个 Java 线程,因此一个网络链接只会对应到一个 Java 线程。socket

一个网络链接对应到一个 Java 线程上,有什么好处呢?最大的好处就是对于一个网络链接的事件处理是单线程的,这样就避免了各类并发问题。

Netty 中的线程模型能够参考下图,这个图和前面咱们提到的理想的线程模型图很是类似,核心目标都是用一个线程处理多个网络链接。

初步探索高性能网络应用框架Netty

Netty 中还有一个核心概念是 EventLoopGroup,顾名思义,一个 EventLoopGroup 由一组 EventLoop 组成。实际使用中,通常都会建立两个 EventLoopGroup,一个称为 bossGroup,一个称为 workerGroup。为何会有两个 EventLoopGroup 呢?

这个和 socket 处理网络请求的机制有关,socket 处理 TCP 网络链接请求,是在一个独立的 socket 中,每当有一个 TCP 链接成功创建,都会建立一个新的 socket,以后对 TCP 链接的读写都是由新建立处理的 socket 完成的。也就是说处理 TCP 链接请求和读写请求是经过两个不一样的 socket 完成的。上面咱们在讨论网络请求的时候,为了简化模型,只是讨论了读写请求,而没有讨论链接请求。

在 Netty 中,bossGroup 就用来处理链接请求的,而 workerGroup 是用来处理读写请求的。bossGroup 处理完链接请求后,会将这个链接提交给 workerGroup 来处理, workerGroup 里面有多个 EventLoop,那新的链接会交给哪一个 EventLoop 来处理呢?这就须要一个负载均衡算法,Netty 中目前使用的是轮询算法。

用 Netty 实现 Echo 程序服务端

下面的示例代码基于 Netty 实现了 echo 程序服务端:首先建立了一个事件处理器(等同于 Reactor 模式中的事件处理器),而后建立了 bossGroup 和 workerGroup,再以后建立并初始化了 ServerBootstrap,代码仍是很简单的,不过有两个地方须要注意一下。

第一个,若是 NettybossGroup 只监听一个端口,那 bossGroup 只须要 1 个 EventLoop 就能够了,多了纯属浪费。

第二个,默认状况下,Netty 会建立“2*CPU 核数”个 EventLoop,因为网络链接与 EventLoop 有稳定的关系,因此事件处理器在处理网络事件的时候是不能有阻塞操做的,不然很容易致使请求大面积超时。若是实在没法避免使用阻塞操做,那能够经过线程池来异步处理。

//事件处理器
final EchoServerHandler serverHandler 
  = new EchoServerHandler();
//boss线程组  
EventLoopGroup bossGroup 
  = new NioEventLoopGroup(1); 
//worker线程组  
EventLoopGroup workerGroup 
  = new NioEventLoopGroup();
try {
  ServerBootstrap b = new ServerBootstrap();
  b.group(bossGroup, workerGroup)
   .channel(NioServerSocketChannel.class)
   .childHandler(new ChannelInitializer<SocketChannel>() {
     @Override
     public void initChannel(SocketChannel ch){
       ch.pipeline().addLast(serverHandler);
     }
    });
  //bind服务端端口  
  ChannelFuture f = b.bind(9090).sync();
  f.channel().closeFuture().sync();
} finally {
  //终止工做线程组
  workerGroup.shutdownGracefully();
  //终止boss线程组
  bossGroup.shutdownGracefully();
}

//socket链接处理器
class EchoServerHandler extends 
    ChannelInboundHandlerAdapter {
  //处理读事件  
  @Override
  public void channelRead(
    ChannelHandlerContext ctx, Object msg){
      ctx.write(msg);
  }
  //处理读完成事件
  @Override
  public void channelReadComplete(
    ChannelHandlerContext ctx){
      ctx.flush();
  }
  //处理异常事件
  @Override
  public void exceptionCaught(
    ChannelHandlerContext ctx,  Throwable cause) {
      cause.printStackTrace();
      ctx.close();
  }
}

总结

Netty 是一个款优秀的网络编程框架,性能很是好,为了实现高性能的目标,Netty 作了不少优化,例如优化了 ByteBuffer、支持零拷贝等等,和并发编程相关的就是它的线程模型了。Netty 的线程模型设计得很精巧,每一个网络链接都关联到了一个线程上,这样作的好处是:对于一个网络链接,读写操做都是单线程执行的,从而避免了并发程序的各类问题。

相关文章
相关标签/搜索