深入理解 Tomcat(八)源碼剖析之連接器

這是我們分析tomcat的第八篇文章,這次我們分析連接器,我們?cè)缇拖敕治鲞B接器了,因?yàn)楦鞣N原因拖了好久。不過(guò)也確實(shí)復(fù)雜。

首先我們之前定義過(guò)連接器:

Tomcat都是在容器里面處理問(wèn)題的, 而容器又到哪里去取得輸入信息呢? Connector就是專干這個(gè)的。 他會(huì)把從socket傳遞過(guò)來(lái)的數(shù)據(jù), 封裝成Request, 傳遞給容器來(lái)處理。 通常我們會(huì)用到兩種Connector,一種叫http connectoer, 用來(lái)傳遞http需求的。 另一種叫AJP, 在我們整合apache與tomcat工作的時(shí)候,apache與tomcat之間就是通過(guò)這個(gè)協(xié)議來(lái)互動(dòng)的。 (說(shuō)到apache與tomcat的整合工作, 通常我們的目的是為了讓apache 獲取靜態(tài)資源, 而讓tomcat來(lái)解析動(dòng)態(tài)的jsp或者servlet。)

簡(jiǎn)單來(lái)說(shuō),連接器就是接收http請(qǐng)求并解析http請(qǐng)求,然后將請(qǐng)求交給servlet容器。

那么在 Tomcat中 ,那個(gè)類表示連接器呢? 答案是 org.apache.catalina.connector.Connector,該類繼承自 LifecycleMBeanBase, 也就是說(shuō),該類的生命周期歸屬于容器管理。而該類的父容器是誰(shuí)呢? 答案是 org.apache.catalina.core.StandardService,也就是我們的Service 組件,StandardService是該接口的標(biāo)準(zhǔn)實(shí)現(xiàn)。StandardService 聚合了 Connector 數(shù)組和一個(gè)Container 容器,也就驗(yàn)證了我們之前說(shuō)的一個(gè)Service 組件中包含一個(gè)Container和多個(gè)連接器。

那么連接器什么時(shí)候初始化被放入容器和JMX呢?這是個(gè)大問(wèn)題,也是我們今天的主要問(wèn)題。

1. Tomcat 解析 server.xml 并創(chuàng)建對(duì)象

我們之前扒過(guò)啟動(dòng)源碼,我們知道,在Catalina 的 load 方法中是初始化容器的方法,所有的容器都是在該方法中初始化的。Connector 也不例外。我們還記得 Tomcat 的conf 目錄下的server.xml 文件嗎?

<?xml version='1.0' encoding='utf-8'?>

<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />

  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />

  <Listener className="org.apache.catalina.core.JasperListener" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>
  <Service name="Catalina">
    <Connector port="8061" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

    <Engine name="Catalina" defaultHost="localhost">

      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log." suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

      </Host>
    </Engine>
  </Service>
</Server>

可以看到該配置文件中有2個(gè)Connector 標(biāo)簽,有就是說(shuō)默認(rèn)有2個(gè)連接器。一個(gè)是HTTP協(xié)議,一個(gè)AJP協(xié)議。

我們的Connector是什么時(shí)候創(chuàng)建的呢?就是在解析這個(gè)xml文件的時(shí)候,那么是怎么解析的呢?我們平時(shí)在解析 xml 的時(shí)候經(jīng)常使用dom4j(真的不喜歡xml,最愛(ài)json),而tomcat 使用的是 Digester 解析xml,我們來(lái)看看 Catalina.load() 關(guān)于解析并創(chuàng)建容器的關(guān)鍵代碼:

    public void load() {
        // Create and execute our Digester
        Digester digester = createStartDigester();
        digester.parse(inputSource);
    }

首先創(chuàng)建一個(gè) Digester, 其中的關(guān)鍵代碼我們看看:

Digester digester = new Digester();

digester.addRule("Server/Service/Connector",
                         new ConnectorCreateRule());
digester.addRule("Server/Service/Connector",
                         new SetAllPropertiesRule(new String[]{"executor"}));
digester.addSetNext("Server/Service/Connector",
                            "addConnector",
                            "org.apache.catalina.connector.Connector");

上面的代碼的意思是將對(duì)應(yīng)的字符串創(chuàng)建成對(duì)應(yīng)的角色,以便后面和xml對(duì)應(yīng)便解析。

我們?cè)倏纯此侨绾谓馕龅?,由?digester.parse(inputSource) 這個(gè)方法調(diào)用層次太深,而且該方法只是解析xml,因此樓主把就不把每段代碼貼出來(lái)了,我們看看IDEA生成的該方法的方法調(diào)用棧:這些方法都是在 rt.jar 包中,因此我們不做分析了。主要就是解析xml。

上圖是樓主在 Digester.startDocument 的方法中打的斷點(diǎn)。該方法作用為開(kāi)始解析 xml 做準(zhǔn)備。

上圖是樓主在 Digester.startElement 的方法中打的斷點(diǎn),startDocument 和 startElement 是多次交替執(zhí)行的,確定他們執(zhí)行邏輯的是什么方法呢?從堆棧圖中我們可以看到:是 com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse()這個(gè)方法,代碼我就不貼出來(lái)了,很長(zhǎng)沒(méi)有意義,該方法在 819行間接調(diào)用 startDocument,在841行間接調(diào)用startElement。上下執(zhí)行,并且回執(zhí)行多次。因?yàn)?xml 會(huì)解析多次嘛。

我們重點(diǎn)說(shuō)說(shuō) startElement 方法:

public void startElement(String namespaceURI, String localName,
                             String qName, Attributes list) {
 List<Rule> rules = getRules().match(namespaceURI, match);
        matches.push(rules);
        if ((rules != null) && (rules.size() > 0)) {
            for (int i = 0; i < rules.size(); i++) {
                try {
                    Rule rule = rules.get(i);
                    if (debug) {
                        log.debug("  Fire begin() for " + rule);
                    }
                    rule.begin(namespaceURI, name, list);
                } catch (Exception e) {
                    log.error("Begin event threw exception", e);
                    throw createSAXException(e);
                } catch (Error e) {
                    log.error("Begin event threw error", e);
                    throw e;
                }
            }
        } 
}

樓主只貼了關(guān)鍵代碼,就是for循環(huán)中的邏輯,便利 Rule 集合,Rule 是什么呢?是我們之前 createStartDigester 方法里創(chuàng)建的。而 Rule 是一個(gè)接口,tomcat 中有很多不同的實(shí)現(xiàn)。然后循環(huán)調(diào)用他們的 begin 方法。我們看看有哪些實(shí)現(xiàn):

$R_$UQLJE)Z0{F~C0(KVWIG.png

我們從上圖中看到了有很多的實(shí)現(xiàn),而我們今天只關(guān)注連接器:也就是 ConnectorCreateRule 的 begin 方法:

@Override
    public void begin(String namespace, String name, Attributes attributes)
            throws Exception {
        Service svc = (Service)digester.peek();
        Executor ex = null;
        if ( attributes.getValue("executor")!=null ) {
            ex = svc.getExecutor(attributes.getValue("executor"));
        }
        Connector con = new Connector(attributes.getValue("protocol"));
        if ( ex != null )  _setExecutor(con,ex);
        
        digester.push(con);
    }
    

方法不長(zhǎng),我們看看該方法邏輯,該方法首先從List中取出一個(gè)Service,然后會(huì)分別創(chuàng)建2個(gè)連接器,一個(gè)是HTTP, 一個(gè)是AJP,也就是我們配置文件中寫(xiě)的。

現(xiàn)在,我們已經(jīng)剖析了tomcat 是如何解析xml的,并如何創(chuàng)建對(duì)象的,接下來(lái),我們就看看創(chuàng)建對(duì)象的邏輯。

2. 創(chuàng)建連接器對(duì)象

我們來(lái)到我們的Connector類的構(gòu)造方法:

   public Connector() {
        this(null);
    }

    public Connector(String protocol) {
        setProtocol(protocol);
        // Instantiate protocol handler
        try {
            Class<?> clazz = Class.forName(protocolHandlerClassName);
            this.protocolHandler = (ProtocolHandler) clazz.newInstance();// 反射創(chuàng)建protocolHandler 默認(rèn) http1.1 協(xié)議實(shí)現(xiàn) (org.apache.coyote.http11.Http11Protocol)
        } catch (Exception e) {
            log.error(sm.getString(
                    "coyoteConnector.protocolHandlerInstantiationFailed"), e);
        }
    }

我記得阿里規(guī)約里說(shuō),構(gòu)造器不要太復(fù)雜,復(fù)雜的邏輯請(qǐng)放在init里,不知道tomcat這么寫(xiě)算好還是不好呢?嘿嘿。我們還是來(lái)看看我們的邏輯吧。

  1. 根據(jù)傳進(jìn)來(lái)的字符串設(shè)置協(xié)議處理器類名(setProtocol 方法中調(diào)用了setProtocolHandlerClassName 方法)。
  2. 根據(jù)剛剛設(shè)置好的 protocolHandlerClassName 反射創(chuàng)建 ProtocolHandler 類型的對(duì)象。

既然是反射創(chuàng)建,那么我們就要看看完整的類名是什么了,所以需要看看設(shè)置 protocolHandlerClassName 方法的細(xì)節(jié):

public void setProtocol(String protocol) {

        if (AprLifecycleListener.isAprAvailable()) {
            if ("HTTP/1.1".equals(protocol)) {
                setProtocolHandlerClassName
                    ("org.apache.coyote.http11.Http11AprProtocol");
            } else if ("AJP/1.3".equals(protocol)) {
                setProtocolHandlerClassName
                    ("org.apache.coyote.ajp.AjpAprProtocol");
            } else if (protocol != null) {
                setProtocolHandlerClassName(protocol);
            } else {
                setProtocolHandlerClassName
                    ("org.apache.coyote.http11.Http11AprProtocol");
            }
        } else {
            if ("HTTP/1.1".equals(protocol)) {
                setProtocolHandlerClassName
                    ("org.apache.coyote.http11.Http11Protocol");
            } else if ("AJP/1.3".equals(protocol)) {
                setProtocolHandlerClassName
                    ("org.apache.coyote.ajp.AjpProtocol");
            } else if (protocol != null) {
                setProtocolHandlerClassName(protocol);
            }
        }

    }

此方法會(huì)直接進(jìn)入下面的else塊,我們知道,該處可能會(huì)傳 HTTP 或者 AJP ,根據(jù)不同的協(xié)議創(chuàng)建不同的協(xié)議處理器。也就是連接器,我們看到這里的全限定名是 org.apache.coyote.http11.Http11Protocol 或者 org.apache.coyote.ajp.AjpProtocol,這兩個(gè)類都是在 coyote 包下,也就是連接器模塊。

好了,到現(xiàn)在,我們的 Connector 對(duì)象就創(chuàng)建完畢了,創(chuàng)建它的過(guò)程同時(shí)也根據(jù)配置文件創(chuàng)建了 protocolHandler, 他倆是依賴關(guān)系。

3. Http11Protocol 協(xié)議處理器構(gòu)造過(guò)程

創(chuàng)建了 Http11Protocol 對(duì)象,我們有必要看看他的構(gòu)造過(guò)程是什么樣的。按照tomcat的性格,一般構(gòu)造器都很復(fù)雜,所以,我們找到該類,看看他的類和構(gòu)造器:

該類的類說(shuō)明是這樣說(shuō)的:

抽象協(xié)議的實(shí)現(xiàn),包括線程等。處理器是單線程的,特定于基于流的協(xié)議,不適合像JNI那樣的Jk協(xié)議。

我們看看構(gòu)造方法

    public Http11Protocol() {
        endpoint = new JIoEndpoint();
        cHandler = new Http11ConnectionHandler(this);
        ((JIoEndpoint) endpoint).setHandler(cHandler);
        setSoLinger(Constants.DEFAULT_CONNECTION_LINGER);
        setSoTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
        setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY);
    }

我就說(shuō)嘛,肯定和復(fù)雜。復(fù)雜也要看啊。

  1. 創(chuàng)建以了一個(gè) JIoEndpoint 對(duì)象。
  2. 創(chuàng)建了一個(gè) Http11ConnectionHandler 對(duì)象,參數(shù)是 Http11Protocol;
  3. 設(shè)置處理器為 Http11ConnectionHandler 對(duì)象。
  4. 設(shè)置一些屬性,比如超時(shí),優(yōu)化tcp性能。

那么我們來(lái)看看 JIoEndpoint 這個(gè)類,這個(gè)類是什么玩意,如果大家平時(shí)調(diào)試tomcat比較多的話,肯定會(huì)熟悉這個(gè)類,樓主今天就遇到了,請(qǐng)看:

    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1041)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:603)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:748)

