tomcat

实践

  • 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()
问题及答案:

  1. URLClassLoader 由谁调用?(ConfigFileLoader 是谁加载的,没明显指定 classloader?) 2. 如何确定 某个class在哪个jar 里?
  2. 如果一个类由类加载器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个问题:

  1. Connector 如何接受请求的?
  2. 如何将请求封装成 Request 和 Response 的?(org.apache.coyote.http11.Http11Processor#prepareRequest)
  3. 封装完之后的 Request 和 Response 如何交给 Container 进行处理的?
  4. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Connector
protected final ProtocolHandler protocolHandler;

AbstractProtocol(ProtocolHandler)
private final AbstractEndpoint<S> endpoint;
private AbstractEndpoint.Handler<S> handler;
private final Set<Processor> waitingProcessors=Collections.newSetFromMap(new ConcurrentHashMap<Processor, Boolean>());
private AsyncTimeout asyncTimeout = null;
protected Adapter adapter;

AbstractEndpoint
protected Acceptor[] acceptors;
protected SynchronizedStack<SocketProcessorBase<S>> processorCache;
private Handler<S> handler = null;

NioEndpoint
private NioSelectorPool selectorPool = new NioSelectorPool();
private ServerSocketChannel serverSock = null;
private volatile CountDownLatch stopLatch = null;
private SynchronizedStack<PollerEvent> eventCache;
private SynchronizedStack<NioChannel> nioChannels;
private Poller[] pollers = null;

流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
new Connector
this.protocolHandler = newInstance Http11NioProtocol
endpoint = new NioEndpoint
handler = new AbstractProtocol.ConnectionHandler

Connector#initInternal
adapter = new CoyoteAdapter
NioEndpoint#bind;//完成了 nio的初始化,包括 ServerSocketChannel,ServerSocket,Selector 等对象的初始化
serverSock = ServerSocketChannel.open();//NioEndpoint.serverSock
selectorPool.open();//NioBlockingSelector.BlockPoller 线程,处理阻塞读写场景

Connector#startInternal
asyncTimeout = new AsyncTimeout();//线程,检查 Processor 是否超时,并处理超时 Processor
processorCache = new SynchronizedStack<>(..);
eventCache = new SynchronizedStack<>(..);
nioChannels = new SynchronizedStack<>(..);
createExecutor();//worker 线程 池(调用业务代码处理请求的线程池)
initializeConnectionLatch(); //创建 LimitLatch,“控制最大连接数”,默认是 10000
pollers = new Poller[getPollerThreadCount()==1];//Poller 线程,应该是循环处理 selector 中的事件(processKey(sk, attachment);)
startAcceptorThreads()==1;//Acceptor 线程,应该是循环获取“当前连接”的数据(setSocketOptions(socket))


CoyoteAdapter 分析
org.apache.coyote.http11.Http11Processor#service//进入 adapter 流程
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);//最后到 servlet


//hand the socket off to an appropriate processor
NioEndpoint#setSocketOptions
socketProperties.setProperties(sock);//定制化 sock
SocketBufferHandler bufhandler = new SocketBufferHandler(...);//nio缓存
channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);//https 时关联到 selectorPool 上
getPoller0().register(channel);//NioChannel 注冊到 Poller

NioEndpoint.Poller 分析
private Selector selector = Selector.open();
private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();//events 队列,此类的核心
NioEndpoint.Poller#register();
new PollerEvent(socket,ka,OP_REGISTER);//将这个 socket 包装为 PollerEvent,添加到 events 中
NioEndpoint.Poller#events();//取出队列中的 PollerEvent,逐个执行其 run,reset 方法。
NioEndpoint.Poller#processKey
unreg(sk, attachment, sk.readyOps());//如接下来是处理 socket 进来的数据,那么就不再监听该 channel 的 OP_READ 事件(socket 数据准备完成?)
SocketProcessorBase<S> sc = createSocketProcessor(socketWrapper, event);//转到 SocketProcessorBase,NioSocketWrapper
executor.execute(sc);//使用 worker

NioEndpoint.PollerEvent#run()//让 poller.selector 监听 socket 上的不同事件
//在 poller.selector 上监听 socket 的 OP_READ 事件,并绑定对象 socketWrapper
socket.getIOChannel().register(socket.getPoller().getSelector(),SelectionKey.OP_READ,socketWrapper);


NioSocketWrapper(SocketWrapperBase)//方法 主要是使用 buffer 对 socket 读写
private final NioSelectorPool pool = endpoint.getSelectorPool();//selectorPool
//NioSocketWrapper 在 selectorPool 上的操作
org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper#read(boolean, java.nio.ByteBuffer);
org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper#doWrite;
//NioSocketWrapper 在 poller.selector 上的操作(TLS/SSL 加密有关 )
org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper#registerReadInterest;
org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper#registerWriteInterest;

NioBlockingSelector#write,read;//使用传入 socket 写读传入 buf,并往 BlockPoller 中加入 event
NioBlockingSelector.BlockPoller#run,events;//使用 sharedSelector 监听传入 channel 中的不同事件

SocketProcessor(SocketProcessorBase,Runnable)
state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
doRun();//handshake 是否加密握手

AbstractProtocol.ConnectionHandler
state = processor.process(wrapper, status);
state = service(socketWrapper);//AbstractProcessorLight
getAdapter().service(request, response);//Http11Processor#service

总结

t_nio 实现思路

BlockPoller 线程引用 sharedSelector 处理阻塞读写场景
Acceptor 线程引用 serverSock 获取到 SocketChannel。
Poller 线程 使用自己的 Selector 及 Acceptor 给的 SocketChannel。(整体上属于 多线程Reactor模式)
NioSocketWrapper 在整个流程中会使用到 sharedSelector 及 poller.selector 对象,贯穿 读请求 及 写响应 全过程?

问题

  1. 请求内容一次tcp连接放不下怎么办?
  2. org.apache.tomcat.util.net.NioEndpoint.Acceptor#run
    SocketChannel socket = serverSock.accept();//没绑定 Selector 能获取数据吗?(阻塞 channel 不需要绑定 Selector)
  3. org.apache.tomcat.util.net.NioBlockingSelector.BlockPoller#selector .selectNow()哪个通道?没有绑定通道
  4. NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment(); 为什么能强转?(上面的 register)

debug tomcat

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>

packaging 类型为war。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
String name = req.getParameter("name");
if (name == null) {
name = "world";
}
PrintWriter pw = resp.getWriter();
pw.write("<h1>Hello, " + name + "!</h1>");
pw.flush();
}
}
public class Main {
public static void main(String[] args) throws Exception {
// 启动Tomcat:
Tomcat tomcat = new Tomcat();
tomcat.setPort(Integer.getInteger("port", 8080));
tomcat.getConnector();
// 创建webapp:
Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
WebResourceRoot resources = new StandardRoot(ctx);
resources.addPreResources(
new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
ctx.setResources(resources);
tomcat.start();
tomcat.getServer().await();
}
}

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;