Tomcat原理分析

Tomcat内部结构

内部可以分为两部分:HTTP服务器 + Servlet容器。

这里以内嵌Tomcat为例,启动类为Tomcat

  1. Tomcat里包含一个Server,类型为StandardServer。
  2. StandardServer中包含对个service,类型为StandardService,在创建StandardServer时添加了一个service对象。
  3. StandardService中包含多个Connector,包含一个Engine(类型为StandardEngine)。
  4. Connector中包含一个ProtocolHandler,一般使用Http11NioProtocol类型,它内部又包含NioEndpoint。
  5. StandardEngine内部包含多个child,一般将Host(类型为StandardHost)作为第一个child。
  6. StandardHost内部也包含多个child,一般将StandardContext作为其第一个child。
  7. StandardContext可以看做一个项目,每个项目都有单独的contextPath,单独的ServletContext(具体类型为ApplicationContext)。
  8. StandardContext内部包含多个child,其实都是Wrapper,具体类型为StandardWrapper,Wrapper可以看做一个Servlet的包装器,其中也包含此Servlet的urlMapping,具体属性为mappings。
  9. StandardContext内部的filterDefs属性保存所有的Filter定义,filterMaps保存urlMapping和Filter的对应关系。

StandardEngine,StandardHost,StandardContext,StandardWrapper,这几个Container中都包含一个Pipeline对象,具体类型为StandardPipeline。StandardPipeline中包含一个Valve对象,可以看做一个链表节点

  1. StandardEngine向Pipeline对象设置了StandardContextValve
  2. StandardContextValve设置了StandardHostValve
  3. StandardContext设置了StandardContextValve
  4. StandardWrapper设置了StandardWrapperValve
  1. Connector中包含一个ProtocolHandler,一般使用Http11NioProtocol类型,它内部又包含一个NioEndpoint对象。

Tomcat启动流程

  1. Tomcat的start()方法
  2. 先调用StandardServer的init()方法,其中又会调用它的内部组件service的init流程,没啥重要的,都是一些MBean监控相关的处理。
  3. 在NioEndpoint的start()方法流程中,会创建ServerSocketChannel对象,接收客户端请求,并创建ThreadPoolExecutor对象,此类继承Java中的ThreadPoolExecutor类
  4. 开启Acceptor线程,它的工作:
    • 调用AbstractEndpoint的serverSocketAccept()方法,对于NioEndpoint来说,就是ServerSocketChannel的accept()。
    • 调用AbstractEndpoint的setSocketOptions()方法,根据accept的SocketChannel创建NioSocketWrapper并注册到Poller对象中
  5. 开启Poller线程,它的工作:
    • 一直监听SynchronizedQueue队列(等待Acceptor线程注册),得到NioSocketWrapper,向JavaNIO中Selector注册读事件
    • 监听Selector读和写,创建SocketProcessorBase对象,具体类型为SocketProcessor,这是一个Runnable,交给上面创建的线程池来处理。
  6. 向Selector注册写事件是通过Poller的addEvent()方法来实现的

Acceptor和Poller之间通过queue通信,可以看做生产者/消费者模式。

Tomcat处理请求的流程

  1. Acceptor线程接收到SocketChannel对象,交给Poller线程处理
  2. Poller线程将SocketChannel包装成NioSocketWrapper对象,在包装到一个SocketProcessor中,它可以看做一个任务,交给线程池处理
  3. SocketProcessor在处理的过程中会创建Http11Processor对象
  4. Http11Processor通过内部的Http11InputBuffer来解析HTTP请求,创建出Request和Response对象。
  5. 交给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);
      
    StandardEngineValve -> StandardHostValve -> StandardContextValve -> StandardWrapperValve
  6. StandardWrapperValve来创建Servlet对象,初始化,通过ApplicationFilterFactory来创建ApplicationFilterChain对象(过滤器链),过滤器信息是从StandardWrapper的parent组件StandardContext中获取的。
  7. 执行过滤器链,依次执行Filter的doFilter()方法,然后执行Servlet的service()方法

HTTP服务器和Servlet容器之间通过CoyoteAdapter对象关联起来。

Tomcat为什么不使用Netty作为连接器

  1. 第一个原因是Tomcat的连接器性能已经足够好了,同样是Java NIO编程,套路都差不多。
  2. 第二个原因是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

  1. 先查本地cache是否已经加载过此类
  2. 查询系统类加载器是否已经加载过
  3. 如果都没有,交给扩展类加载器加载(它会委托根类加载器加载),这个是为了避免覆盖JDK核心类。
  4. 扩展类加载器加载失败,在本地Web应用下查找(自己加载)
  5. 没找到,交给系统类加载器加载

和双亲委派机制不同的地方在于,先自己尝试加载,再交给父类加载器加载。

关于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-极客时间

posted @ 2024-04-06 09:11  strongmore  阅读(28)  评论(0编辑  收藏  举报