異常信息,我們看倒數(shù)第四行,就是 JIoEndpoint 的內(nèi)部類 SocketProcessor 的 run 方法報(bào)錯(cuò)了,我們今天就親密接觸一下這個(gè)類,順便扒了它的衣服:

赤裸裸的在我們面前。所有錯(cuò)誤的根源都在該方法中。

不扯了,我們繼續(xù)看 JIoEndpoint 的構(gòu)造器,該構(gòu)造器很簡(jiǎn)單,就是設(shè)置最大連接數(shù)。默認(rèn)是0,我們看代碼:


上圖中什么看到該方法將 maxConnections 設(shè)置為0,本來(lái)是10000,然后進(jìn)入else if(maxCon > 0) 的邏輯。這里也就完成了 JIoEndpoint 對(duì)象的創(chuàng)建過(guò)程。

我們回到 Http11Protocol 的構(gòu)造方法中,執(zhí)行完了 JIoEndpoint 的創(chuàng)建過(guò)程,下面就執(zhí)行 Http11ConnectionHandler 的構(gòu)造。參數(shù)是Http11Protocol自己,Http11ConnectionHandler 是 Http11Protocol 的靜態(tài)內(nèi)部類,該類中有一個(gè)屬性就是Http11Protocol,一個(gè)簡(jiǎn)單的創(chuàng)建過(guò)程,然后設(shè)置 Http11Protocol 的 Handler 屬性為 Http11ConnectionHandler。可以感覺(jué)的到,Http11Protocol, JIoEndpoint , Http11ConnectionHandler 這三個(gè)類是互相依賴關(guān)系。

至此,完成了 Http11Protocol 對(duì)象的創(chuàng)建。同時(shí)也完成了 Connector 對(duì)象的創(chuàng)建。 創(chuàng)建完對(duì)象干嘛呢。。。。不要想歪了,不是啪啪啪,而是初始化。

4. Connector 連接器的初始化 init 方法

我們知道 Connector 的父容器是 Service ,Service 執(zhí)行 initInternal 方法初始化的時(shí)候會(huì)同時(shí)初始化子容器,也就是 Connector,在一個(gè) for 循環(huán)重啟動(dòng)。

該段代碼抽取自 StandardService.initInternal 方法,也就是Service 組件。通過(guò)debug我們知道了該連接器數(shù)組中只有2個(gè)連接器,就是我們的HTTP和AJP,剛剛創(chuàng)建的。并調(diào)用他們的 init 方法。我們看看該方法,該方法同所有容器一樣,執(zhí)行了LifecycleBase 的模板方法,重點(diǎn)在子類重寫(xiě)的抽象方法 initInternal 中。

