如果大家觉得文章有错误内容,欢迎留言或者私信讨论~
Jetty 同样也是一个 “HTTP 服务器 + Servlet 容器”,Jetty 在架构设计上和 Tomcat 有很多相似之处,但是他更小巧、更容易定制化。Jetty 作为后起之秀,应用范围也越来越广,比如 Google App Engine 就采用了 Jetty。今天我们就通过聊聊 Jetty 与 Tomcat 的不同之处,加深对 Web 容器架构设计的理解,另一方面更清楚它们的设计区别。
Jetty 的架构是由多个 Connector、Handler 以及一个线程池组成的,如下图:
跟 tomcat 一样,Jetty 也有 HTTP 服务器和 Servlet 容器的功能,因此 Jetty 中的 Connector 组件和 Handler 组件分别来实现这两个功能,而这两个组件工作时所需要的线程资源都直接从一个全局线程池 ThreadPool 中获取。
Jetty 的 Server 可以有多个 Connector 在不同的端口监听,然后对于请求处理的 Handler 组件,则可以根据不同的场景使用不同的 Handler。比如需要支持 Session,则再增加一个 SessionHandler 即可。
为了启动和协调上面的核心组件工作,Jetty 提供了一个 Server 类来做这个事情,它负责创建并初始化 Connector、Handler、ThreadPool 组件,然后调用 start 方法启动它们。
我们对比一下 Tomcat 的整体架构图,你会发现 Tomcat 跟 Jetty 很相似,它们的第一个区别是 Jetty 中没有 Service 的概念,Tomcat 中的 Service 包装了多个连接器和一个容器组件,一个 Tomcat 实例可以配置多个 Service,不同的 Service 通过不同的连接器监听不同的端口;而 Jetty 中 Connector 是被所有 Handler 共享的。
第二个区别是,Tomcat 每个连接器都有自己的线程池,而 Jetty 中的 Connector 共享一个全局的线程池。
跟 Tomcat 一样,Connector 的主要功能就是对 I/O 模型和应用协议的封装。I/O 模型方面,最新的 Jetty9 只支持 NIo,因此 Jetty 的 Connector 设计有明显的 Java NIO 通信模型的痕迹。至于应用层协议方面,跟 Tomcat 的 Processor 一样,Jetty 抽象出了 Connection 组件来封装应用层协议的差异。
大部分的开发同学接触的 IO 开发应该不多(毕竟好多都是现成已经封装好的,我们只要做 API 调用战士就好)。让我们先来回顾一下。Java NIO 的核心组件是 Channel、Buffer 和 Selector。Channel 表示一个连接,可以理解为一个 Socket,通过它可以读取和写入数据,但是并不能直接操作数据,需要通过 Buffer 来中转。
Selector 可以检测 Channel 上的 I/O 事件,比如读就绪、写就绪,一个 Selector 可以处理多个 Channel,因此单线程就可以监听多个 Channel,这样可以减少线程上下文切换的开销。
让我们一起写一个经典的例子:
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(port));
server.configureBlocking(false);
然后,创建 Selector,并在 Selector 中注册 Channel 感兴趣的事件 OP_ACCEPT,告诉 Selector 如果客户端有新的连接请求到这个端口就通知我。
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
接下来 Selector 会在一个死循环里不断地调用 select 去查询 I/O 状态,select 会返回一个 SelectionKey 列表,Selector 会遍历这个列表,看看是否有“客户”感兴趣的事件,如果有,就采取相应的动作。比如下面这个例子:
while (true) {
selector.select();//查询I/O事件
for (Iterator<SelectionKey> i = selector.selectedKeys().iterator(); i.hasNext();) {
SelectionKey key = i.next();
i.remove();
if (key.isAcceptable()) {
// 建立一个新连接
SocketChannel client = server.accept();
client.configureBlocking(false);
//连接建立后,告诉Selector,我现在对I/O可读事件感兴趣
client.register(selector, SelectionKey.OP_READ);
}
}
}
简单回顾,服务端在 I/O 通信上主要做了三件事情:监听连接、I/O 事件查询以及数据读写。因此 Jetty 设计了对应的 Acceptor、SelectorManager 和 Connection 分别来做这三件事情。
Acceptor 用于接受请求,在 Connector 的实现类 ServerConnector 中,有一个 _acceptors 的数组,在 Connector 启动的时候, 会根据_acceptors数组的长度创建对应数量的 Acceptor,而 Acceptor 的个数可以配置。
for (int i = 0; i < _acceptors.length; i++)
{
Acceptor a = new Acceptor(i);
getExecutor().execute(a);
}
可以看到 Acceptor 可以被提交到线程池(前面提到的全局的线程池)中,所以它是一个 Runnable,并且是 ServerConnector 的一个内部类。
Acceptor 通过阻塞的方式来接受连接,这一点跟 Tomcat 也是一样的。
public void accept(int acceptorID) throws IOException
{
ServerSocketChannel serverChannel = _acceptChannel;
if (serverChannel != null && serverChannel.isOpen())
{
// 这里是阻塞的
SocketChannel channel = serverChannel.accept();
// 执行到这里时说明有请求进来了
accepted(channel);
}
}
接受连接成功后会调用 accepted 函数,accepted 函数中会将 SocketChannel 设置为非阻塞模式,然后交给 Selector 去处理,因此这也就到了 Selector 的地界了。
private void accepted(SocketChannel channel) throws IOException
{
channel.configureBlocking(false);
Socket socket = channel.socket();
configure(socket);
// _manager是SelectorManager实例,里面管理了所有的Selector实例
_manager.accept(channel);
}
Jetty 的 Selector 由 SelectorManager 类管理,而被管理的 Selector 叫作 ManagedSelector。SelectorManager 内部有一个 ManagedSelector 数组,真正干活的是 ManagedSelector。接着上面的 accept 函数
public void accept(SelectableChannel channel, Object attachment)
{
//选择一个ManagedSelector来处理Channel
final ManagedSelector selector = chooseSelector();
//提交一个任务Accept给ManagedSelector
selector.submit(selector.new Accept(channel, attachment));
}
我们看到这里会提交任务 Accept 给 ManageSelector,这里主要是两步:
_key = _channel.register(selector, SelectionKey.OP_ACCEPT, this);
private void createEndPoint(SelectableChannel channel, SelectionKey selectionKey) throws IOException
{
//1. 创建EndPoint
EndPoint endPoint = _selectorManager.newEndPoint(channel, this, selectionKey);
//2. 创建Connection
Connection connection = _selectorManager.newConnection(channel, endPoint, selectionKey.attachment());
//3. 把EndPoint、Connection和SelectionKey绑在一起
endPoint.setConnection(connection);
selectionKey.attach(endPoint);
}
这两步的意义是什么呢?通俗的来讲,就是你去饭店吃饭,先点菜(注册 I/O 事件),服务员(ManagerSelector)给你一个单子(SelectionKey),等菜做好了(I/O事件到了),服务员根据单子就知道哪桌点了什么菜,于是喊一嗓子某某桌子的菜好了(调用 EndPoint 的方法)。
这里需要你特别注意的是,ManagedSelector 并没有直接调用 EndPoint 的方法去处理数据,而是通过调用 EndPoint 的方法返回一个 Runnable,然后把这个 Runnable 扔给线程池执行,所以你能猜到,这个 Runnable 才会去真正读数据和处理请求。
Connection 就是 Tomcat 的 Processor, 负责协议的解析,得到 Request 对象,然后丢给 Handler 处理。下面简单介绍一下它的具体实现类 HttpConnection 对请求和响应的处理过程。
请求处理: HttpConnection 并不会主动向 EndPoint 读取数据,而是向在 EndPoint 中注册一堆回调方法:
getEndPoint().fillInterested(_readCallback);
这段代码就是告诉 EndPoint,数据到了你就调我这些回调方法_readCallback吧,有点异步 I/O 的感觉,也就是说 Jetty 在应用层面模拟了异步 I/O 模型。
而在回调方法内,会调用 EndPoint 的接口去读取数据,读完让 Http 解析器去解析字节流得到包括请求行、请求头相关信息并存到 Request 对象里。
响应处理: Connection 调用 Handler 进行业务处理,Handler 会通过 Response 对象来操作响应流,向流里面写入数据,HttpConnection 再通过 EndPoint 把数据写到 Channel,这样一次响应就完成了。
到这里你应该就了解了 Connector 的工作原理,你可以参照下图回忆: