当前位置: 首页 > 科技观察

面试官:说到Tomcatconnector,我一头雾水...

时间:2023-03-19 17:31:10 科技观察

【.com原稿】Tomcat是目前使用最广泛的web容器,各大厂商都在使用。从架构上看,Tomcat分为connection和container两部分。connector负责IO请求转换,网络请求解析,请求适配。图片来自宝兔网为了深入了解其工作原理,今天让我们走进Tomcat连接器的原理与实现。Tomcat连接器结构及原理在开始介绍Tomcat连接器之前,我们先来回顾一下连接器的结构和工作原理。图1:连接器结构图如图1所示,Tomcat连接器接收到浏览器的请求,通过ProtocolHandler中的EndPoint和Processor组件完成对IO模型的解析和处理,并通过适配器适配器。给容器一个句柄。连接器对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:将请求/响应适配到不同容器的Servlets。解析客户端通过ProtocolHandler接口发送过来的TomcatRequest对象,生成一个ServletRequest,发送给容器中的Servlet。下面就给大家简单介绍一下Tomcat连接器的基本功能、组成和组件。具体架构在另一篇Tomcat架构文章中有详细介绍。回到本文的主题,Tomcatconnector的目标是处理IO请求并进行转换和传递的能力。下面我将陆续为大家介绍几个IO处理组件:NioEndpiont、Nio2Endpoint,以及Tomcat的连接池是如何配合这些组件完成IO处理操作的。IO模型和多路复用Tomcat连接器的IO处理组件包括三个,NioEndpiont、Nio2Endpoint和AprEndpoint。我们将从NioEndpoint开始,NioEndpoint组件实际上实现了IO多路复用模型。因此,在介绍NioEndpoint之前,有必要对IO模型和复用模型进行讲解,让大家对NioEndpoint的工作原理有一个深入的了解。就操作系统而言,它的核心是内核,它独立于普通的应用程序。内核空间可以访问受保护的内存空间和底层硬件设备。为了保证用户进程不能直接操作内核)和保证内核的安全,操作系统将空间分为两部分,一部分是内核空间,一部分是用户空间。同时,用户空间需要通过内核访问硬件设备。当一个网络数据请求到来时,用户进程会先等待内核将数据从网卡拷贝到内核空间,然后再从内核空间拷贝数据到用户空间。IO模型就是对这个过程的描述。如果有多个网络请求进来,它会为多个用户运行并处理这些请求。为了控制进程的执行,内核必须具有挂起CPU上运行的进程并恢复执行先前挂起的进程的能力。也就是说,内核会让满足运行条件的进程运行,不满足条件的进程挂起,条件满足时恢复运行。内核的这种行为称为进程切换。但恰恰这种进程切换是非常耗费资源的,因为内核需要保存进程的状态和运行上下文的信息。对于正在执行的进程,工作也会因为某些事件的发生而被挂起,比如请求系统资源失败、等待某些操作完成、新数据未到达或没有新的工作要做等,那么系统会自动执行阻塞原语(Block),将自己从运行状态变为阻塞状态。阻塞状态由进程本身的行为决定。当进程处于运行状态时,会占用CPU资源,但进入阻塞状态后,就不会消耗CPU资源。有了以上知识的铺垫,我们再来看看IO模型的几种实现方式:从网卡读取数据。所以用户进程让出CPU。当网卡数据到达,将数据从网卡复制到内核空间,再将数据复制到用户空间。此时进行进程切换,唤醒用户进程,继续读操作。②同步非阻塞IO用户进程会不断发起read调用。如果数据没有到达内核空间,那么每次读取调用都会返回失败。.数据到达内核空间后,进程会在数据从内核空间拷贝到用户空间的这段时间内进入阻塞状态,直到数据到达用户空间后进程才会被唤醒。③IO多路复用的实现方式有3种:select、poll、epoll。这里我们就以select为例来给大家讲解一下。这里,用户进程的读操作分为两步。用户进程首先向内核发起select调用,询问数据是否就绪。当内核准备数据时,用户进程发起读取调用。但是在等待数据从内核空间拷贝到用户空间的这段时间里,进程仍然处于阻塞状态。也就是说,在发起select调用和read调用期间,进程不会被阻塞,可以进行其他操作。与同步非阻塞IO相比,省去了不断发起read调用的过程,提高了效率。这里的多路复用意味着一个select调用可以从内核中检查多个数据通道(Channel)的状态。由于NioEndpoint组件使用的是复用模型,这里对模型做一个详细的介绍,以便大家从原理上了解这个模型的工作原理。图2:多路复用的read和select流程如图2所示,用户空间的用户进程会创建一个fd的列表,以便访问对应的文件,其中fd是文件描述符(Filedescriptor)的缩写,而its用于表示文件引用。当使用或打开一个文件时,返回一个文件描述符,它只是一个操作文件的许可证。从图中可以看出,用户进程和内核进程都维护着同一个fd列表,也就是需要读取的文件列表。从图中可以看出,每个fd在用户空间和内核空间之间都是一一对应的。这里我们使用虚线为文件描述符fd建立通道。接下来我们看一下select和read的过程:用户进程根据需要操作的文件的fd列表向内核发起select调用。内核空间收到select请求后,会通过多种方式监控fd列表,查看哪些文件已经从网卡复制到内核空间。假设这里标记为绿色的fd文件是从网卡复制到内核空间的,内核会通过select返回通知用户进程文件准备好了。用户进程收到内核的select返回后,会开始执行read调用,读取相应的文件内容。NioEndpoint组件有上面对IO多路复用原理的介绍。下面介绍一下NioEndpoint组件接收和处理网络请求的过程。该过程将涉及五个组件:LimitLatch、Acceptor、Poller、SocketProcessor和Executor。图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返回给发送队列。这里重点关注连接器的工作,并没有花容器的部分。⑧发送队列收到响应数据包后,将响应数据通过网卡返回给网络请求者。说完NioEndpoint的执行过程,我们需要顺便提一下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类似,这里不展开描述,只对它的不同点和特点进行展开。上面的Nio2Endpoint组件介绍了NioEndpoint的实现原理和处理流程。Nio2Endpoint和NioEndpoint最大的区别在于前者是一步执行请求处理,而后者是同步执行请求处理。异步的特点是用户空间的应用不需要触发内核空间到用户空间的数据拷贝。这是因为应用程序不能直接访问内核空间,所以数据拷贝工作是由内核完成的。NioEndpoint是一个用户空间进程,通过select方法监控通道中的数据/文件是否就绪,如果就绪,则通过read调用将内核数据复制到用户空间。而Nio2Endpoint是内核在数据/文件就绪时主动将数据/文件复制到用户空间。在NioEndpoint组件的工作模式下,在数据从内核空间拷贝到用户空间的这段时间内,进程仍然处于阻塞状态,必须等到数据拷贝完成后才能继续执行。Nio2Endpoint组件的工作模式下,数据拷贝过程中不会阻塞进程。以读取网络数据为例,当使用Nio2Endpoint的异步模式时,用户进程在通过read读取网络数据时,会告诉内核两件事:第一,数据准备好后复制到哪个Buffer。第二,调用用户进程中的哪个回调函数来处理数据。图4:Nio2Endpiont异步读取数据如图4所示,当内核收到读取命令后,仍然等待网卡数据到达。数据到达后,产生硬件中断。内核在中断程序中将数据从网卡复制到内核空间。然后在TCP/IP协议层做数据的拆包和重组,然后将数据拷贝到应用程序指定的Buffer中,最后调用用户进程指定的回调函数。了解了Nio2Endpoint读取数据的过程之后,让我们进一步分解内部执行过程,借助图5进行分析,就像分析NioEndpoint的过程一样。图5:Nio2Endpint工作流程如图5所示,将网卡、内核空间、Nio2Endpoint组件一起讨论,看看进程调用和数据流是如何进行的。①当服务器收到网络请求时,首先会到达网卡,此时网卡上的数据会被复制到内存空间中。②内存空间会创建一个接收队列来存放数据包,并将数据包传递给Nio2Endpoint组件进行处理。LimitLatch是Nio2Endpoint的连接控制器,用于控制最大连接数。NIO模式下,默认为10000,当达到这个阈值时,连接请求将被拒绝。③请求通过LimitLatch到达Nio2Acceptor。Nio2Acceptor是一个进程组,它扩展了Acceptor以通过异步I/O的方式接收连接。④Nio2Acceptor接收到新连接后,获取一个AsynchronousSocketChannel,并封装到Nio2SocketWrapper中,并对应创建一个SocketProcessor任务类,由线程池处理。⑤Executor线程池,负责运行SocketProcessor任务类,SocketProcessor的run方法会调用Http11Processor读取和解析请求数据。⑥Http11Processor会通过Nio2SocketWrapper读取并解析请求数据。请求被容器处理后,响应会通过Nio2SocketWrapper写出。⑦Http11Processor通过调用Nio2SocketWrapper的read方法发送第一个读请求,同时通知数据拷贝的buffer和回调类的readCompletionHandler。Http11Processor将Nio2SocketWrapper标记为数据不完整,因为数据尚未准备好。对应的SocketProcessor线程被回收,Http11Processor没有阻塞等待数据。同时Http11Processor会维护一个Nio2SocketWrapper的列表,目的是维护连接的状态,方便数据准备好后进行后续的回调操作。⑧当数据准备好后,内核已经将数据复制到Http11Processor指定的Buffer中,同时调用了readCompletionHandler。在回调处理中新建一个SocketProcessor来处理连接,因为Http11Processor维护着Nio2SocketWraper的列表。这时,即使是新的SocketProcessor任务类也可以支持发起读命令的Nio2SocketWrapper。这时候Http11Processor就可以通过这个Nio2SocketWrapper从buffer中读取数据,进行后续的处理。⑨Nio2SocketWrapper将处理后的数据传送到发送队列。⑩发送队列收到响应数据包后,将响应数据通过网卡返回给网络请求者。总结本文从Tomcat连接器的结构和原理入手,让大家知道Tomcat连接器通过Endpoint接收IO请求,通过Processor处理请求内容,并使用Adapter将TomcatReqeust转化为ServletRequestServlet容器可以接受并交给容器处理。然后将焦点转移到连接器的核心Endpoint组件上,看看它们如何处理来自网络的IO请求。在介绍IO组件的具体实现之前,通过IO模型的演进和复用模型的铺设,让大家了解底层原理。然后分别介绍NioEndpoint组件和Nio2Endpoint组件实现网络请求监听和处理的原理和流程。作者:崔浩简介:十六年开发架构经验。曾在惠普武汉交付中心担任技术专家、需求分析师、项目经理,后在一家初创公司担任技术/产品经理。善于学习,乐于分享。目前专注于技术架构和研发管理。编辑:陶佳龙征稿:如有意向投稿或寻求报道,请联系editor@51cto.com【原创稿件请注明原作者和出处为.com,合作网站转载】