這既是 Connector 的 initInternal 方法實(shí)現(xiàn),該方法有幾個(gè)步驟:

  1. 調(diào)用父類的 initInternal 方法,將自己注冊(cè)到JMX中。
  2. 創(chuàng)建一個(gè) CoyoteAdapter 對(duì)象,參數(shù)是自己。
  3. 設(shè)置 Http11Protocol 的適配器為剛剛創(chuàng)建的 CoyoteAdapter 適配器。
  4. 設(shè)置解析請(qǐng)求的請(qǐng)求方法類型,默認(rèn)是 POST。
  5. 初始化 Http11Protocol(不要小看這個(gè)類,Connector就是一個(gè)虛的,真正做事的就是這個(gè)類和 JIoEndpoint);
  6. 初始化 mapperListener;

我們重點(diǎn)關(guān)注 CoyoteAdapter 和 Http11Protocol 的初始化,CoyoteAdapter 是連接器的一種適配,構(gòu)造參數(shù)是 Connector ,很明顯,他是要適配 Connector,這里的設(shè)計(jì)模式就是適配器模式了,所以,寫(xiě)設(shè)計(jì)模式的時(shí)候,一定要在類名上加上設(shè)計(jì)模式的名字。方便后來(lái)人讀代碼。接下就是設(shè)置 Http11Protocol 的適配器為 剛剛構(gòu)造的 CoyoteAdapter ,也就是說(shuō),tomcat 的設(shè)計(jì)者為了解耦或者什么將 Http11Protocol 和 Connector 中間插入了一個(gè)適配器。最后來(lái)到我們的 Http11Protocol 的初始化。

這個(gè) Http11Protocol 的初始化很重要。Http11Protocol 不屬于 Lifecycle 管理,他的 init 方法在他的抽象父類 org.apache.coyote.AbstractProtocol 中就已經(jīng)寫(xiě)好了,我們來(lái)看看該方法的實(shí)現(xiàn)(很重要):

上圖就是 AbstractProtocol 的 init 方法,我們看看紅框中的邏輯。

  1. 將 endpoint 注冊(cè)到JMX中。
  2. 將 Http11ConnectionHandler(Http11Protocol 的 內(nèi)部類)注冊(cè)到JMX中。
  3. 設(shè)置 endpoint 的名字,Http 連接器是 http-bio-8080;
  4. endpoint 初始化。

設(shè)置JMX的邏輯我們就不講了,之前講生命周期的時(shí)候講過(guò)了,設(shè)置名字也沒(méi)生命好講的。最后講最重要的 endpoint 的初始化。我們來(lái)看看他的 init 方法。該方法是 JIoEndpoint 抽象父類 AbstractEndpoint 的模板方法。該類被3個(gè)類繼承:AprEndpoint, JIoEndpoint, NioEndpoint,我們今天只關(guān)心JIoEndpoint。我們還是先看看 AbstractEndpoint 的 init 方法吧:

其中 bind()是抽象方法,然后設(shè)置狀態(tài)為綁定已經(jīng)初始化。我們看看 JIoEndpoint 的 bind 方法。有興趣也可以看看其他 Endpoint 的 bind 方法,比如NIO。我們看看JIo的:

這個(gè)方法很重要,我們仔細(xì)看看邏輯:

  1. 設(shè)置最大線程數(shù),默認(rèn)是200;
  2. 創(chuàng)建一個(gè)默認(rèn)的 serverSocketFactory 工廠(就是一個(gè)封裝了ServerSocket 的類);
  3. 使用剛剛工廠創(chuàng)建一個(gè) serverSocket。因此,JIoEndpoint 也就有了 serverSocket。

至此,我們完成了 Connector, Http11Protocol,JIoEndpoint 的初始化。

