Tomcat原理分析
Tomcat内部结构
内部可以分为两部分:HTTP服务器 + Servlet容器。
这里以内嵌Tomcat为例,启动类为Tomcat
- Tomcat里包含一个Server,类型为StandardServer。
- StandardServer中包含对个service,类型为StandardService,在创建StandardServer时添加了一个service对象。
- StandardService中包含多个Connector,包含一个Engine(类型为StandardEngine)。
- Connector中包含一个ProtocolHandler,一般使用Http11NioProtocol类型,它内部又包含NioEndpoint。
- StandardEngine内部包含多个child,一般将Host(类型为StandardHost)作为第一个child。
- StandardHost内部也包含多个child,一般将StandardContext作为其第一个child。
- StandardContext可以看做一个项目,每个项目都有单独的contextPath,单独的ServletContext(具体类型为ApplicationContext)。
- StandardContext内部包含多个child,其实都是Wrapper,具体类型为StandardWrapper,Wrapper可以看做一个Servlet的包装器,其中也包含此Servlet的urlMapping,具体属性为mappings。
- StandardContext内部的filterDefs属性保存所有的Filter定义,filterMaps保存urlMapping和Filter的对应关系。
StandardEngine,StandardHost,StandardContext,StandardWrapper,这几个Container中都包含一个Pipeline对象,具体类型为StandardPipeline。StandardPipeline中包含一个Valve对象,可以看做一个链表节点
- StandardEngine向Pipeline对象设置了StandardContextValve
- StandardContextValve设置了StandardHostValve
- StandardContext设置了StandardContextValve
- StandardWrapper设置了StandardWrapperValve
- Connector中包含一个ProtocolHandler,一般使用Http11NioProtocol类型,它内部又包含一个NioEndpoint对象。
Tomcat启动流程
- Tomcat的start()方法
- 先调用StandardServer的init()方法,其中又会调用它的内部组件service的init流程,没啥重要的,都是一些MBean监控相关的处理。
- 在NioEndpoint的start()方法流程中,会创建ServerSocketChannel对象,接收客户端请求,并创建ThreadPoolExecutor对象,此类继承Java中的ThreadPoolExecutor类
- 开启Acceptor线程,它的工作:
- 调用AbstractEndpoint的serverSocketAccept()方法,对于NioEndpoint来说,就是ServerSocketChannel的accept()。
- 调用AbstractEndpoint的setSocketOptions()方法,根据accept的SocketChannel创建NioSocketWrapper并注册到Poller对象中
- 开启Poller线程,它的工作:
- 一直监听SynchronizedQueue队列(等待Acceptor线程注册),得到NioSocketWrapper,向JavaNIO中Selector注册读事件
- 监听Selector读和写,创建SocketProcessorBase对象,具体类型为SocketProcessor,这是一个Runnable,交给上面创建的线程池来处理。
- 向Selector注册写事件是通过Poller的addEvent()方法来实现的
Acceptor和Poller之间通过queue通信,可以看做生产者/消费者模式。
Tomcat处理请求的流程
- Acceptor线程接收到SocketChannel对象,交给Poller线程处理
- Poller线程将SocketChannel包装成NioSocketWrapper对象,在包装到一个SocketProcessor中,它可以看做一个任务,交给线程池处理
- SocketProcessor在处理的过程中会创建Http11Processor对象
- Http11Processor通过内部的Http11InputBuffer来解析HTTP请求,创建出Request和Response对象。
- 交给CoyoteAdapter对象的service()方法来处理
- 先找到满足请求路径的Servlet
//通过Mapper对象根据请求url找到符合要求的Servlet,具体为internalMapWrapper()方法 connector.getService().getMapper().map(serverName, decodedURI, version, request.getMappingData());
- 调用Servlet
//StandardService的container为StandardEngine,通过pipeline最终会调用 connector.getService().getContainer().getPipeline().getFirst().invoke( request, response);
- 先找到满足请求路径的Servlet
- StandardWrapperValve来创建Servlet对象,初始化,通过ApplicationFilterFactory来创建ApplicationFilterChain对象(过滤器链),过滤器信息是从StandardWrapper的parent组件StandardContext中获取的。
- 执行过滤器链,依次执行Filter的doFilter()方法,然后执行Servlet的service()方法
HTTP服务器和Servlet容器之间通过CoyoteAdapter对象关联起来。
Tomcat为什么不使用Netty作为连接器
- 第一个原因是Tomcat的连接器性能已经足够好了,同样是Java NIO编程,套路都差不多。
- 第二个原因是Tomcat做为Web容器,需要考虑到Servlet规范,Servlet规范规定了对HTTP Body的读写是阻塞的,因此即使用到了Netty,也不能充分发挥它的优势。所以Netty一般用在非HTTP协议和Servlet的场景下。
SpringBoot内嵌Tomcat为何不扫描WebServlet等Servlet3.0注解
Embedded Tomcat does not honor ServletContainerInitializers
为什么嵌入式Tomcat不会扫描ServletContainerInitializer类,是因为Context中没有添加ContextConfig这个LifecycleListener,没有使用Tomcat那一套流程,SpringBoot自己定义了一套规范,如ServletContextInitializer接口及@ServletComponentScan注解。
我们可以通过以下方式添加,但不确定是否有什么冲突和隐患。
@Bean
public TomcatContextConfigWebServerCustomizer websocketServletWebServerCustomizer() {
return new TomcatContextConfigWebServerCustomizer();
}
public class TomcatContextConfigWebServerCustomizer
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory>, Ordered {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addContextCustomizers((context) -> context.addLifecycleListener(new ContextConfig()));
}
@Override
public int getOrder() {
return 0;
}
}
Tomcat中的线程池
具体类为 ThreadPoolExecutor(tomcat包下)。
- java中的线程池默认是到达核心线程数之后添加队列,队列满了之后创建新的线程直到最大线程数,再之后执行拒绝策略
- Tomcat中的流程为,到达核心线程数之后创建新的线程直到最大线程数,之后再添加队列,队列满了之后执行拒绝策略,通过重写execute()方法和重写队列的offer()方法来实现。当核心线程数小于最大线程数,就添加队列失败。
原因:
- JDK默认提供的线程池为CPU密集型
- Tomcat需要的是IO密集型
Tomcat中的类加载器
具体类为 WebappClassLoaderBase
- 先查本地cache是否已经加载过此类
- 查询系统类加载器是否已经加载过
- 如果都没有,交给扩展类加载器加载(它会委托根类加载器加载),这个是为了避免覆盖JDK核心类。
- 扩展类加载器加载失败,在本地Web应用下查找(自己加载)
- 没找到,交给系统类加载器加载
和双亲委派机制不同的地方在于,先自己尝试加载,再交给父类加载器加载。
关于Tomcat中的Session实现原理
- 使用StandardManager来管理Session,父类ManagerBase中的sessions(Map类型)来保存所有Session。
- 使用StandardSessionIdGenerator来生成sessionid
- 创建一个空的Session对象,在设置Id时,向ManagerBase中的sessions中添加自身。
- Session的过期清理是借助Tomcat的热加载机制来处理的,具体为ManagerBase的backgroundProcess()方法,每隔60秒检查一次,当然每次具体使用时也会检查。
参考
tomcat源码阅读
Tomcat NIO 模型的实现
原生线程池这么强大,Tomcat 为何还需扩展线程池?
深入拆解 Tomcat & Jetty-极客时间