Jetty 架构——Connector 组件

如果大家觉得文章有错误内容,欢迎留言或者私信讨论~

  Jetty 同样也是一个 “HTTP 服务器 + Servlet 容器”,Jetty 在架构设计上和 Tomcat 有很多相似之处,但是他更小巧、更容易定制化。Jetty 作为后起之秀,应用范围也越来越广,比如 Google App Engine 就采用了 Jetty。今天我们就通过聊聊 Jetty 与 Tomcat 的不同之处,加深对 Web 容器架构设计的理解,另一方面更清楚它们的设计区别。

Jetty 整体架构

  Jetty 的架构是由多个 Connector、Handler 以及一个线程池组成的,如下图:
Jetty 架构——Connector 组件_第1张图片

  跟 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 共享的。

Jetty 架构——Connector 组件_第2张图片

  第二个区别是,Tomcat 每个连接器都有自己的线程池,而 Jetty 中的 Connector 共享一个全局的线程池。

Connector 组件

  跟 Tomcat 一样,Connector 的主要功能就是对 I/O 模型和应用协议的封装。I/O 模型方面,最新的 Jetty9 只支持 NIo,因此 Jetty 的 Connector 设计有明显的 Java NIO 通信模型的痕迹。至于应用层协议方面,跟 Tomcat 的 Processor 一样,Jetty 抽象出了 Connection 组件来封装应用层协议的差异。

Java NIO 回顾

  大部分的开发同学接触的 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

  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);
}

SelectorManager

  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,这里主要是两步:

  1. 调用 Selector 的 register 方法把 Channel 注册到 Selector 上,拿到一个 SelectionKey
 _key = _channel.register(selector, SelectionKey.OP_ACCEPT, this);
  1. 创建一个 EndPoint 和 Connection,并跟这个 SelectionKey(Channel)绑在一起
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

  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 的工作原理,你可以参照下图回忆:
Jetty 架构——Connector 组件_第3张图片

  1. Acceptor 监听连接请求,当有连接请求到达时就接受连接,一个连接对应一个 Channel,Acceptor 将 Channel 交给 ManagedSelector 来处理。
  2. ManagedSelector 把 Channel 注册到 Selector 上,并创建一个 EndPoint 和 Connection 跟这个 Channel 绑定,接着就不断地检测 I/O 事件。
  3. I/O 事件到了就调用 EndPoint 的方法拿到一个 Runnable,并扔给线程池执行。
  4. 线程池中调度某个线程执行 Runnable。
  5. Runnable 执行时,调用回调函数,这个回调函数是 Connection 注册到 EndPoint 中的。
  6. 回调函数内部实现,其实就是调用 EndPoint 的接口方法来读数据。
  7. Connection 解析读到的数据,生成请求对象并交给 Handler 组件去处理。

你可能感兴趣的:(Tomcat,jetty,架构)