接下來(lái)就是啟動(dòng)了

5. 連接器啟動(dòng)

如我們所知,Connector 啟動(dòng)肯定在 startInternal 方法中,因此我們直接看此方法。

該方法步驟如下:

  1. 設(shè)置啟動(dòng)中狀態(tài)。狀態(tài)更新會(huì)觸發(fā)事件監(jiān)聽(tīng)機(jī)制。
  2. 啟動(dòng) org.apache.coyote.http11.Http11Protocol 的 srart 方法。
  3. 啟動(dòng) org.apache.catalina.connector.MapperListener 的 start 方法。

我們感興趣的是 org.apache.coyote.http11.Http11Protocol 的 srart 方法。該方法由其抽象父類 AbstractProtocol.start 執(zhí)行,我們看看該方法:

該方法主要邏輯是啟動(dòng) endpoint 的 start 方法。說(shuō)明干事的還是 endpoint 啊 ,我們看看該方法實(shí)現(xiàn),該方法調(diào)用了抽象父類的模板方法 AbstractEndpoint.start:

其主要邏輯是調(diào)用子類重寫(xiě)的 startInternal 方法,我們來(lái)看 JIoEndpoint 的實(shí)現(xiàn):

該方法可以說(shuō)是 Tomcat 中 真正做事情的方法,絕對(duì)不是摸魚(yú)員工。說(shuō)說(shuō)他的邏輯:

  1. 創(chuàng)建一個(gè)線程阻塞隊(duì)列,和一個(gè)線程池。
  2. 初始化最大連接數(shù),默認(rèn)200.
  3. 調(diào)用抽象父類 AbstractEndpoint 的 startAcceptorThreads 方法,默認(rèn)創(chuàng)建一個(gè)守護(hù)線程。他的任務(wù)是等待客戶端請(qǐng)求,并將請(qǐng)求(socket 交給線程池)。AbstractEndpoint 中有一個(gè) Acceptor 數(shù)組,作用接收新的連接和傳遞請(qǐng)求。
  4. 創(chuàng)建一個(gè)管理超時(shí)socket 的線程。

讓我們看看他的詳細(xì)實(shí)現(xiàn):

6. JIoEndpoint startInternal(Tomcat socket 管理) 方法的詳細(xì)實(shí)現(xiàn)

先看第一步:創(chuàng)建一個(gè)線程阻塞隊(duì)列,和一個(gè)線程池。我們進(jìn)入該方法:

該方法步驟:

  1. 創(chuàng)建一個(gè) “任務(wù)隊(duì)列”,實(shí)際上是一個(gè)繼承了 LinkedBlockingQueue<Runnable> 的類。該隊(duì)列最大長(zhǎng)度為 int 最大值 0x7fffffff。
  2. 創(chuàng)建一個(gè)線程工廠,TaskThreadFactory 是一個(gè)繼承 ThreadFactory 的類,默認(rèn)創(chuàng)建最小線程 10, 最大線程200, 名字為 “http-bio-8080-exec-” 拼接線程池編號(hào),優(yōu)先級(jí)為5。
  3. 使用上面的線程工廠創(chuàng)建一個(gè)線程池,預(yù)先創(chuàng)建10個(gè)線程,最大線程200,線程空閑實(shí)際60秒.
  4. 將線程池設(shè)置為隊(duì)列的屬性,方便后期判斷線程池狀態(tài)而做一些操作。

再看第二步:初始化最大連接數(shù),默認(rèn)200.

該方法很簡(jiǎn)單,就是設(shè)置最大連接數(shù)為200;

第三步:用抽象父類 AbstractEndpoint 的 startAcceptorThreads 方法,默認(rèn)創(chuàng)建一個(gè)守護(hù)線程。他的任務(wù)是等待客戶端請(qǐng)求,并將請(qǐng)求(socket 交給線程池)。AbstractEndpoint 中有一個(gè) Acceptor 數(shù)組,作用接收新的連接和傳遞請(qǐng)求。我們看看該方法:

