配色: 字号:
Tomcat 连接器原理
2022-01-20 | 阅:  转:  |  分享 
  
Tomcat作为应用最广泛的Web容器被各大厂商所使用,从体系结构上看Tomcat分为连接器和容器两个部分。其中连接器负责IO请求转换、网络请求解析以及请求适配等工作。

为了深入了解其工作原理,今天让我们走进Tomcat连接器原理与实现。

Tomcat连接器https://www.connector-world.com/结构与原理

在开始介绍Tomcat连接器之前,先来回顾一下连接器的结构和工作原理。

Tomcat连接器接收来自浏览器的请求,通过ProtocolHandler中的EndPoint和Processor组件完成对IO模型额解析和处理,并且通过Adapter适配器将请求转化为ServletRequest交给容器处理。

连接器对于Servlet容器屏蔽了协议及IO模型,让容器专注于ServletRequest的处理工作。无论是HTTP还是AJP请求,在容器最终都会获取ServletRequest对象。

因此Tomcat连接器的主要功能是:

监听网络端口。

接收网络请求的字节流信息。

根据协议(HTTP/AJP)解析字节流,生成TomcatRequest对象。

将TomcatRequest对象转成ServletRequest对象发送给容器。

获取容器返回的ServletResponse对象,并且将其转化为TomcatResponse对象。

将TomcatResponse转成网络字节流,返回给网络请求方。

为了实现上述功能,Tomcat连接器需要下面几个组件的支持:

Endpoint:监听通信接口,用来接收和发送网络请求,它对传输层进行了抽象。

Acceptor:当Endpoint接收到Socket请求以后,由Acceptor对其进行监听。其中SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在run方法里调用Processor进行处理。

Processor:接收来自Endpoint的Socket请求,并将其解析成TomcatRequest对象,并交给Adapter进行后续的转换处理。

Adapter:针对不同容器的Servlet进行请求/响应适配。将客户端发过来的TomcatRequest对象通过ProtocolHandler接口解析生成ServletRequest,并将其发送给容器中的Servlet。

这里我们将Tomcat连接器的基本功能、构成和组件给大家做了简单介绍,其具体架构的介绍在另外一篇的Tomcat架构文章中有详细描述。

回到本篇的主题,针对Tomcat连接器处理IO请求并且进行转换传递的能力。

会陆续给大家介绍几个IO处理组件:NioEndpiont、Nio2Endpoint以及Tomcat的连接池是如何配合这几个组件完成IO处理操作的。

IO模型与多路复用

Tomcat连接器IO处理的组件包括三个,NioEndpiont、Nio2Endpoint和AprEndpoint。

我们会从NioEndpoint开始介绍,NioEndpoint组件实际上是实现了IO多路复用模型。

所以在介绍NioEndpoint之前需要对IO模型与多路复用模型进行讲解,从而让大家对NioEndpoint的工作原理能够有深刻的认识。

就操作系统而言它的核心是内核,是独立于普通的应用程序,内核空间可以访问受保护的内存空间,也有访问底层硬件设备。

为了保证用户进程不能直接操作内核),保证内核的安全,操作系统将空间划分为两部分,一部分为内核空间,一部分为用户空间。同时用户空间需要通过内核访问硬件设备。

当网络数据请求达到时,用户进程会先等待内核将数据从网卡拷贝到内核空间,然后再将数据从内核空间拷贝到用户空间,IO模型就是对这一过程的描述。

如果有多个网络请求进来,那么就对应多个用户进行运行,并且处理这些请求。

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。

换句话说内核会让哪些具备运行条件的进程运行,让哪些不满足条件的进程挂起,当条件满足的时候再恢复运行,内核的这种行为被称为进程切换。

但是恰恰是这种进程切换是非常耗费资源的,因为内核需要保存进程的状态和运行上下文的信息。

对于正在执行的进程而言,也会因为某些事件的发生导致工作暂停,例如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。

阻塞状态是因为进程自身行为决定,当进程在运行状态时会占用CPU资源,但是进入阻赛状态后就不会消耗CPU资源了。

有了上面知识铺垫以后,我们来看看IO模型的几种实现方式:

①同步阻塞IO

用户进程发起read调用后就进程阻塞了,因为此时需要等待网卡的数据才能read。

于是用户进程让出CPU,当网卡数据到来了并且数据从网卡拷贝到内核空间,接着将数据拷贝到用户空间,此时进行进程切换将用户进程唤醒,继续read操作。

②同步非阻塞IO

用户进程会不断的发起read调用,如果数据没有到达内核空间,那么每次read调用都会返回失败,不过此时的用户进程并没有阻赛,仅仅需要不断询问内核空间数据是否达到。

当数据到达内核空间之后,但是在数据从内核空间拷贝到用户空间这段时间里进程会进入阻塞状态,等数据到了用户空间才会把进程叫醒。

③IO多路复用

