乐趣区

关于后端:谈谈Netty线程模型

大家好,我是易安!

Netty 是一个高性能网络应用框架,利用十分广泛,目前在 Java 畛域里,Netty 基本上成为网络程序的标配了。Netty 框架功能丰富,也非常复杂,明天咱们次要剖析 Netty 框架中的线程模型,而 线程模型间接影响着网络程序的性能

网络编程性能的瓶颈

BIO 模型里,所有 read()操作和 write()操作都会阻塞以后线程的,如果客户端曾经和服务端建设了一个连贯,而迟迟不发送数据,那么服务端的 read()操作会始终阻塞,所以 应用 BIO 模型,个别都会为每个 socket 调配一个独立的线程,这样就不会因为线程阻塞在一个 socket 上而影响对其余 socket 的读写。BIO 的线程模型如下图所示,每一个 socket 都对应一个独立的线程;为了防止频繁创立、耗费线程,能够采纳线程池,然而 socket 和线程之间的对应关系并不会变动。

BIO 的线程模型

BIO 这种线程模型实用于 socket 连贯不是很多的场景;然而当初的互联网场景,往往须要服务器可能撑持十万甚至百万连贯,而创立十万甚至上百万个线程显然并不事实,所以 BIO 线程模型无奈解决百万连贯的问题。如果仔细观察,你会发现互联网场景中,尽管连贯多,然而每个连贯上的申请并不频繁,所以线程大部分工夫都在期待 I / O 就绪。也就是说线程大部分工夫都阻塞在那里,这齐全是节约,如果咱们可能解决这个问题,那就不须要这么多线程了。

顺着这个思路,咱们能够将线程模型优化为下图这个样子,能够用一个线程来解决多个连贯,这样线程的利用率就上来了,同时所需的线程数量也跟着降下来了。这个思路很好,可是应用 BIO 相干的 API 是无奈实现的,这是为什么呢?因为 BIO 相干的 socket 读写操作都是阻塞式的,而一旦调用了阻塞式 API,在 I / O 就绪前,调用线程会始终阻塞,也就无奈解决其余的 socket 连贯了。

现实的线程模型图

好在 Java 里还提供了非阻塞式(NIO)API,利用非阻塞式 API 就可能实现一个线程解决多个连贯了 。那具体如何实现呢?当初广泛都是 采纳 Reactor 模式,包含 Netty 的实现。所以,要想了解 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()。

Reactor 模式类结构图

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 线程

一个网络连接对应到一个 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。

用 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 的线程模型设计得很精美,每个网络连接都关联到了一个线程上,这样做的益处是:对于一个网络连接,读写操作都是单线程执行的,从而防止了并发程序的各种问题。

本文由 mdnice 多平台公布

退出移动版