步驟:

  1. 獲取可接收的連接數(shù),并創(chuàng)建一個(gè)連接線程數(shù)組。
  2. 循環(huán)該數(shù)組,設(shè)置優(yōu)先級(jí)為5,設(shè)置為守護(hù)線程。
    3.啟動(dòng)該線程。

該方法也不是很復(fù)雜,獲取的這個(gè)連接數(shù),在完美初始化的時(shí)候,調(diào)用bind 方法的時(shí)候設(shè)置的,請(qǐng)看:

設(shè)置為1.

復(fù)雜的是 Acceptor 中的邏輯,Acceptor 是一個(gè)抽象靜態(tài)內(nèi)部類,實(shí)現(xiàn)了 Runnable 接口,JIoEndpoint 類中也繼承了該類,其中 run 方法如下(高能預(yù)警,方法很長(zhǎng))。

        @Override
        public void run() {

            int errorDelay = 0;

            // Loop until we receive a shutdown command
            while (running) {

                // Loop if endpoint is paused
                while (paused && running) {
                    state = AcceptorState.PAUSED;
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                if (!running) {
                    break;
                }
                state = AcceptorState.RUNNING;

                try {
                    //if we have reached max connections, wait
                    countUpOrAwaitConnection();

                    Socket socket = null;
                    try {
                        // Accept the next incoming connection from the server
                        // socket
                        socket = serverSocketFactory.acceptSocket(serverSocket);
                    } catch (IOException ioe) {
                        countDownConnection();
                        // Introduce delay if necessary
                        errorDelay = handleExceptionWithDelay(errorDelay);
                        // re-throw
                        throw ioe;
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;

                    // Configure the socket
                    if (running && !paused && setSocketOptions(socket)) {
                        // Hand this socket off to an appropriate processor
                        if (!processSocket(socket)) {
                            countDownConnection();
                            // Close socket right away
                            closeSocket(socket);
                        }
                    } else {
                        countDownConnection();
                        // Close socket right away
                        closeSocket(socket);
                    }
                } catch (IOException x) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), x);
                    }
                } catch (NullPointerException npe) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), npe);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
            }
            state = AcceptorState.ENDED;
        }

其實(shí)邏輯也還好,不是那么復(fù)雜,我們化整為零,一個(gè)一個(gè)分析,首先判斷狀態(tài),進(jìn)入循環(huán),然后設(shè)置一些狀態(tài),最后進(jìn)入一個(gè) try 塊。

  1. 執(zhí)行 countUpOrAwaitConnection 方法,該方法注釋說(shuō):如果已經(jīng)達(dá)到最大連接,就等待。
  2. 阻塞獲取socket。
  3. setSocketOptions(socket) 設(shè)置 tcp 一些屬性優(yōu)化性能,比如緩沖字符大小,超時(shí)等。
  4. 執(zhí)行 processSocket(socket) 方法,將請(qǐng)求包裝一下交給線程池執(zhí)行。

我們看看第一個(gè)方法 countUpOrAwaitConnection:

主要是 latch.countUpOrAwait() 這個(gè)方法,我們看看該方法內(nèi)部實(shí)現(xiàn):

這個(gè) Sync 類 變量是 繼承了java.util.concurrent.locks.AbstractQueuedSynchronizer 抽象類(該類是JDK 1.8 新增的),說(shuō)實(shí)話,樓主不熟悉這個(gè)類。再一個(gè)今天的主題也不是并發(fā),因此放過(guò)這個(gè)類,給大家一個(gè)鏈接 深度解析Java 8:AbstractQueuedSynchronizer的實(shí)現(xiàn)分析(下);
我們暫時(shí)知道這個(gè)方法作用是什么就行了,就像注釋說(shuō)的:如果已經(jīng)達(dá)到最大連接,就等待。我們繼續(xù)我們的分析。

