Tomcat學習(三):請求處理

目錄

請求預處理

獲取請求處理器

請求映射

請求處理

Mapper.map()


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。

發佈了84 篇原創文章 · 獲贊 12 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章