目錄
Tomcat在啓動時,就已經將ServerSocket(或ServerSocketChannel等)初始化完畢,並啓動了Acceptor線程等待請求到達。
請求預處理
在Acceptor線程的run方法中,有如下代碼片段:
U socket = null;
try {
// Accept the next incoming connection from the server
// socket
socket = endpoint.serverSocketAccept();
} catch (Exception ioe) {
// 略
}
// Successful accept, reset the error delay
errorDelay = 0;
// Configure the socket
if (endpoint.isRunning() && !endpoint.isPaused()) {
// setSocketOptions() will hand the socket off to
// an appropriate processor if successful
if (!endpoint.setSocketOptions(socket)) {
endpoint.closeSocket(socket);
}
} else {
endpoint.destroySocket(socket);
}
關鍵語句就是 socket = endpoint.serverSocketAccept() 和 endpoint.setSocketOptions(socket)。
前者在NioEndpoint中,其實就是返回 serverSock.accept(),即獲取一個請求。
後者則是對請求對象進行封裝,在NioEndpoint中的實現如下:
protected boolean setSocketOptions(SocketChannel socket) {
NioSocketWrapper socketWrapper = null;
try {
// Allocate channel and wrapper
NioChannel channel = null;
if (nioChannels != null) {
channel = nioChannels.pop();
}
if (channel == null) {
SocketBufferHandler bufhandler = new SocketBufferHandler(
socketProperties.getAppReadBufSize(),
socketProperties.getAppWriteBufSize(),
socketProperties.getDirectBuffer());
if (isSSLEnabled()) {
channel = new SecureNioChannel(bufhandler, selectorPool, this);
} else {
channel = new NioChannel(bufhandler);
}
}
NioSocketWrapper newWrapper = new NioSocketWrapper(channel, this);
channel.reset(socket, newWrapper);
connections.put(socket, newWrapper);
socketWrapper = newWrapper;
// Set socket properties
// Disable blocking, polling will be used
socket.configureBlocking(false);
socketProperties.setProperties(socket.socket());
socketWrapper.setReadTimeout(getConnectionTimeout());
socketWrapper.setWriteTimeout(getConnectionTimeout());
socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
socketWrapper.setSecure(isSSLEnabled());
poller.register(channel, socketWrapper);
return true;
} catch (Throwable t) {
// 略
}
// Tell to close the socket if needed
return false;
}
可見是將請求封裝成一個SocketWrapper。值得注意的是,在Tomcat的啓動階段,ServerSocketChannel被設置爲同步(configureBlocking(true)),在這裏又設置爲false了,註釋也做了解釋,是爲了應用Poller來多線程處理請求。
封裝後,將SocketWrapper發送給Poller處理。register方法將NioChannel封裝爲一個PollerEvent對象,併爲SocketWrapper設置SelectionKey爲OP_READ:
public void register(final NioChannel socket, final NioSocketWrapper socketWrapper) {
socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
PollerEvent r = null;
if (eventCache != null) {
r = eventCache.pop();
}
if (r == null) {
r = new PollerEvent(socket, OP_REGISTER);
} else {
r.reset(socket, OP_REGISTER);
}
addEvent(r);
}
addEvent方法就是將PollerEvent塞進同步隊列中,並嘗試調用 selector.wakeup() 方法,喚醒阻塞在selector上的線程。那麼Poller線程就能獲取到這個PollerEvent,並能夠取出SocketWrapper,根據SelectionKey,調用了 processSocket(socketWrapper, SocketEvent.OPEN_READ, true) 來處理。
該方法會創建一個SocketProcessor,提交到線程池中負責處理該請求。這裏我們看NioEndpoint的實現,對於讀操作,有如下處理語句:
state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
追蹤Handler接口的實現類,指向了AbstractProtocol的內部類ConnectionHandler,下面對該方法的流程做介紹。
獲取請求處理器
首先,代碼嘗試從SocketWrapper中獲取Processor,Processor以及前面提到的Endpoint,還有後面會提到的ProtocolHandler、UpgradeProtocol等,都被封裝在Coyote包下,可以理解爲一個請求處理框架:
Processor processor = (Processor) wrapper.getCurrentProcessor();
在下面也能看到setCurrentProcessor的調用,說明Tomcat會把Processor緩存起來。
如果沒能獲取到Processor,說明可能是新請求,或者之前的Processor放了太久所以從緩存裏回收了。接下來就分三個if塊進行處理:
if (processor == null) {
String negotiatedProtocol = wrapper.getNegotiatedProtocol();
if (negotiatedProtocol != null && negotiatedProtocol.length() > 0) {
UpgradeProtocol upgradeProtocol = getProtocol().getNegotiatedProtocol(negotiatedProtocol);
if (upgradeProtocol != null) {
processor = upgradeProtocol.getProcessor(wrapper, getProtocol().getAdapter());
} else if (negotiatedProtocol.equals("http/1.1")) {
} else {
return SocketState.CLOSED;
}
}
}
第一個if塊,是用來進行協議升級的,之前有提到,Tomcat默認協議是HTTP 1.1,如果現在有HTTP 2.0或者WebSocket請求到達,HTTP 1.1的Endpoint肯定是無法獲取到合適的處理器的,這就需要嘗試升級協議,獲取更高級的處理器。
不過這裏從變量命名來看,是在協議已經經過協商,完成升級之後的處理。如果是第一次連接,還是會以HTTP 1.1的處理器先進行協商。
如果不需要升級,那麼就嘗試回收一個Processor:
if (processor == null) {
processor = recycledProcessors.pop();
if (getLog().isDebugEnabled()) {
getLog().debug(sm.getString("abstractConnectionHandler.processorPop", processor));
}
}
如果回收不到,說明這個URI的請求是第一次到達,那麼只能創建一個新的:
if (processor == null) {
processor = getProtocol().createProcessor();
register(processor);
if (getLog().isDebugEnabled()) {
getLog().debug(sm.getString("abstractConnectionHandler.processorCreate", processor));
}
}
然後set到SocketWrapper上緩存起來,方便下次再用。
然後就是調用Processor的process方法處理請求,OP_READ操作會使用service方法處理。
如果請求可以處理,則委託Adapter處理請求。
如果需要升級,則構造InternalHttpUpgradeHandler和UpgradeToken,返回SocketState.UPGRADING。此時會執行協議升級流程,釋放掉用來協商的HTTP 1.1處理器,然後根據UpgradeToken構造新的協議升級處理器。根據協議不同,有Internal和External兩種協議升級處理器,Internal負責處理 HTTP 2.0和WebSocket協議,External則是處理Tomcat不支持的其他協議。然後調用httpUpgradeHandler.init((WebConnection) processor)繼續處理。
對於HTTP 2.0,會委託StreamProcessor處理請求;對於WebSocket,會構造WebSocket會話,交由容器處理。
StreamProcessor和HTTP 1.1處理器一樣,最終也是交給Adapter來處理請求。
Adapter接口的實現類是CoyoteAdapter,負責將請求和響應轉發到Container。
它轉發的Request、Response,全限定名爲org.apache.coyote.Request和org.apache.coyote.Response,對於HTTP 1.1處理器,會創建空的請求和響應,對於HTTP 2.0,會從Stream中構造請求和響應。
但是這裏的請求和響應是Tomcat的,而不是Servlet規範的,所以CoyoteAdapter的service方法,首先要對它們做轉換。轉換過程比較簡單就不貼代碼了。
請求映射
完成Request和Response的轉換後,就需要將請求傳給合適的Container。解析URI,找到匹配的Container的過程就是請求映射。在CoyoteAdapter中,請求映射功能由postParseRequest方法完成,該方法大約300行,非常複雜。
在開始請求匹配之前,方法先對請求和響應做了一些處理,例如代理配置、響應頭、編碼等,還有從URI解析出路徑參數。
然後定義了三個變量:version、versionContext、mapRequired,分別代表:請求匹配的Web應用版本,此版本對應的Web應用,以及是否需要繼續通過循環做匹配。
在while循環中,通過下面一句進行匹配:
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
Mapper顧名思義,就是用來進行請求匹配的類,在org.apache.catalina.mapper包下,還有MappingData,代表匹配結果,以及在啓動階段見過的MapperListener,它負責監聽Container的addChild、removeChild等方法的事件,以便及時更新映射列表。
Mapper.map()方法也比較複雜,暫時先不看。退出map方法後,如果還是沒有找到匹配的Context,就會返回404:
if (request.getContext() == null) {
if (!response.isError()) {
response.sendError(404, "Not found");
}
return true;
}
然後爲其配置會話和Cookie,並設置mapRequired=false,準備退出循環。
接下來驗證結果,如果version不爲空,且versionContext和map方法找到的Context相同,則說明匹配正確。否則,需要把version和versionContext重置,並且用sessionId檢查所有匹配到的結果(MappingData.contexts),找到會話對應的Context作爲匹配結果。如果沒有會話ID,就不重新匹配。
接下來需要檢查Context'是否正在運行,例如資源發生變化導致重新部署,那麼Context此時就會是Pause狀態,無法正常提供服務。Tomcat會等待1秒,然後重新匹配。
到這一步,就找到了合適的Context,下面會處理重定向和過濾TRACE請求。
對於重定向請求,匹配結果MappingData的redirectPath會包含重定向路徑信息,如果不爲空,則組裝重定向路徑,然後通過response.sendRedirect進行重定向。
過濾Trace請求就是組裝不含TRACE的響應頭,然後返回405。
請求處理
匹配成功後,就可以通過Pipeline來將請求轉發給Container了:
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
getContainer方法返回Engine,所以是從Engine的第一個Valve開始,沿責任鏈逐步向下調用,直到Wrapper的最後一個Valve,即StandardWrapperValve。它會構造和執行FilterChain,最終調用servlet.service(request,response)方法,調用Servlet處理請求。
FilterChain的構造也很簡單,就是對Context中所有的Filter,用請求地址或Servlet名去測試是否能被過濾,能則添加到FilterChain中。
FilterChain的doFilter方法調用internalDoFilter完成實際處理,其實就是去調用Filter的doFilter方法。當到達鏈的最後,說明碰到了Servlet,於是調用其service方法進行請求處理。
Tomcat默認的Servlet是DefaultServlet和JspServlet,前者主要用於處理靜態資源,後者用來訪問和編譯JSP。以DefaultServlet的doGet方法(處理GET請求)爲例,它就會根據請求路徑,找到對應的WebResource,配置MIME類型、LastModified、Content-Length等信息,然後將資源內容寫進響應中。
Mapper.map()
Tomcat只能有一個Engine,但可以有多個Host、Context、Wrapper。所以Container的匹配,必須從Host開始。
前面提到,MapperListener會處理addChild、removeChild等容器元素變更事件,註冊Container的映射關係,這種映射關係,是以Mapper內部類MappedHost、MappedContext等的形式維護的。MappedHost維護了屬於它的Context的列表篇,以及一個別名列表;MappedContext內部維護了一個ContextVersion[]數組,ContextVersion又維護了一系列MappedWrapper數組、WebResourceRoot等:
protected static final class ContextVersion extends MapElement<Context> {
public final String path;
public final int slashCount;
public final WebResourceRoot resources;
public String[] welcomeResources;
public MappedWrapper defaultWrapper = null;
public MappedWrapper[] exactWrappers = new MappedWrapper[0];
public MappedWrapper[] wildcardWrappers = new MappedWrapper[0];
public MappedWrapper[] extensionWrappers = new MappedWrapper[0];
public int nesting = 0;
private volatile boolean paused;
...
}
這四類MappedWrapper在匹配時有不同的優先級,下面再介紹。
// Virtual host mapping
MappedHost[] hosts = this.hosts;
MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
if (mappedHost == null) {
// Note: Internally, the Mapper does not use the leading * on a
// wildcard host. This is to allow this shortcut.
int firstDot = host.indexOf('.');
if (firstDot > -1) {
int offset = host.getOffset();
try {
host.setOffset(firstDot + offset);
mappedHost = exactFindIgnoreCase(hosts, host);
} finally {
// Make absolutely sure this gets reset
host.setOffset(offset);
}
}
if (mappedHost == null) {
mappedHost = defaultHost;
if (mappedHost == null) {
return;
}
}
}
mappingData.host = mappedHost.object;
exactFindIgnoreCase(hosts, host)的第二個參數就是Host名稱,在server.xml,Host元素的name屬性配置。例如localhost。如果沒有找到,則嘗試縮短用於匹配的Host名稱。例如,虛擬主機名稱配置爲 baidu.com,一個www.baidu.com的請求過來,肯定是匹配不到的,縮短一節以後,變成baidu.com,就能匹配到了。
找到Host後,就可以從它的Context列表中,去匹配Context。Context就是Host後面,用"/"分隔的部分,例如一個URI: localhost/test/pic/1.jpg,localhost是它的Host名稱,那麼現在就是拿/test/pic/1.jpg這部分去匹配Context,而且要儘可能長地去匹配,即,如果有 /test 和 /test/pic 這兩個Context,會優先匹配長的那個:
MappedContext context = null;
while (pos >= 0) {
context = contexts[pos];
if (uri.startsWith(context.name)) {
length = context.name.length();
if (uri.getLength() == length) {
found = true;
break;
} else if (uri.startsWithIgnoreCase("/", length)) {
found = true;
break;
}
}
if (lastSlash == -1) {
lastSlash = nthSlash(uri, contextList.nesting + 1);
} else {
lastSlash = lastSlash(uri);
}
uri.setEnd(lastSlash);
pos = find(contexts, uri);
}
pos有兩種可能:
-1:代表沒有匹配到,此時直接選擇Context列表的第一個作爲結果
>=0:代表匹配到結果,還需要進一步處理,主要是處理多版本Context的問題
MappedContext中維護了ContextVersion數組,如果傳入的version不爲空,則會進行精確查找,否則選擇最新版本的。
最後,將找到的Context保存到MappingData。到這一步,Context的匹配也完成了。
最後就是匹配Wrapper,即Servlet。Servlet在配置時,是會配置servlet-mapping的,裏面的url-pattern就指明瞭這個Servlet能夠處理哪些資源。繼續用上面的例子,假設Context匹配掉了test,那麼URI還剩下/pic/1.jpg。
類似於Context的匹配,首先也是先獲取MappedWrapper列表。不過前面提到,ContextVersion維護了四類MappedWrapper,它們分別是:/
- defaultWrapper:默認,即url-pattern="/"
- exactWrapper:精確匹配
- wildcardWrapper:前綴加通配符匹配,例如url-pattern="/*"
- extensionWrapper:擴展名匹配,例如url-pattern="/*.html"
在匹配時,會按照:exactWrapper - wildcardWrapper - extensionWrapper - 歡迎文件(welcom-file-list配置,一般都是index.html、index.jsp之類的) - defaultWrapper的順序進行匹配。
如果匹配到了歡迎文件結果,且歡迎文件是物理存在的,會重新組裝URI,然後再按 exactWrapper - wildcardWrapper - extensionWrapper - defaultWrapper的順序匹配一次。
假設url-pattern被配置爲"/*.do",上面的 /pic/1.jpg 在匹配時,會匹配不到結果,那麼也會嘗試縮短匹配範圍,直到path爲空,那麼就返回defaultWrapper。