我們跳過(guò)設(shè)置 tcp 優(yōu)化,重點(diǎn)查看 processSocket 方法,這個(gè)方法是 JIoEndpoint 的,我們看看該方法實(shí)現(xiàn):

該方法邏輯是:

  1. 封裝一個(gè) SocketWrapper。
  2. 設(shè)置長(zhǎng)連接時(shí)間為100,
    3.然后封裝成 SocketProcessor(還記得這個(gè)類嗎,就是我們剛開(kāi)始異常信息里出現(xiàn)的類,原來(lái)是在這里報(bào)錯(cuò)的,哈哈) 交給線程池執(zhí)行。

到這里,我們必須停下來(lái),因?yàn)槿绻^續(xù)追蹤 SocketProcessor 這個(gè)類,這篇文章就停不下來(lái)了,樓主想留在下一篇文章慢慢咀嚼。慢慢品味。

第四步:好了,回到我們的 JIoEndpoint.startInternal 方法,我們已經(jīng)解析完了 startAcceptorThreads 方法,那么我們繼續(xù)向下走,看到一個(gè) timeoutThread 線程。創(chuàng)建一個(gè)管理超時(shí)socket 的線程。設(shè)置為了守護(hù)線程,名字叫 “http-bio-8080-AsyncTimeout”,優(yōu)先級(jí)為5.

我們看看該程序的實(shí)現(xiàn),還好,代碼不多:

我們看看主要邏輯:

  1. 獲取當(dāng)前時(shí)間;
  2. waitingRequests 的類型是 ConcurrentLinkedQueue<SocketWrapper<Socket>>,一個(gè)并發(fā)安全的阻塞對(duì)壘,里面有包裝過(guò)的 SocketWrapper。
  3. 判斷如果該隊(duì)列中有,則取出,判斷如果該socket 設(shè)定的超時(shí)時(shí)間大于0(默認(rèn)-1),且當(dāng)前時(shí)間大于訪問(wèn)時(shí)間,則交給線程池處理。

那么什么時(shí)候會(huì)往該 waitingRequests 里添加呢?我們看過(guò)之前的 SocketProcessor. run 方法, 如果 SocketState 的狀態(tài)是LONG,就設(shè)置該 socket 的訪問(wèn)時(shí)間為當(dāng)前時(shí)間,并添加進(jìn)超時(shí)隊(duì)列。而這個(gè)超時(shí)的判斷也非常的復(fù)雜,想想也對(duì),任何一個(gè)連接都不能隨意丟棄。所以需要更嚴(yán)謹(jǐn)?shù)膶?duì)待,萬(wàn)一是個(gè)支付請(qǐng)求呢?

好了,JIoEndpoint 的 startInternal 方法已經(jīng)執(zhí)行完畢,總結(jié)一下該方法:創(chuàng)建線程池,初始化最大連接數(shù),啟動(dòng)接收請(qǐng)求線程,設(shè)置超時(shí)線程。可以說(shuō)tomcat 考慮的很周到。很完美。不過(guò)還有更完美的NIO還沒(méi)有體驗(yàn)。

7. 總結(jié)

今天的文章真是超長(zhǎng)啊,誰(shuí)叫連接器是tomcat中最重要的組件呢?其實(shí)還沒(méi)有講完呢;我們講了連接器的如何根據(jù)server.xml 創(chuàng)建對(duì)象,如何初始化connector 和 endpoint ,如何啟動(dòng)connector,如何啟動(dòng) endpoint中各個(gè)線程。樓主能力有限,暫時(shí)先講這么多,還有很多的東西我們都還沒(méi)講,沒(méi)事,留著下次講。

天色已晚,樓主該回家了。各位再見(jiàn)?。。?!

good luck ?。。?!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,560評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,035評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,814評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,224評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,444評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,804評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,998評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,237評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,665評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,927評(píng)論 1 287
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,706評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,993評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容