Tomcat如何处理一个请求

来源:互联网 发布:查看windows序列号 编辑:程序博客网 时间:2024/06/08 18:10

       Connector(也称为通道或连接器),Tomcat中用于处理请求与响应的组件,接受来自客户端的请求并将请求转交给Engine处理,同时将来自Engine的答复返回给客户端,Connector主要负责处理与客户端的通信。Connector的核心是ProtocolHandler,ProtocolHandler的实现类分为HTTP及AJP两种。HTTP顾名思义就是处理HTTP协议的(HTTP/1.1),一般都使用的都是这个实现。AJP是处理与其它web容器请求,一般用来与Apache Http Server做集群用。现如今大多都是使用Nginx,Tomcat协议一般为HTTP。(本文基于Tomcat8.5)

       ProtocolHandler Http协议实现有:Http11Nio2Protocol、Http11NioProtocol、Http11Protocol、Http11AprProtocol等,Tomcat默认是Http11NioProtocol,看名字就知道是Http协议并基于Java NIO。Protocol中比较核心的相关类:EndpointAdapterProcessor。Endpoint 提供I/O相关功能(NioEndpoint、NioEndpoint、AprEndpoint、JIoEndpoint)。Tomcat初始化Connector主要就是初始化ProtocolHandler,我们来看看它的初始化流程。

       首先调用bind方法帮定端口(如8080)并设置backlog(对应于Tomcat的acceptCount),以及设置一些Socket超时、SSL、打开共享Selector等操作。

       然后调用startInternal初始化队列、线程池、并发限制连接数、Poller、Acceptor等操作。每个Poller中都会打开一个Selector以及创建一个SynchronizedQueue<PollerEvent>队列。Acceptor共享一个ServerSocketChannel获取连接,ServerSocketChannel是线程安全的,可以多个线程共同执行。Tomcat中很多都是使用的自定义实现,如SynchronizedStack栈、SynchronizedQueue队列、ThreadPoolExecutor线程池、LimitLatch并发连接数限制(Tomcat的maxConnections)。LimitLatch基于AbstractQueuedSynchronizer实现的同步锁,详情见AbstractQueuedSynchronizer介绍。SynchronizedStack及SynchronizedQueue内部都是基于数组,但该数组是循环使用的,其中有一个入队索引,一个出队索引,当出入队索引到达同一位置时则进行数组扩容。

       Acceptor负责用来管理连接到tomcat服务器的数量,Acceptor线程中是一个while循环,每次循环首先会调用LimitLatch自增1,如果目前正在处理的连接已达到最大数,则使当前Acceptor进入等待状态,当连接数没超过最大数时则调用ServerSocketChannel.accept()获取Socket。获取Socket成功之后,首先会设置一些环境变量,然后包装成NioChannel,再与KeyAttachment一起封装成PollerEvent并添加到Poller的SynchronizedQueue<PollerEvent>队列中。至此Acceptor工作完成了,继续进入下一个循环处理下个请求。

       Socket内容的读写是通过Poller来实现,Poller使用java nio来实现连接的管理。Acceptor及Poller线程个数默认都是1。Nio2实现中去除了Poller线程,增加了一个AsyncTimeout线程用于检查异步请求是否超时。关于nio,主要需要明确三个概念:Channel、Selector和SelectionKey。在这里的使用上,它们之间的关系可以简单这样理解,Channel必须注册到Selector上才能用于接收socket数据,在Selector上有数据到达的Channel可以用SelectionKey来表示。

       Poller线程每次循环会从SynchronizedQueue<PollerEvent>队列循环获取已生成的PollerEvent,如果PollerEvent是新过来的请求则调用PollerEvent.run()来把SocketChannel注册到Selector。处理完events队列中的数据后则调用Selector.selectedKeys()获取已读取数据的事件Key,并循环调用processKey方法处理读数据、写数据等,通过SelectionKey.attachment()可以获取到相关对象的引用。处理相关事件的时候都会把该事件从Selector中取消注册,以免重复处理同一个事件。

       处理读写等Socket事件的时候会调用processSocket方法,把相关数据封装到SocketProcessor对象中(该对象会缓存,并不会每次都new一个),然后抛给线程池去处理。SocketProcessor本身实现了Runnable接口,大致是调用ConnectionHandler.process使用Http11Processor(Processor对象也是会缓存的)处理请求数据,调用Http11Processor.process会按Http协议把数据解析好,这些数据并不是直接解析成字符串,而是保存了一个字节数据引用及开始及结束下标,这样可以避免创建过多的String对象,等到真正使用的时候才会创建对应的String对象。解析完了后会调用CoyoteAdapter.service。

       如果把整个Tomcat内核最高抽象程度模块化,可以看成是由连接器Connector和容器Container组成,连接器负责HTTP请求接收及响应,生成请求对象及响应对象并交由容器处理,而容器则根据请求路径找到相应的Servlet进行处理。请求响应对象从连接器传送到容器需要一个桥梁,这个桥梁正是CoyoteAdapter,这个组件的作用很明显就是充当一个适配器,把连接器与容器连接起来。CoyoteAdapter.service方法会根据底层Request和Response生成Connector使用的Request(HttpServletRequest)和Response(HttpServletResponse),然后通过postParseRequest来解析请求的参数并填充至HttpServletRequest及HttpServletResponse。查找context及Wrapper(调用connector.getService().getMapper().map方法查找),解析CookieID,session等。然后用connector.getService().getContainer().getPipeline().getFirst().invoke调用容器的方法。getService()获取到StandardService,getContainer()获取到StandardEngine,最终默认调用StandardEngineValve.invoke。然后又会调用host.getPipeline().getFirst().invoke方法,最终默认是调用的StandardHostValve.invoke。然后又会调用context.getPipeline().getFirst().invoke方法,最终默认调用的StandardContextValve.invoke。然后是wrapper.getPipeline().getFirst().invoke方法,最终默认调用的是StandardWrapperValve.invoke。是不是和Tomcat的server.xml配置文件里的配置项比较似曾相识,就是查找Service》Engine》Host》Context。

       Tomcat提供了engine,host,context及wrapper四种容器。在总体结构中已经阐述了他们之间的包含关系。这四种容器继承了一个容器基类,因此可以定制化,并各自提供了标准实现,StandardEngine》StandardHost》StandardContext》StandardWrapper。每个容器对象都有一个pipeline,它就是容器对象实现逻辑操作的骨架,在pipeline上配置不同的valve,当需要调用此容器实现逻辑时,就会按照顺序将此pipeline上的所有valve调用一遍,这里可以参考责任链模式。

       Engine是最顶层的容器,它是host容器的组合,它的特殊点在处理Tomcat集群(jvmRouteId),权限管理(Realm)。Host是engine的子容器,它是context容器的集合。

       Context是host的子容器,它是wrapper容器的集合,标准实现为StandardContext,功能较多,它封装的是每个webapp(即webapps下每个应用就是一个Context),它会读取并处理web.xml配置(如果有的话)。一个context代表一个web应用,它运行在特定的虚拟主机中,每个web应用要么是一个war文件,要么是一个符合规范的目录。一般HTTP请求路径中带有requestURI,我们可以从requestURI中获取上下文路径,根据上下文路径可以选择适合的web应用程序来处理这个请求。你还可以定义多个context,但每个context的名称必须唯一。

       Context有很多功能如:Manager主要是应用的session管理模块,其主要功能是session的创建、维护、持久化,以及跨context的session的管理等,标准实现为StandardManager。Resources它是每个webapp对应的部署结构的封装,比如有的app是tomcat的webapps目录下的某个子目录或是在context节点配置的其他目录,或者是war/jar文件部署的结构等,管理着项目文件资源,标准实现为StandardRoot。Loader是对每个webapp的自有的classloader的封装,具体内容涉及到tomcat的classloader体系(ParallelWebappClassLoader),Tomcat正是有一套完整的classloader体系,才能保证每个web app或是独立运营,或是共享某些对象等等,标准实现为WebappLoader。JarScanner扫描Jar文件,Filter过滤器、LoginConfig登录权限、Servlet映射、管理work目录、wrapper管理、重加载支持、ErrorPage错误页面、InstanceManager(Servlet、Filter、Listener实例化等)等。

       Wrapper是context的子容器,它封装的处理资源的每个具体的servlet,Wrapper表示了一个servlet定义,一个servlet类对应一个wrapper,标准实现为StandardWrapper。严格的说,并不是每一个访问资源对应一个wrapper对象。而是每一种访问资源对应一个wrapper对象。其大致可分为三种:处理静态资源的一个wrapper(DefaultServlet)、处理jsp的一个wrapper(JspServlet)、处理servlet的若干wrapper(在web.xml中配置的servlet),前两种是在tomcat的全局conf目录下的web.xml中配置的。请求进入后,context利用mapper匹配requesturl对应的wrapper,交给classloader来load wrapper,然后调用wrapper的invoke()方法。wrapper也是一个pipeline,拿到请求后,交给valve即StandardWrapperValve处理。StandardWrapperValve.invoke()的主要逻辑为:创建servlet实例(wrapper.allocate)、创建一个filterchain过滤器链(ApplicationFilterFactory.createFilterChain)进行filter过滤(filterChain.doFilter)、在filterchain中经过过滤之后再调用servlet.service()方法,执行业务逻辑(往后就是Servlet中判断执行Get、Post等)。

       接上CoyoteAdapter.service方法,在Servlet正常处理完成后会调用response.finishResponse(),这个方法刚开始没注意,但你点进去之后就会发现大有乾坤。刷好coyoteResponse缓冲之后会调用coyoteResponse.action(ActionCode.CLOSE,null),其中主要的调用是Http11Processor.prepareResponse(),它主要的工作就是按Http协议设置好header、content、keepAlive、compressed等。设置好数据后调用Http11OutputBuffer.commit(),其中又会把写入数据的工作委托给SocketWrapperBase.write,默认情况下我们没有使用异步请求,所以是阻塞的,调用了SocketWrapperBase.writeBlocking。真正的写数据实现类是根据所选的IO方式,Tomcat8.5默认为NIO,所以是调用了NioSocketWrapper.doWrite。既然是NIO,过程就是首先使用SocketChannel.write往通道里写入数据,然后向Selector注册OP_WRITE事件,Poller线程会接收到该事件最终会Close掉Socket,这里在关闭之前会再次向Selector注册一个OP_READ事件来继续监听客户端是否还有请求(长轮询)。

原创粉丝点击