实践
- springboot下开启tomcat debug(打印接受到的报文):logging.level.org.apache=debug
- The HTTP specification does not allow for POST data to be of an unknown length. 文件上传(multipart uploads)时特别注意设置 content-length.
Lifecycle
是一个状态机,对组件的由生到死状态的管理
LifecycleBase 是使用了状态机+模板模式来实现的
Valve
作为一个个基础的阀门,扮演着业务实际执行者的角色
Pipeline 作为一个管道,我们可以简单认为是一个Valve的集合,内部会对这个集合进行遍历,调用每个元素的业务逻辑方法invoke()。只能 setBasic() 和 setContainer()
StandardEngine 的初始化
直接子容器是 StardandHost,但是对其 initInternal() 方法的分析,并没有发现对 StardandHost 进行初始化操作,这是因为 StandardEngine 不按照套路出牌,把初始化过程放在 start 阶段。
原因:Host、Context、Wrapper 这些容器和具体的 webapp 应用相关联了,初始化过程会更加耗时,因此在 start 阶段用多线程完成初始化以及start生命周期。
否则,像顶层的 Server、Service 等组件需要等待 Host、Context、Wrapper 完成初始化才能结束初始化流程,整个初始化过程是具有传递性的。
start 阶段,会执行 ContainerBase 的 startInternal 方法,它会寻找子容器,并且把子容器包装成 StartChild 任务丢给线程池处理,来异步启动子容器,再得到 Futures,并且会遍历所有的 Future 调用 result.get() 进行阻塞,这个操作是将异步启动转同步,子容器启动完成才会继续运行。
tomcat 类加载
定义:Java虚拟机把Class文件加载进内存,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制;
细节:加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性;
类加载器的设计:
- 分类 启动类加载器、扩展类加载器、应用程序类加载器
- 双亲委派模型:优先使用父类加载,逻辑看java.lang.ClassLoader
特殊情况:线程上下文类加载器–通过java.lang.Thread类的setContextClassLoader
tomcat 自己实现的类加载器:
- Common 类加载器,负责加载Tomcat和Web应用都复用的类
- Catalina 类加载器,负责加载Tomcat专用的类,而这些被加载的类在Web应用中将不可见
- Shared 类加载器,负责加载Tomcat下所有的Web应用程序都复用的类,而这些被加载的类在Tomcat中将不可见
- WebApp 类加载器,负责加载具体的某个Web应用程序所使用到的类,而这些被加载的类在Tomcat和其他的Web应用程序都将不可见
- Jsp 类加载器,每个jsp页面一个类加载器,不同的jsp页面有不同的类加载器,方便实现jsp页面的热插拔
类加载实现逻辑的方法:
Bootstrap.main() –> Bootstrap.init()
initClassLoaders() 指定初始化哪些 类加载器 –commonLoader、catalinaLoader、sharedLoader
createClassLoader(…) 解析类加载器的路径 repository
ClassLoaderFactory.createClassLoader(…) 创建类加载器,最终所有的类加载器都是 URLClassLoader 对象
WebappLoader 类加载器 StandardContext.startInternal()
问题及答案:
- URLClassLoader 由谁调用?(ConfigFileLoader 是谁加载的,没明显指定 classloader?) 2. 如何确定 某个class在哪个jar 里?
- 如果一个类由类加载器A加载,那么这个类的依赖类也优先由类加载器A加载(jvm控制?) 2. URLClassPath 可能能解决;
ContextClassLoader 使用案例
JDBC: java.sql.DriverManager#getConnection(java.lang.String, java.util.Properties, java.lang.Class<?>)
SPI 实现类: java.util.ServiceLoader#load(java.lang.Class)
tomcat 启动过程一
- Bootstrap.load() —> Catalina.load()
- InputSource、InputStream 对象创建,首先尝试加载conf/server.xml 两次;如果不存在conf/server.xml,则加载server-embed.xml(该xml在catalina.jar中);
- getServer().init() –调用 server 钩子方法 initInternal() 完成初始化 —接下来向下 初始化子容器(StandardService,Connector,StandardEngine);
- Class<?> clazz = Class.forName(protocolHandlerClassName);//Connector 为什么要使用反射初始化 protocolHandler??因为 protocolHandler 是动态确定的。
- StandardEngine 继承自 ContainerBase,而 ContainerBase 重写了initInternal()方法,用于初始化 start、stop 线程池
tomcat 启动过程二
- Bootstrap的 load() 方法反射调用Catalina的load方法 完成tomcat的初始化,包括server.xml的解析、实例化各大组件、初始化组件等逻辑
- Bootstrap的 start() 方法反射调用Catalina的start方法 完成tomcat的启动过程
HTTP请求处理过程(一)
要理解 Connector,我们需要问自己4个问题:
- Connector 如何接受请求的?
- 如何将请求封装成 Request 和 Response 的?(org.apache.coyote.http11.Http11Processor#prepareRequest)
- 封装完之后的 Request 和 Response 如何交给 Container 进行处理的?
- Container 处理完之后如何交给 Connector 并返回给客户端的?
Connector 的结构图
版本:org.apache.tomcat:tomcat-catalina:8.5.29
Connector 使用 ProtocolHandler 来处理请求的,不同的协议、不同的通信方式 ProtocolHandler 会有不同的实现。
- ajp 和 http11 是两种不同的协议,如 AbstractHttp11Protocol,AbstractAjpProtocol 等
- nio、nio2 和 apr 是不同的通信方式,如 NioEndpoint,Nio2Endpoint,AprEndpoint 等
协议和通信方式可以相互组合,如 AjpNioProtocol,Http11NioProtocol,Http11AprProtocol 等。
ProtocolHandler 由包含了三个部件:Endpoint、Processor、Adapter、AsyncTimeout、Handler。
- Endpoint 由于是处理底层的 Socket 网络连接,因此 Endpoint 是用来实现TCP/IP协议的,
- Processor 用来实现 HTTP 协议的,
- Adapter 将请求适配到 Servlet 容器进行具体的处理。
Endpoint 结构大致可分为 Acceptor、Poller、Handler。
Acceptor 线程主要用于监听套接字,将已连接套接字转给 Poller 线程;
结构
1 | Connector |
流程
1 | new Connector |
总结
t_nio 实现思路
BlockPoller 线程引用 sharedSelector 处理阻塞读写场景
Acceptor 线程引用 serverSock 获取到 SocketChannel。
Poller 线程 使用自己的 Selector 及 Acceptor 给的 SocketChannel。(整体上属于 多线程Reactor模式)
NioSocketWrapper 在整个流程中会使用到 sharedSelector 及 poller.selector 对象,贯穿 读请求 及 写响应 全过程?
问题
- 请求内容一次tcp连接放不下怎么办?
- org.apache.tomcat.util.net.NioEndpoint.Acceptor#run
SocketChannel socket = serverSock.accept();//没绑定 Selector 能获取数据吗?(阻塞 channel 不需要绑定 Selector) - org.apache.tomcat.util.net.NioBlockingSelector.BlockPoller#selector .selectNow()哪个通道?没有绑定通道
- NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment(); 为什么能强转?(上面的 register)
debug tomcat
1 | <dependency> |
packaging 类型为war。
1 | "/") (urlPatterns = |
tomcat 概览分析 by javadoop
debug-tomcat工程:引入依赖 org.apache.tomcat:tomcat-catalina:8.5.29:provided 即可
- 初始化时 启动了 BlockPoller 线程(bind()方法)
- 启动时 启动了 AsyncTimeout、 工作线程池、 poller 线程组、acceptor 线程组
请求处理逻辑见上面
tomcat 相关并发数参数的解释(百度摘录):
- maxThreads(最大线程数):每一次HTTP请求到达Web服务,tomcat都会创建一个线程来处理该请求,那么最大线程数决定了Web服务可以同时处理多少个请求,默认200。
- accepCount(最大等待数):当调用Web服务的HTTP请求数达到tomcat的最大线程数时,还有新的HTTP请求到来,这时就会加入等待队列,acceptCount默认为100。如果等待队列也被放满了,这个时候再来新的请求就会被tomcat拒绝(connection refused)。
- maxConnections(最大连接数):这个参数是指在同一时间,tomcat能够接受的最大连接数。一般这个值要大于maxThreads+acceptCount。
上面的描述是错误的,代码中有两个关键的参数:
- initializeConnectionLatch –getMaxConnections 默认 maxConnections = 10000;
- createExecutor 初始化线程池 getMaxThreads –默认 maxThreads = 200;