从Jetty、Tomcat和Mina中提炼NIO构架网络服务器的经典模式
编者注:本文为历史博文归档;涉及 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 已有更多演进,实际开发请以官方最新文档为准。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。