TOMCAT 源碼分析 – 一次請求
前語
在上一篇源碼分析《TOMCAT源碼分析–啓動》中已經知道,Tomcat在啓動中,會通過NIO
監聽端口,而真正去接收請求的是pollerThread.start()
輪詢線程的啓動,那麼請求的入口應該是到NIO
中,最後被輪詢線程發現並被處理,那麼自然就要去看Poller
線程的run()
方法,看其是如何處理。(PS:心中要有上一篇裏面的模塊架構圖,很重要!)
端點接收請求
// org.apache.tomcat.util.net.NioEndpoint.Poller#run
@Override
public void run() {
// Loop until destroy() is called
// 一直循環,直到destroy方法被調用
while (true) {
boolean hasEvents = false;
try {
if (!close) {
// 遍歷事件隊列判斷是否有事件(請求)待處理
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
// If we are here, means we have other stuff to do
// Do a non blocking select
// 如果在這個分支,說明有其他工作要做,這裏就不去做一個阻塞的select
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
} catch (Throwable x) {
}
// ...
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
if (socketWrapper == null) {
iterator.remove();
} else {
// socketWrapper 包裝不爲空,將其取出,進行處理
iterator.remove();
processKey(sk, socketWrapper);
}
}
// Process timeouts
timeout(keyCount,hasEvents);
}
getStopLatch().countDown();
}
端點一直進行循環,檢測有沒有可用的NIO
準備就緒,如果有就拿去processKey
消費掉。那麼看看它具體怎麼做的
// org.apache.tomcat.util.net.NioEndpoint.Poller#processKey
protected void processKey(SelectionKey sk, NioSocketWrapper socketWrapper) {
try {
if (close) {
cancelledKey(sk, socketWrapper);
} else if (sk.isValid() && socketWrapper != null) {
if (sk.isReadable() || sk.isWritable()) {
if (socketWrapper.getSendfileData() != null) {
processSendfile(sk, socketWrapper, false);
} else {
unreg(sk, socketWrapper, sk.readyOps());
boolean closeSocket = false;
// Read goes before write
if (sk.isReadable()) {
if (socketWrapper.readOperation != null) {
if (!socketWrapper.readOperation.process()) {
closeSocket = true;
}
// 最終進入下面這個分支
} else if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
}
}
} else {
}
} catch (CancelledKeyException ckx) {
} catch (Throwable t) {
}
}
// org.apache.tomcat.util.net.AbstractEndpoint#processSocket
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
try {
if (socketWrapper == null) {
return false;
}
SocketProcessorBase<S> sc = null;
// 想從緩存處理器中取一個出來先
if (processorCache != null) {
sc = processorCache.pop();
}
// 如果沒取到則創建一個處理器
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
// 獲取線程池處理 -- 這裏也就是併發去處理這個處理器了,這個線程就自己回去處理NIO的監聽了。
Executor executor = getExecutor();
if (dispatch && executor != null) {
// 可以觀察到進入了這個分支
executor.execute(sc);
} else {
// 如果沒有獲取到線程池就直接調用這個處理器的run方法
sc.run();
}
} catch (RejectedExecutionException ree) {
}
// ...
return true;
}
- 從上面的執行過程可以看到他使用線程池提交了
sc
處理器,那麼就去看處理器的run
方法,且從運行中可以看到這個sc
實際爲NioEndPoint$SocketProcessor
的實例,發現這個實例中並沒有run
方法,但是其超類SocketProcessorBase
中的run
方法實際調用了實例的doRun
方法
// org.apache.tomcat.util.net.NioEndpoint.SocketProcessor#doRun
@Override
protected void doRun() {
NioChannel socket = socketWrapper.getSocket();
Poller poller = NioEndpoint.this.poller;
try {
int handshake = -1;
if (handshake == 0) {
SocketState state = SocketState.OPEN;
// Process the request from this socket
// 很容器看出來,它將在下面獲取處理器進行處理這個請求
if (event == null) {
state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
} else {
// 實際上,進的是這個分支,而event事件剛好也是上面的OPEN_READ
state = getHandler().process(socketWrapper, event);
}
} catch (CancelledKeyException cx) {
} catch (Throwable t) {
} finally {
}
}
- 從上面獲取了處理器要進行處理,也就是上一篇中的
coyote
的端點EndPoint
獲取Processor
,並且調用他的process
方法進行處理。這個處理方法十分長,它的主要邏輯就是Handler
獲取真正的處理器Process
,然後調用它的process
方法處理
// org.apache.coyote.AbstractProtocol.ConnectionHandler#process
@Override
public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
// 獲取處理器 -- 實際本次請求當前處理器爲Null
Processor processor = (Processor) wrapper.getCurrentProcessor();
try {
if (processor == null) {
// 獲取協議,需要通過協議去找對應的協議處理器 -- 這兒實際返回Null
String negotiatedProtocol = wrapper.getNegotiatedProtocol();
// ...
}
if (processor == null) {
// 還是null,就先去已經回收但還未釋放的處理器棧中彈出一個來
// 因爲已經使用過,所以這裏就返回了Http11Processor
processor = recycledProcessors.pop();
}
if (processor == null) {
// 如果還沒獲取到處理器,就根據協議去創建一個對應的處理器,第一次訪問時會如此
processor = getProtocol().createProcessor();
register(processor);
}
do {
// 協議處理器開始處理包裝的請求
state = processor.process(wrapper, status);
} while ( state == SocketState.UPGRADING);
}
- 可以發現找到處理器後,調用
process
處理,不過這個方法是超類中的方法,且Http11Processor
未進行覆蓋
// org.apache.coyote.AbstractProcessorLight#process
@Override
public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status)
throws IOException {
SocketState state = SocketState.CLOSED;
Iterator<DispatchType> dispatches = null;
do {
if (dispatches != null) {
} else if (status == SocketEvent.DISCONNECT) {
// Do nothing here, just wait for it to get recycled
} else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) {
state = dispatch(status);
state = checkForPipelinedData(state, socketWrapper);
} else if (status == SocketEvent.OPEN_WRITE) {
state = SocketState.LONG;
} else if (status == SocketEvent.OPEN_READ) {
// 根據DEBUG之前提到了,event的類型就是OPEN_READ,故進這個分支,進入核心的處理。
state = service(socketWrapper);
} else if (status == SocketEvent.CONNECT_FAIL) {
logAccess(socketWrapper);
} else {
state = SocketState.CLOSED;
}
return state;
}
那麼state = service(socketWrapper);
就是Http11Processor
的核心處理了
// org.apache.coyote.http11.Http11Processor#service
@Override
public SocketState service(SocketWrapperBase<?> socketWrapper)
throws IOException {
// ...
// Process the request in the adapter
if (getErrorState().isIoAllowed()) {
try {
rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);
// 就是這一步去找適配器並進行處理
// 獲得CoyoteAdapter的實例
getAdapter().service(request, response);
if(keepAlive && !getErrorState().isError() && !isAsync() &&
statusDropsConnection(response.getStatus())) {
setErrorState(ErrorState.CLOSE_CLEAN, null);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("http11processor.request.process"), t);
// 500 - Internal Server Error
response.setStatus(500);
setErrorState(ErrorState.CLOSE_CLEAN, t);
getAdapter().log(request, response, 0);
}
}
}
- 這一步很關鍵,他去獲得了
Adapter
,也就是模塊圖中,要通過這個適配器,去進行將Request
進行轉換封裝,最後調用Catalina
進行處理的位置
// org.apache.catalina.connector.CoyoteAdapter#service
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
// 將Request 進行轉換封裝
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());
try {
// Parse and set Catalina and configuration specific
// 解析並設置Catalina和特定於配置 -- 什麼意思呢,就是在這裏去找配置-映射了
// request parameters
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
// 調用容器的管道,進行流水線處理了
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
}
}
- 看到這兒就清晰多了,他兩步走,第一步根據配置尋找映射,
- 第二步,調用了
Service
容器的關掉進行處理這個請求,那麼根據套娃模式
,他也必將通過一層又一層的相似調用進去,拭目以待~
// org.apache.catalina.connector.CoyoteAdapter#postParseRequest
protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
org.apache.coyote.Response res, Response response) throws IOException, ServletException {
// 獲得請求解碼後的地址
MessageBytes undecodedURI = req.requestURI();
while (mapRequired) {
// This will map the the latest version by default
// 在容器中尋找映射
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
}
}
// map方法最終進入這個超類
// org.apache.catalina.mapper.Mapper#map(org.apache.tomcat.util.buf.MessageBytes, org.apache.tomcat.util.buf.MessageBytes, java.lang.String, org.apache.catalina.mapper.MappingData)
public void map(MessageBytes host, MessageBytes uri, String version,
MappingData mappingData) throws IOException {
// 將主機名和URI做映射
// 內部映射
internalMap(host.getCharChunk(), uri.getCharChunk(), version, mappingData);
}
// org.apache.catalina.mapper.Mapper#internalMap
private final void internalMap(CharChunk host, CharChunk uri,
String version, MappingData mappingData) throws IOException {
// Context mappinp
// 在這一步執行完之後,在contextList中就已經可以看到配置的具體映射了
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
// Wrapper mapping
if (!contextVersion.isPaused()) {
// 將映射數據包裝
internalMapWrapper(contextVersion, uri, mappingData);
}
}
// org.apache.catalina.mapper.Mapper#internalMapWrapper
private final void internalMapWrapper(ContextVersion contextVersion,
CharChunk path,
MappingData mappingData) throws IOException {
// ...
// 這兒的邏輯很長,但一直調式,發現他最後走了Rule 7
// Rule 7 -- Default servlet
// 默認的servlet
if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
if (contextVersion.defaultWrapper != null) {
mappingData.wrapper = contextVersion.defaultWrapper.object;
mappingData.requestPath.setChars
(path.getBuffer(), path.getStart(), path.getLength());
mappingData.wrapperPath.setChars
(path.getBuffer(), path.getStart(), path.getLength());
mappingData.matchType = MappingMatch.DEFAULT;
}
// Redirection to a folder
char[] buf = path.getBuffer();
if (contextVersion.resources != null && buf[pathEnd -1 ] != '/') {
String pathStr = path.toString();
// Note: Check redirect first to save unnecessary getResource()
// call. See BZ 62968.
if (contextVersion.object.getMapperDirectoryRedirectEnabled()) {
WebResource file;
// Handle context root
if (pathStr.length() == 0) {
file = contextVersion.resources.getResource("/");
} else {
file = contextVersion.resources.getResource(pathStr);
}
if (file != null && file.isDirectory()) {
// Note: this mutates the path: do not do any processing
// after this (since we set the redirectPath, there
// shouldn't be any)
path.setOffset(pathOffset);
path.append('/');
mappingData.redirectPath.setChars
(path.getBuffer(), path.getStart(), path.getLength());
} else {
mappingData.requestPath.setString(pathStr);
mappingData.wrapperPath.setString(pathStr);
}
} else {
mappingData.requestPath.setString(pathStr);
mappingData.wrapperPath.setString(pathStr);
}
}
}
}
把映射對象包裝好後,進入第二步,去套娃中挖掘具體的處理:
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
// org.apache.catalina.core.StandardEngineValve#invoke
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined. This is handled by the CoyoteAdapter.
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
// 要求主機處理此請求
host.getPipeline().getFirst().invoke(request, response);
}
- 可以看到進入了
StandardEngineValve
類中,那麼根據上一篇的模塊架構圖,以及server.xml
的配置就可以知道,它將一層一層處理管道線,一層一層的進行invoke
。那麼中間省略他套娃式的幾步invoke
,走到了下面StandardContextValve
的代碼
// org.apache.catalina.core.StandardContextValve#invoke
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// ...
// 管道處理
// 此時他已經一層一層的走進來,一直走到包裝了servlet的wrapper
wrapper.getPipeline().getFirst().invoke(request, response);
}
那麼看看這個wrapper
的值是什麼:StandardEngine[Catalina].StandardHost[localhost].StandardContext[/webmvc].StandardWrapper[springmvc]
,可以發現引擎是“Catalina”,虛擬主機是“localhost”,上下文是“webmvc”,進行處理的servlet是“springmvc”,那麼就沒錯了。放入tomcat的webapps目錄中的樣例工程,就是這個上下文,以及spring mvc
的servlet入口。
接下來,有了對應的wrapper之後,它還經過了過濾鏈進行一定的過濾filter
,最終在下面這個地方進行調用:
// org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// 真正調用servlet對請求做處理的位置,如果是spring mvc 這裏就是dispatcherServlet對象
servlet.service(request, response);
}
- DEBUG可以看到這個
servlet
實例的值是DispatcherServlet
,同時也可以看到它的上下文對象是spring
的上下文,如下圖:
那麼,接下來請求的處理就是調用spring mvc
進行完成了,tomcat對請求的處理分發就完成了!
結語
Tomcat的一次請求走的代碼看起來很長,實際上了解其架構之後,邏輯還是十分清晰的。
本次在Tomcat源碼的入門學習中,有以下體會:
- Tomcat的架構清晰,多層嵌套,看起來複雜,實際上就是剝洋蔥的行爲,看源碼的過程根據DEBUG來追蹤就可以清晰的一層一層的進去。
- 看源碼DEBUG真的很重要!
- 瞭解架構,從宏觀出發,不抓細枝末節,是快速看完源碼,瞭解主要思想的重要途徑。過分追求每一行代碼的行爲容易忘了自己前面看過的,以及你到底想看的是什麼。
- 瞭解完請求過程,那麼對一次請求最終調用之前打一個Log日誌,做統計等等都是可以完成的了~
推廣
推廣我一分錢也沒得賺!!!!
喝水不忘打井人吧,這個是在拉勾教育上看的《Tomcat核心源碼剖析》,應癲出品。只有三天的課程,第三天還會推薦你報班(手動滑稽~)。
槽點: 這是個“訓練營”課程,18元人民幣,有效期18天。到期就沒得看了,也督促我三天看完,五天內完成筆記~