有select、poll、epoll三种实现方式,这里以select为例给大家讲解。

这里将用户进程的read操作分成两步了,用户进程先向内核发起select调用,询问数据数据是否准备好了。

当内核把数据准备好了,用户进程再发起read调用。但是在等待数据从内核空间拷贝到用户空间这段时间里,进程还是阻塞的。

换句话说在发起select调用和read调用这段时间内进程并没有阻赛,还可以执行其他操作,与同步非阻赛IO相比就省去了不断发起read调用的过程,提升了效率。

这里的多路复用是指一次select调用可以向内核查多个数据通道(Channel)的状态。

由于NioEndpoint组件使用的多路复用模型,这里对该模型进行详细的介绍,争取让大家从原理上能够理解这个模型的工作原理。



图2:多路复用的read和select过程

如图2所示,用户空间的用户进程为了访问对应的文件会建立一个fd的列表,这里的fd是文件描述符(Filedescriptor)的缩写,其用来表述文件引用。

当使用或者打开一个文件时,会返回一个文件描述符,说白了就是操作文件的许可证。

从图中可以看到用户进程和内核进程中都维护了相同的fd列表,这就是需要进行read调用的文件列表。

从图中可以看出每个fd在用户空间和内核空间都是一一对应的,这里我们用虚线将文件描述符fd建立了一个通道(channel)。

接着来看看select和read的过程:

用户进程根据需要操作文件的fd列表向内核发起select调用。

内核空间接到select请求以后,会针对这个fd列表进行多路的监听,看看哪些文件的从网卡拷贝到内核空间了。

假设这里标有绿色的fd文件从网卡拷贝到了内核空间,此时内核会通过select返回的方式通知用户进程有文件准备好了。

用户进程接收到内核的select返回以后,会开始执行read调用读取对应的文件内容。

NioEndpoint组件

有了上面对IO多路复用原理的介绍,这里对NioEndpoint组件接收网络请求并处理的过程进行介绍。

过程中会涉及到:LimitLatch、Acceptor、Poller、SocketProcessor和Executor等5个组件。



图3:NioEndpoint运行原理

如图3所示,NioEndpoint经过8个步骤来出来网络请求:

①当服务器接收到网络请求,最先会到达网卡,此时网卡上的数据会被拷贝到内存空间上。

②内存空间会创建接收队列,用来存放数据包,将数据包传递给NioEndpoint组件处理。

LimitLatch是NioEndpoint的连接控制器,用来控制最大连接数,NIO模式下默认是10000,达到这个阈值后,连接请求会被拒绝。

③请求通过LimitLatch到达Acceptor,Acceptor跑在一个单独的线程里,通过在死循环里调用accept方法来接收新连接。

④一旦有新的连接请求到来,accept方法返回一个Channel对象,接着把Channel对象交给Poller去处理。这里的Channel对象就是用来监听数据包是否可以读取的。

⑤Poller在内部维护Channel数组,并且会不断检测Channel的数据就绪状态。

一旦就绪就说明Channel中的数据包可读,于是生成一个SocketProcessor任务对象交给Executor去处理。

⑥Executor线程池,负责运行SocketProcessor任务类,SocketProcessor的run方法会调用Http11Processor来读取和解析请求数据。

⑦Http11Processor是应用层协议的封装,它会调用对应的容器并且获得响应,再把响应通过Channel返回给发送队列。这里专注于连接器的工作,就没有花容器的部分。

⑧发送队列在接收到响应的数据包以后会将响应数据通过网卡返回给网络请求方。

说完了NioEndppoint的执行流程,这里需要顺便提一下AprEndpoint组件。

APR(ApachePortableRuntimeLibraries)是Apache可移植运行时库,它是用C语言实现的,它的工作也是处理包括文件和网络IO请求。

为什么要在这里提起是因为AprEndpoint与NioEndpoint一样居于非阻塞IO的多路复用机制实现的。

所不同的是,AprEndpoint是通过JNI调用APR本地库而实现非阻塞I/O的。APR的实现是为了处理一些特殊场景,比如一些需要频繁与操作系统交互的场景。

例如Web应用使用了TLS进行加密传输,由于在传输过程中存在多次网络交互,这种情况C语言程序与操作系统交互操作会提高执行效率,这也是APR的强项。

另外补充一点,上面提到的JNI(JavaNativeInterface)是JDK提供的一个编程接口,它允许Java程序调用其他语言编写的程序或者代码库,其实JDK本身的实现也大量用到JNI技术来调用本地C程序库。

说白了就是通过JNI去调用C,通过C与操作系统进行交互操作,目的是提高交互执行的效率,用在与操作系统交互频繁的场景。

由于AprEndpoint组件的原理和NioEndpiont相似,我们这里就将其不同点和特点做扩展说明,不去展开描述了。



献花(0)
+1
(本文系zhuqi123456...首藏)