编者注:本文为历史博文归档;涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效,阅读时请注意时效性。

如何正确使用 NIO 构建网络服务器一直是值得深入探讨的问题。为此,我们分析了 Jetty、Tomcat 和 Mina 有关 NIO 的源码,发现三者基于类似的方式实现。这应该算是 NIO 构架网络服务器的经典模式。基于这种模式,我们编写了一个小型网络服务器,经过压力测试,效果良好。下文将具体分析这三者是如何使用 NIO 的。

Jetty Connector 的实现

先看看有关类图:

其中各组件职责如下:

  • SelectChannelConnector:负责组装各组件
  • SelectSet:负责侦听客户端请求
  • SelectChannelEndPoint:负责 IO 的读和写
  • HttpConnection:负责逻辑处理

在整个服务端处理请求的过程可以分为三个阶段,时序图如下所示:

阶段一:监听并建立连接

这一过程主要是启动一个线程负责 accept 新连接,监听到后分配给相应的 SelectSet,分配的策略就是轮询。

阶段二:监听客户端的请求

这一过程主要是启动多个线程(线程数一般为服务器 CPU 的个数),让 SelectSet 监听所管辖的 channel 队列。每个 SelectSet 维护一个 Selector,这个 Selector 监听队列里所有的 channel,一旦有读事件,从线程池里拿线程去做处理请求。

阶段三:处理请求

这一过程就是每次客户端请求的数据处理过程。值得注意的是,为了不让后端的业务处理阻碍 Selector 监听新的请求,采用多线程来分隔开监听请求和处理请求两个阶段。

由此可以大致总结出 Jetty 有关 NIO 使用的模式,如下图所示:

最核心的设计就是把三件不同的事情隔离开,并用不同规模的线程去处理,最大限度地利用 NIO 的异步和通知特性。

Tomcat Connector 的实现

下面再来看看 Tomcat 是如何使用 NIO 来构架 Connector 这块的。先看看 Tomcat Connector 这块的类图:

其中各组件职责如下:

  • NioEndpoint:负责组装各部件
  • Acceptor:负责监听新连接,并把连接交给 Poller
  • Poller:负责监听所管辖的 channel 队列,并把请求交给 SocketProcessor 处理
  • SocketProcessor:负责数据处理,并把请求传递给后端业务处理模块

在整个服务端处理请求的过程可以分为三个阶段,时序图如下所示:

阶段一:监听并建立连接

这一阶段主要是 Acceptor 监听新连接,并轮询取一个 Poller,把连接交付给 Poller。

阶段二:监听客户端的请求

这一过程主要是让每个 Poller 监听所管辖的 channel 队列,select 到新请求后交付给 SocketProcessor 处理。

阶段三:处理请求

这一过程就是从多线程执行 SocketProcessor,做数据和业务处理。

于是我们发现,抛开具体代码细节,Tomcat 和 Jetty 在 NIO 的使用方面是非常一致的,采用的模式依然是下图:

Mina 的实现

最后我们再看看 NIO 方面最著名的框架 Mina。抛开 Mina 有关 session 和处理链条等方面的设计,单单挑出前端网络层处理来看,也采用的是与 Jetty 和 Tomcat 类似的模式。只不过它做了些简化,没有隔开请求侦听和请求处理两个阶段,因此宏观上看它只分为两个阶段。

先看看它的类图:

其中各组件职责如下:

  • SocketAcceptor:起线程调用 SocketAcceptor.Work 负责新连接侦听,并交给 SocketIoProcessor 处理
  • SocketIoProcessor:起线程调用 SocketIoProcessor.Work 负责侦听所管辖的 channel 队列,select 到新请求后交给 IoFilterChain 处理
  • IoFilterChain:组装了 Mina 的处理链条

在整个服务端处理请求的过程可以分为两个阶段,时序图如下所示:

阶段一:监听并建立连接

阶段二:监听并处理客户端的请求

总结与实践

总结来看 Jetty、Tomcat 和 Mina,我们大概清楚了该如何基于 NIO 来构架网络服务器。通过这个提炼出来的模式,我们写了个很简单的 NIO Server。在保持连接的情况下,可以很轻松地保持 6 万连接(由于有 65535 连接限制),并能在负载只有 3 左右的情况下(4 核),承担 3 到 4 万的 TPS 请求(当然做的事情很简单,仅仅是把 buffer 转化为自定义协议的包,然后再把包转为 buffer 写到客户端)。

简单地实践一下可以证明这个模式的有效性。不妨再看看这个图,希望对大伙以后写 server 有用:


说明:本文内容基于 2011 年左右的技术背景整理(参考图片链接时间戳),文中涉及的 Jetty、Tomcat 及 Mina 版本架构可能已与当前最新版本存在差异。现代 NIO 框架(如 Netty)及 JDK NIO.2 已有更多演进,实际开发请以官方最新文档为准。