誰會成為數字貨幣遊戲的掌控者|改變比特幣的規則
Tomcat是有一系列邏輯模組組織而成,這些模組主要包括:
- 核心架構模組,例如Server,Service,engine,host和context及wrapper等
- 網路介面模組connector
- log模組
- session管理模組
- jasper模組
- naming模組
- JMX模組
- 許可權控制模組
- ……
這些模組會在相關的文件裡逐一描述,本篇文件以介紹核心架構模組為主。
核心架構模組說明
核心架構模組之間是層層包含關係。例如可以說Service是Server的子元件,Server是Service的父元件。在server.xml已經非常清晰的定義了這些元件之間的關係及配置。
需要強調的是Service中配置了實際工作的Engine,同時配置了用來處理時間業務的執行緒組Executor(如果沒有配置則用系統預設的WorkThread模式的執行緒組),以及處理網路socket的相關元件connector。詳細情況如圖所示。
圖中,1:n代表一對多的關係;1:1代表一對一的關係。
StandEngine, StandHost, StandContext及StandWrapper是容器,他們之間有互相的包含關係。例如,StandEngine是StandHost的父容器,StandHost是StandEngine的子容器。在StandService內還包含一個Executor及Connector。
1) Executor是執行緒池,它的具體實現是java的concurrent包實現的executor,這個不是必須的,如果沒有配置,則使用自寫的worker thread執行緒池
2) Connector是網路socket相關介面模組,它包含兩個物件,ProtocolHandler及Adapter
- ProtocolHandler是接收socket請求,並將其解析成HTTP請求物件,可以配置成nio模式或者傳統io模式
- Adapter是處理HTTP請求物件,它就是從StandEngine的valve一直呼叫到StandWrapper的valve
分層建模
對於上述的各個邏輯模組,理解起來可能比較抽象。其實一個伺服器無非是接受HTTP request,然後處理請求,產生HTTP response通過原有連線返回給客戶端(瀏覽器)。那為什麼會整出這麼多的模組進行處理,這些模組是不是有些多餘。
其實這些模組各司其職,我們從底層wrapper開始講解,一直上溯到頂層的server。這樣易於理解。通過這些描述,會發現這正是tomcat架構的高度模組化的體現。這些細分的模組,使得tomcat非常健壯,通過一些配置和模組定製化,可以很大限度的擴充套件tomcat。
首先,我們以一個典型的頁面訪問為例,假設訪問的URL是
引用 http://www.mydomain.com/app/index.html
詳細情況如圖所示。
- Wrapper封裝了具體的訪問資源,例如 index.html
- Context 封裝了各個wrapper資源的集合,例如 app
- Host 封裝了各個context資源的集合,例如 www.mydomain.com
按照領域模型,這個典型的URL訪問,可以解析出三層領域物件,他們之間互有隸屬關係。這是最基本的建模。從上面的分析可以看出,從wrapper到host是層層遞進,層層組合。那麼host 資源的集合是什麼呢,就是上面所說的engine。 如果說以上的三個容器可以看成是物理模型的封裝,那麼engine可以看成是一種邏輯的封裝。
好了,有了這一整套engine的支援,我們已經可以完成從engine到host到context再到某個特定wrapper的定位,然後進行業務邏輯的處理了(關於怎麼處理業務邏輯,會在之後的blog中講述)。就好比,一個酒店已經完成了各個客房等硬體設施的建設與裝修,接下來就是前臺接待工作了。
先說執行緒池,這是典型的執行緒池的應用。首先從執行緒池中取出一個可用執行緒(如果有的話),來處理請求,這個元件就是connector。它就像酒店的前臺服務員登記客人資訊辦理入住一樣,主要完成了HTTP訊息的解析,根據tomcat內部的mapping規則,完成從engine到host到context再到某個特定wrapper的定位,進行業務處理,然後將返回結果返回。之後,此次處理結束,執行緒重新回到執行緒池中,為下一次請求提供服務。
如果執行緒池中沒有空閒執行緒可用,則請求被阻塞,一直等待有空閒執行緒進行處理,直至阻塞超時。執行緒池的實現有executor及worker thread兩種。預設的是worker thread 模式。
至此,可以說一個酒店有了前臺接待,有了房間等硬體設施,就可以開始正式運營了。那麼把engine,處理執行緒池,connector封裝在一起,形成了一個完整獨立的處理單元,這就是service,就好比某個獨立的酒店。
通常,我們經常看見某某集團旗下酒店。也就是說,每個品牌有多個酒店同時運營。就好比tomcat中有多個service在獨自執行。那麼這多個service的集合就是server,就好比是酒店所屬的集團。
作用域
那為什麼要按層次分別封裝一個物件呢?這主要是為了方便統一管理。類似名稱空間的概念,在不同層次的配置,其作用域不一樣。以tomcat自帶的列印request與response訊息的RequestDumperValve為例。這個valve的類路徑是:
引用 org.apache.catalina.valves.RequestDumperValve
valve機制是tomcat非常重要的處理邏輯的機制,會在相關文件裡專門描述。 如果這個valve配置在server.xml的節點下,則其只打印出訪問這個app(my)的request與response訊息。
Xml程式碼
- <Hostname="localhost"appBase="webapps"
- unpackWARs="true"autoDeploy="true"
- xmlValidation="false"xmlNamespaceAware="false">
- <Contextpath="/my"docBase="/usr/local/tomcat/backup/my">
- <ValveclassName="org.apache.catalina.valves.RequestDumperValve"/>
- </Context>
- <Contextpath="/my2"docBase="/usr/local/tomcat/backup/my">
- </Context>
- </Host>
如果這個valve配置在server.xml的節點下,則其可以打印出訪問這個host下兩個app的request與response訊息。
Xml程式碼
- <Hostname="localhost"appBase="webapps"
- unpackWARs="true"autoDeploy="true"
- xmlValidation="false"xmlNamespaceAware="false">
- <ValveclassName="org.apache.catalina.valves.RequestDumperValve"/>
- <Contextpath="/my"docBase="/usr/local/tomcat/backup/my">
- </Context>
- <Contextpath="/my2"docBase="/usr/local/tomcat/backup/my">
- </Context>
- </Host>
在這裡貼一個預設的server.xml的配置,通過這些配置可以加深對tomcat核心架構分層模組的理解,關於tomcat的配置,在相關的文件裡另行說明。為了篇幅,我把裡面的註釋給刪了。
Xml程式碼
- <Serverport="8005"shutdown="SHUTDOWN">
- <ListenerclassName="org.apache.catalina.core.AprLifecycleListener"SSLEngine="on"/>
- <ListenerclassName="org.apache.catalina.core.JasperListener"/>
- <ListenerclassName="org.apache.catalina.mbeans.ServerLifecycleListener"/>
- <ListenerclassName="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
- <GlobalNamingResources>
- <Resourcename="UserDatabase"auth="Container"
- type="org.apache.catalina.UserDatabase"
- description="Userdatabasethatcanbeupdatedandsaved"
- factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
- pathname="conf/tomcat-users.xml"/>
- </GlobalNamingResources>
- <Servicename="Catalina">
- <Executorname="tomcatThreadPool"namePrefix="catalina-exec-"
- maxThreads="150"minSpareThreads="4"/>
- <Connectorport="80"protocol="HTTP/1.1"
- connectionTimeout="20000"
- redirectPort="7443"/>
- <Connectorport="7009"protocol="AJP/1.3"redirectPort="7443"/>
- <Enginename="Catalina"defaultHost="localhost">
- <RealmclassName="org.apache.catalina.realm.UserDatabaseRealm"
- resourceName="UserDatabase"/>
- <Hostname="localhost"appBase="webapps"
- unpackWARs="true"autoDeploy="true"
- xmlValidation="false"xmlNamespaceAware="false">
- <Contextpath="/my"docBase="/usr/local/tomcat/backup/my">
- </Context>
- </Host>
- </Engine>
- </Service>
- </Server>
-
Tomcat提供了engine,host,context及wrapper四種容器。在總體結構中已經闡述了他們之間的包含關係。這四種容器繼承了一個容器基類,因此可以定製化。當然,tomcat也提供了標準實現。
- Engine:org.apache.catalina.core.StandardEngine
- Host: org.apache.catalina.core.StandardHost
- Context:org.apache.catalina.core.StandardContext
- Wrapper:org.apache.catalina.core.StandardWrapper
所謂容器,就是說它承載了若干邏輯單元及執行時資料。好比,整個酒店是一個容器,它包含了各個樓層等設施;每個樓層也是容器,它包含了各個房間;每個房間也是容器,它包含了各種家電等等。
首先來看一下容器類的類結構。
基類ContainerBase
ContainerBase是個abstract基類。其類路徑為:- org.apache.catalina.core.ContainerBase
這裡只列出一些比較核心功能的元件及方法。需要注意的是,類中的方法及屬性很多,限於篇幅不全部列出來了。
Enigne
Engine是最頂層的容器,它是host容器的組合。其標準實現類為:- org.apache.catalina.core.StandardEngine
看一下StandardEngine的主要邏輯單元概念圖。
從圖中可以看出,engine有四大元件:- Cluster: 實現tomcat叢集,例如session共享等功能,通過配置server.xml可以實現,對其包含的所有host裡的應用有效,該模組是可選的。其實現方式是基於pipeline+valve模式的,有時間會專門整理一個pipeline+valve模式應用系列;
- Realm:實現使用者許可權管理模組,例如使用者登入,訪問控制等,通過通過配置server.xml可以實現,對其包含的所有host裡的應用有效,該模組是可選的;
- Pipeline:這裡簡單介紹下,之後會有專門文件說明。每個容器物件都有一個pipeline,它不是通過server.xml配置產生的,是必須有的。它就是容器物件實現邏輯操作的骨架,在pipeline上配置不同的valve,當需要呼叫此容器實現邏輯時,就會按照順序將此pipeline上的所有valve呼叫一遍,這裡可以參考責任鏈模式;
- Valve:實現具體業務邏輯單元。可以定製化valve(實現特定介面),然後配置在server.xml裡。對其包含的所有host裡的應用有效。定製化的valve是可選的,但是每個容器有一個預設的valve,例如engine的StandardEngineValve,是在StandardEngine裡自帶的,它主要實現了對其子host物件的StandardHostValve的呼叫,以此類推。
配置的例子有:- <Enginename="Catalina"defaultHost="localhost">
- <ValveclassName="MyValve0"/>
- <ValveclassName="MyValve1"/>
- <ValveclassName="MyValve2"/>
- ……
- <Hostname="localhost"appBase="webapps">
- </Host>
- </Engine>
需要注意的是,執行環境中,pipeline上的valve陣列按照配置的順序載入,但是無論有無配置定製化的valve或有多少定製化的valve,每個容器預設的valve,例如engine的StandardEngineValve,都會在陣列中最後一個。
Host
Host是engine的子容器,它是context容器的集合。其標準實現類為:- org.apache.catalina.core.StandardHost
StandardHost的核心模組與StandardEngine差不多。只是作用域不一樣,它的模組只對其包含的子context有效。除此,還有一些特殊的邏輯,例如context的部署。Context的部署還是比較多的,主要分為:- War部署
- 資料夾部署
- 配置部署等
有時間單獨再說吧。照例貼個核心模組概念圖。
Java程式碼
Context
Context是host的子容器,它是wrapper容器的集合。其標準實現類為:- org.apache.catalina.core.StandardContext
應該說StandardContext是tomcat中最大的一個類。它封裝的是每個web app。
看一下StandardContext的主要邏輯單元概念圖。
Pipeline,valve,realm與上面容器一樣,只是作用域不一樣,不多說了。- Manager: 它主要是應用的session管理模組。其主要功能是session的建立,session的維護,session的持久化(persistence),以及跨context的session的管理等。Manager模組可以定製化,tomcat也給出了一個標準實現;
- org.apache.catalina.session.StandardManager
manager模組是必須要有的,可以在server.xml中配置,如果沒有配置的話,會在程式裡生成一個manager物件。- Resources: 它是每個web app對應的部署結構的封裝,比如,有的app是tomcat的webapps目錄下的某個子目錄或是在context節點配置的其他目錄,或者是war檔案部署的結構等。它對於每個web app是必須的。
- Loader:它是對每個web app的自有的classloader的封裝。具體內容涉及到tomcat的classloader體系,會在一篇文件中單獨說明。Tomcat正是有一套完整的classloader體系,才能保證每個web app或是獨立運營,或是共享某些物件等等。它對於每個web app是必須的。
- Mapper:它封裝了請求資源URI與每個相對應的處理wrapper容器的對映關係。
以某個web app的自有的web.xml配置為例;- <servlet>
- <servlet-name>httpserver</servlet-name>
- <servlet-class>com.gearever.servlet.TestServlet</servlet-class>
- </servlet>
- <servlet-mapping>
- <servlet-name>httpserver</servlet-name>
- <url-pattern>/*.do</url-pattern>
- </servlet-mapping>
對於mapper物件,可以抽象的理解成一個map結構,其key是某個訪問資源,例如/*.do,那麼其value就是封裝了處理這個資源TestServlet的某個wrapper物件。當訪問/*.do資源時,TestServlet就會在mapper物件中定位到。這裡需要特別說明的是,通過這個mapper物件定位特定的wrapper物件的方式,只有一種情況,那就是在servlet或jsp中通過forward方式訪問資源時用到。例如,- request.getRequestDispatcher(url).forward(request,response)
關於mapper機制會在一篇文件中專門說明,這裡簡單介紹一下,方便理解。如圖所示。
Mapper物件在tomcat中存在於兩個地方(注意,不是說只有兩個mapper物件存在),其一,是每個context容器物件中,它只記錄了此context內部的訪問資源與相對應的wrapper子容器的對映;其二,是connector模組中,這是tomcat全域性的變數,它記錄了一個完整的對映對應關係,即根據訪問的完整URL如何定位到哪個host下的哪個context的哪個wrapper容器。
這樣,通過上面說的forward方式訪問資源會用到第一種mapper,除此之外,其他的任何方式,都是通過第二種方式的mapper定位到wrapper來處理的。也就是說,forward是伺服器內部的重定向,不需要經過網路介面,因此只需要通過記憶體中的處理就能完成。這也就是常說的forward與sendRedirect方式重定向區別的根本所在。
看一下request.getRequestDispatcher(url) 方法的原始碼。- publicRequestDispatchergetRequestDispatcher(Stringpath){
- //Validatethepathargument
- if(path==null)
- return(null);
- if(!path.startsWith("/"))
- thrownewIllegalArgumentException
- (sm.getString
- ("applicationContext.requestDispatcher.iae",path));
- //Getquerystring
- StringqueryString=null;
- intpos=path.indexOf('?');
- if(pos>=0){
- queryString=path.substring(pos+1);
- path=path.substring(0,pos);
- }
- path=normalize(path);
- if(path==null)
- return(null);
- pos=path.length();
- //UsethethreadlocalURIandmappingdata
- DispatchDatadd=dispatchData.get();
- if(dd==null){
- dd=newDispatchData();
- dispatchData.set(dd);
- }
- MessageBytesuriMB=dd.uriMB;
- uriMB.recycle();
- //Usethethreadlocalmappingdata
- MappingDatamappingData=dd.mappingData;
- //MaptheURI
- CharChunkuriCC=uriMB.getCharChunk();
- try{
- uriCC.append(context.getPath(),0,context.getPath().length());
- /*
- *Ignoreanytrailingpathparams(separatedby';')formapping
- *purposes
- */
- intsemicolon=path.indexOf(';');
- if(pos>=0&&semicolon>pos){
- semicolon=-1;
- }
- uriCC.append(path,0,semicolon>0?semicolon:pos);
- <spanstyle="color:#ff0000;">context.getMapper().map(uriMB,mappingData);</span>
- if(mappingData.wrapper==null){
- return(null);
- }
- /*
- *Appendanytrailingpathparams(separatedby';')thatwere
- *ignoredformappingpurposes,sothatthey'rereflectedinthe
- *RequestDispatcher'srequestURI
- */
- if(semicolon>0){
- uriCC.append(path,semicolon,pos-semicolon);
- }
- }catch(Exceptione){
- //Shouldneverhappen
- log(sm.getString("applicationContext.mapping.error"),e);
- return(null);
- }
- <spanstyle="color:#ff0000;">Wrapperwrapper=(Wrapper)mappingData.wrapper;</span>
- StringwrapperPath=mappingData.wrapperPath.toString();
- StringpathInfo=mappingData.pathInfo.toString();
- mappingData.recycle();
- //ConstructaRequestDispatchertoprocessthisrequest
- returnnewApplicationDispatcher
- (<spanstyle="color:#ff0000;">wrapper</span>,uriCC.toString(),wrapperPath,pathInfo,
- queryString,null);
- }
紅色部分標記了從context的mapper物件中定位wrapper子容器,然後封裝在一個dispatcher物件內並返回。通過上面的闡述,也說明了為什麼forward方法不能跨context訪問資源了。
Wrapper
Wrapper是context的子容器,它封裝的處理資源的每個具體的servlet。其標準實現類為:- org.apache.catalina.core.StandardWrapper
應該說StandardWrapper是tomcat中比較重要的一個類。初認識它時,比較容易混淆。
先看一下StandardWrapper的主要邏輯單元概念圖。
Pipeline,valve與上面容器一樣,只是作用域不一樣,不多說了。
主要說說servlet物件與servlet stack物件。這兩個物件在wrapper容器中只存在其中之一,也就是說只有其中一個不為空。當以servlet物件存在時,說明此servlet是支援多執行緒併發訪問的,也就是說不存線上程同步的過程,此wrapper容器中只包含一個servlet物件(這是我們常用的模式);當以servlet stack物件存在時,說明servlet是不支援多執行緒併發訪問的,每個servlet物件任一時刻只有一個執行緒可以呼叫,這樣servlet stack實現的就是個簡易的執行緒池,此wrapper容器中只包含一組servlet物件,它的基本原型是worker thread模式實現的。
那麼,怎麼來決定是以servlet物件方式儲存還是servlet stack方式儲存呢?其實,只要在開發servlet類時,實現一個SingleThreadModel介面即可。
如果需要執行緒同步的servlet類,例如:- publicclassLoginServletextendsHttpServletimplementsjavax.servlet.SingleThreadModel{……}
但是值得注意的是,這種同步機制只是從servlet規範的角度來說提供的一種功能,在實際應用中並不能完全解決執行緒安全問題,例如如果servlet中有static資料訪問等,因此如果對執行緒安全又比較嚴格要求的,最好還是用一些其他的自定義的解決方案。
Wrapper的基本功能已經說了。那麼再說一個wrapper比較重要的概念。嚴格的說,並不是每一個訪問資源對應一個wrapper物件。而是每一種訪問資源對應一個wrapper物件。其大致可分為三種:- 處理靜態資源的一個wrapper:例如html,jpg等靜態資源的wrapper,它包含了一個tomcat的實現處理靜態資源的預設servlet:
- org.apache.catalina.servlets.DefaultServlet
- 處理jsp的一個wrapper:例如訪問的所有jsp檔案,它包含了一個tomcat的實現處理jsp的預設servlet:
- org.apache.jasper.servlet.JspServlet
它主要實現了對jsp的編譯等操作- 處理servlet的若干wrapper:它包含了自定義的servlet物件,就是在web.xml中配置的servlet。
需要注意的是,前兩種wrapper分別是一個,主要是其對應的是DefaultServlet及JspServlet。這兩個servlet是在tomcat的全域性conf目錄下的web.xml中配置的,當app啟動時,載入到記憶體中。- <servlet>
- <servlet-name>default</servlet-name>
- <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
- <init-param>
- <param-name>debug</param-name>
- <param-value>0</param-value>
- </init-param>
- <init-param>
- <param-name>listings</param-name>
- <param-value>false</param-value>
- </init-param>
- <load-on-startup>1</load-on-startup>
- </servlet>
- <servlet>
- <servlet-name>jsp</servlet-name>
- <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
- <init-param>
- <param-name>fork</param-name>
- <param-value>false</param-value>
- </init-param>
- <init-param>
- <param-name>xpoweredBy</param-name>
- <param-value>false</param-value>
- </init-param>
- <load-on-startup>3</load-on-startup>
- </servlet>
至此,闡述了tomcat的四大容器結構。 有時間接著探討tomcat如何將這四大容器串起來運作的。關於tomcat的內部邏輯單元的儲存空間已經在相關容器類的blog裡闡述了。在每個容器物件裡面都有一個pipeline及valve模組。它們是容器類必須具有的模組。在容器物件生成時自動產生。Pipeline就像是每個容器的邏輯匯流排。在pipeline上按照配置的順序,載入各個valve。通過pipeline完成各個valve之間的呼叫,各個valve實現具體的應用邏輯。
先看一下pipeline及valve的邏輯概念圖。
這些valve就是在tomcat的server.xml中配置,只要滿足一定條件,繼承ValveBase基類
就可以在不同的容器中配置,然後在訊息流中被逐一呼叫。每個容器的valve的作用域不一樣,在總體結構中已有說明。這裡紅色標記的是配置的自定義的valve,這樣可以擴充套件成多個其他應用,例如cluster應用等。
Tomcat實現
Tomcat提供了Pipeline的標準實現:
引用 org.apache.catalina.core.StandardPipeline
四大容器類StandardEngine,StandardHost,StandardContext及StandardWrapper都有各自預設的標準valve實現。它們分別是- Engine:org.apache.catalina.core.StandardEngineValve
- Host: org.apache.catalina.core.StandardHostValve
- Context:org.apache.catalina.core.StandardContextValve
- Wrapper:org.apache.catalina.core.StandardWrapperValve
容器類生成物件時,都會生成一個pipeline物件,同時,生成一個預設的valve實現,並將這個標準的valve物件繫結在其pipeline物件上。以StandardHost類為例:
Java程式碼- publicclassStandardHostextendsContainerBaseimplementsHost{
- protectedPipelinepipeline=newStandardPipeline(this);
- publicStandardHost(){
- super();
- pipeline.setBasic(newStandardHostValve());
- }
- }
Valve實現了具體業務邏輯單元。可以定製化valve(實現特定介面),然後配置在server.xml裡。每層容器都可以配置相應的valve,當只在其作用域內有效。例如engine容器裡的valve只對其包含的所有host裡的應用有效。定製化的valve是可選的,但是每個容器有一個預設的valve,例如engine的StandardEngineValve,是在StandardEngine裡自帶的,它主要實現了對其子host物件的StandardHostValve的呼叫,以此類推。
配置的例子有:
Xml程式碼- <Enginename="Catalina"defaultHost="localhost">
- <ValveclassName="MyValve0"/>
- <ValveclassName="MyValve1"/>
- <ValveclassName="MyValve2"/>
- ……
- <Hostname="localhost"appBase="webapps">
- </Host>
- </Engine>
當在server.xml檔案中配置了一個定製化valve時,會呼叫pipeline物件的addValve方法,將valve以連結串列方式組織起來,看一下程式碼;
Java程式碼- publicclassStandardPipelineimplementsPipeline,Contained,Lifecycle{
- protectedValvefirst=null;
- publicvoidaddValve(Valvevalve){
- //ValidatethatwecanaddthisValve
- if(valveinstanceofContained)
- ((Contained)valve).setContainer(this.container);
- //Startthenewcomponentifnecessary
- if(started){
- if(valveinstanceofLifecycle){
- try{
- ((Lifecycle)valve).start();
- }catch(LifecycleExceptione){
- log.error("StandardPipeline.addValve:start:",e);
- }
- }
- //Registerthenewlyaddedvalve
- registerValve(valve);
- }
- //將配置的valve新增到連結串列中,並且每個容器的標準valve在連結串列的尾端
- if(first==null){
- first=valve;
- valve.setNext(basic);
- }else{
- Valvecurrent=first;
- while(current!=null){
- if(current.getNext()==basic){
- current.setNext(valve);
- valve.setNext(basic);
- break;
- }
- current=current.getNext();
- }
- }
- }
- }
從上面可以清楚的看出,valve按照容器作用域的配置順序來組織valve,每個valve都設定了指向下一個valve的next引用。同時,每個容器預設的標準valve都存在於valve連結串列尾端,這就意味著,在每個pipeline中,預設的標準valve都是按順序,最後被呼叫。
訊息流
先看一下四大容器的標準valve的呼叫邏輯圖。從中可以梳理出標準valve的邏輯。注意此圖只是在預設配置下的狀態,也就是說每個pipeline只包含一個標準valve的情況。
圖中顯示的是各個容器預設的valve之間的實際呼叫情況。從StandardEngineValve開始,一直到StandardWrapperValve,完成整個訊息處理過程。注意每一個上層的valve都是在呼叫下一層的valve返回後再返回的,這樣每個上層valve不僅具有request物件,同時還能拿到response物件,想象一下,這樣是不是可以批量的做很多東西?現在通過原始碼來加深下理解。侯捷說過,原始碼面前,了無祕密。通過這些程式碼,可以看到在tomcat中我們經常碰到的一些現象或配置是怎麼實現的。
StandardEngineValve
看一下StandardEngineValve的呼叫邏輯;
Java程式碼- publicfinalvoidinvoke(Requestrequest,Responseresponse)
- throwsIOException,ServletException{
- //定位host
- Hosthost=request.getHost();
- if(host==null){
- ......
- return;
- }
- //呼叫host的第一個valve
- host.getPipeline().getFirst().invoke(request,response);
- }
可以清晰的看到,根據request定位到可以處理的host物件,同時,開始從頭呼叫host裡的pipeline上的valve。
StandardHostValve
看一下StandardHostValve的呼叫邏輯;
Java程式碼- publicfinalvoidinvoke(Requestrequest,Responseresponse)
- throwsIOException,ServletException{
- //定位context
- Contextcontext=request.getContext();
- if(context==null){
- ......
- return;
- }
- ......
- //呼叫context的第一個valve
- context.getPipeline().getFirst().invoke(request,response);
- //更新session
- if(Globals.STRICT_SERVLET_COMPLIANCE){
- request.getSession(false);
- }
- //Errorpageprocessing
- response.setSuspended(false);
- //如果有拋異常或某個HTTP錯誤,導向響應的配置頁面
- Throwablet=(Throwable)request.getAttribute(Globals.EXCEPTION_ATTR);
- if(t!=null){
- throwable(request,response,t);
- }else{
- status(request,response);
- }
- //Restorethecontextclassloader
- Thread.currentThread().setContextClassLoader
- (StandardHostValve.class.getClassLoader());
- }
可以清晰的看到,註釋部分里根據request定位到可以處理的context物件,同時,開始從頭呼叫context裡的pipeline上的valve。在呼叫完context的所有的valve之後(當然也是context呼叫完其對應的wrapper上的所有valve之後),藍色部分顯示了拿到response物件時可以做的處理。
熟悉tomcat的可能有配置錯誤資訊的經驗,例如;
Xml程式碼- <error-page>
- <error-code>404</error-code>
- <location>/error.jsp</location>
- </error-page>
它就是為了在使用者訪問資源出現HTTP 404錯誤時,將訪問重定向到一個統一的錯誤頁面。這樣做一是為了美觀,另一個主要作用是不會將一些具體的錯誤資訊例如java拋異常時的棧資訊暴露給使用者,主要還是出於安全的考慮。 上述程式碼中的註釋部分就是實現這個重定向功能。
StandardContextValve
看一下StandardContextValve的呼叫邏輯;其程式碼比較多,只貼一些比較核心的吧。
Java程式碼- publicfinalvoidinvoke(Requestrequest,Responseresponse)
- throwsIOException,ServletException{
- ......
- //定位wrapper
- Wrapperwrapper=request.getWrapper();
- if(wrapper==null){
- notFound(response);
- return;
- }elseif(wrapper.isUnavailable()){
- ......
- }
- //Normalrequestprocessing
- //web.xml中配置web-app/listener/listener-class
- Objectinstances[]=context.getApplicationEventListeners();
- ServletRequestEventevent=null;
- //響應request初始化事件,具體的響應listener是可配置的
- ......
- //呼叫wrapper的第一個valve
- wrapper.getPipeline().getFirst().invoke(request,response);
- //響應request撤銷事件,具體的響應listener是可配置的
- ......
- }
可以清晰的看到,註釋部分里根據request定位到可以處理的wrapper物件,同時,開始從頭呼叫wrapper裡的pipeline上的valve。 需要注意的是,這裡在呼叫wrapper的valve前後,分別有響應request初始化及撤銷事件的邏輯,tomcat有一整套事件觸發體系,這裡限於篇幅就不闡述了。有時間專門說。
StandardWrapperValve
看一下StandardWrapperValve的呼叫邏輯;其程式碼比較多,只貼一些比較核心的吧;
Java程式碼- publicfinalvoidinvoke(Requestrequest,Responseresponse)
- throwsIOException,ServletException{
- ......
- requestCount++;
- //定位wrapper
- StandardWrapperwrapper=(StandardWrapper)getContainer();
- Servletservlet=null;
- Contextcontext=(Context)wrapper.getParent();
- ......
- //Allocateaservletinstancetoprocessthisrequest
- try{
- if(!unavailable){
- //載入servlet
- servlet=wrapper.allocate();
- }
- }catch(UnavailableExceptione){
- ......
- }
- ......
- //根據配置建立一個filter-servlet的處理連結串列,servlet在連結串列的尾端
- ApplicationFilterFactoryfactory=
- ApplicationFilterFactory.getInstance();
- ApplicationFilterChainfilterChain=
- factory.createFilterChain(request,wrapper,servlet);
- //Resetcometflagvalueaftercreatingthefilterchain
- request.setComet(false);
- //Callthefilterchainforthisrequest
- //NOTE:Thisalsocallstheservlet'sservice()method
- try{
- StringjspFile=wrapper.getJspFile();
- if(jspFile!=null)
- request.setAttribute(Globals.JSP_FILE_ATTR,jspFile);
- else
- request.removeAttribute(Globals.JSP_FILE_ATTR);
- if((servlet!=null)&&(filterChain!=null)){
- //Swallowoutputifneeded
- if(context.getSwallowOutput()){
- try{
- SystemLogHandler.startCapture();
- if(comet){
- filterChain.doFilterEvent(request.getEvent());
- request.setComet(true);
- }else{
- //呼叫filter-servlet連結串列
- filterChain.doFilter(request.getRequest(),
- response.getResponse());
- }
- }finally{
- Stringlog=SystemLogHandler.stopCapture();
- if(log!=null&&log.length()>0){
- context.getLogger().info(log);
- }
- }
- }else{
- if(comet){
- request.setComet(true);
- filterChain.doFilterEvent(request.getEvent());
- }else{
- //呼叫filter-servlet連結串列
- filterChain.doFilter
- (request.getRequest(),response.getResponse());
- }
- }
- }
- request.removeAttribute(Globals.JSP_FILE_ATTR);
- }catch(ClientAbortExceptione){
- request.removeAttribute(Globals.JSP_FILE_ATTR);
- throwable=e;
- exception(request,response,e);
- }
- ......
- }
可以清晰的看到,註釋部分裡,先是能拿到相應的wrapper物件;然後完成載入wrapper物件中的servlet,例如如果是jsp,將完成jsp編譯,然後載入servlet等;再然後,根據配置生成一個filter棧,通過執行棧,呼叫完所有的filter之後,就呼叫servlet,如果沒有配置filter,就直接呼叫servlet,生成filter棧是通過request的URL模式匹配及servlet名稱來實現的,具體涉及的東西在tomcat的servlet規範實現中再闡述吧。
以上,完成了一整套servlet呼叫的過程。通過上面的闡述,可以看見valve是個很靈活的機制,通過它可以實現很大的擴充套件。
Valve的應用及定製化
Tomcat除了提供上面提到的幾個標準的valve實現外,也提供了一些用於除錯程式的valve的實現。實現valve需要繼承org.apache.catalina.valves.ValveBase基類。 以RequestDumperValve為例,
引用 org.apache.catalina.valves.RequestDumperValve
RequestDumperValve是打印出request及response資訊的valve。其實現方法為:
Java程式碼- publicvoidinvoke(Requestrequest,Responseresponse)
- throwsIOException,ServletException{
- Loglog=container.getLogger();
- //Logpre-serviceinformation
- log.info("REQUESTURI="+request.getRequestURI());
- ......
- log.info("queryString="+request.getQueryString());
- ......
- log.info("-------------------------------------------------------");
- //呼叫下一個valve
- getNext().invoke(request,response);
- //Logpost-serviceinformation
- log.info("-------------------------------------------------------");
- ......
- log.info("contentType="+response.getContentType());
- Cookiercookies[]=response.getCookies();
- for(inti=0;i<rcookies.length;i++){
- log.info("cookie="+rcookies[i].getName()+"="+
- rcookies[i].getValue()+";domain="+
- rcookies[i].getDomain()+";path="+rcookies[i].getPath());
- }
- Stringrhnames[]=response.getHeaderNames();
- for(inti=0;i<rhnames.length;i++){
- Stringrhvalues[]=response.getHeaderValues(rhnames[i]);
- for(intj=0;j<rhvalues.length;j++)
- log.info("header="+rhnames[i]+"="+rhvalues[j]);
- }
- log.info("message="+response.getMessage());
- log.info("========================================================");
- }
可以很清晰的看出,它打印出了request及response的資訊,其中紅色部分顯示它呼叫valve連結串列中的下一個valve。我們可以這樣配置它;
Xml程式碼- <Hostname="localhost"appBase="webapps"
- unpackWARs="true"autoDeploy="true"
- xmlValidation="false"xmlNamespaceAware="false">
- <ValveclassName="org.apache.catalina.valves.RequestDumperValve"/>
- <Contextpath="/my"docBase="/usr/local/tomcat/backup/my">
- </Context>
- <Contextpath="/my2"docBase="/usr/local/tomcat/backup/my">
- </Context>
- </Host>
這樣,只要訪問此host下的所有context,都會打印出除錯資訊。 Valve的應用有很多,例如cluster,SSO等,會有專門一章來講講。Session管理是JavaEE容器比較重要的一部分,在app中也經常會用到。在開發app時,我們只是獲取一個session,然後向session中存取資料,然後再銷燬session。那麼如何產生session,以及session池如何維護及管理,這些並沒有在app涉及到。這些工作都是由容器來完成的。
Tomcat中主要由每個context容器內的一個Manager物件來管理session。對於這個manager物件的實現,可以根據tomcat提供的介面或基類來自己定製,同時,tomcat也提供了標準實現。
在tomcat架構分析(容器類)中已經介紹過,在每個context物件,即web app都具有一個獨立的manager物件。通過server.xml可以配置定製化的manager,也可以不配置。不管怎樣,在生成context物件時,都會生成一個manager物件。預設的是StandardManager類,其類路徑為:
引用 org.apache.catalina.session.StandardManager
Session物件也可以定製化實現,其主要實現標準servlet的session介面:
引用 javax.servlet.http.HttpSession
Tomcat也提供了標準的session實現:
引用 org.apache.catalina.session.StandardSession
本文主要就是結合訊息流程介紹這兩個類的實現,及session機制。
Session方面牽涉的東西還是蠻多的,例如HA,session複製是其中重要部分等,不過本篇主要從功能方面介紹session管理,有時間再說說擴充套件。
Session管理主要涉及到這幾個方面:- 建立session
- 登出session
- 持久化及啟動載入session
建立session
在具體說明session的建立過程之前,先看一下BS訪問模型吧,這樣理解直觀一點。- browser傳送Http request;
- tomcat核心Http11Processor會從HTTP request中解析出“jsessionid”(具體的解析過程為先從request的URL中解析,這是為了有的瀏覽器把cookie功能禁止後,將URL重寫考慮的,如果解析不出來,再從cookie中解析相應的jsessionid),解析完後封裝成一個request物件(當然還有其他的http header);
- servlet中獲取session,其過程是根據剛才解析得到的jsessionid(如果有的話),從session池(session maps)中獲取相應的session物件;這個地方有個邏輯,就是如果jsessionid為空的話(或者沒有其對應的session物件,或者有session物件,但此物件已經過期超時),可以選擇建立一個session,或者不建立;
- 如果建立新session,則將session放入session池中,同時將與其相對應的jsessionid寫入cookie通過Http response header的方式傳送給browser,然後重複第一步。
以上是session的獲取及建立過程。在servlet中獲取session,通常是呼叫request的getSession方法。這個方法需要傳入一個boolean引數,這個引數就是實現剛才說的,當jsessionid為空或從session池中獲取不到相應的session物件時,選擇建立一個新的session還是不建立。
看一下核心程式碼邏輯;
Java程式碼- protectedSessiondoGetSession(booleancreate){
- ……
- //先獲取所在context的manager物件
- Managermanager=null;
- if(context!=null)
- manager=context.getManager();
- if(manager==null)
- return(null);//Sessionsarenotsupported
- //這個requestedSessionId就是從Httprequest中解析出來的
- if(requestedSessionId!=null){
- try{
- //manager管理的session池中找相應的session物件
- session=manager.findSession(requestedSessionId);
- }catch(IOExceptione){
- session=null;
- }
- //判斷session是否為空及是否過期超時
- if((session!=null)&&!session.isValid())
- session=null;
- if(session!=null){
- //session物件有效,記錄此次訪問時間
- session.access();
- return(session);
- }
- }
- //如果引數是false,則不建立新session物件了,直接退出了
- if(!create)
- return(null);
- if((context!=null)&&(response!=null)&&
- context.getCookies()&&
- response.getResponse().isCommitted()){
- thrownewIllegalStateException
- (sm.getString("coyoteRequest.sessionCreateCommitted"));
- }
- //開始建立新session物件
- if(connector.getEmptySessionPath()
- &&isRequestedSessionIdFromCookie()){
- session=manager.createSession(getRequestedSessionId());
- }else{
- session=manager.createSession(null);
- }
- //將新session的jsessionid寫入cookie,傳給browser
- if((session!=null)&&(getContext()!=null)
- &&getContext().getCookies()){
- Cookiecookie=newCookie(Globals.SESSION_COOKIE_NAME,
- session.getIdInternal());
- configureSessionCookie(cookie);
- response.addCookieInternal(cookie);
- }
- //記錄session最新訪問時間
- if(session!=null){
- session.access();
- return(session);
- }else{
- return(null);
- }
- }
儘管不能貼出所有程式碼,但是上述的核心邏輯還是很清晰的。從中也可以看出,我們經常在servlet中這兩種呼叫方式的不同;
新建立session
引用 request.getSession(); 或者request.getSession(true);
不建立session
引用 request.getSession(false);
接下來,看一下StandardManager的createSession方法,瞭解一下session的建立過程;
Java程式碼- publicSessioncreateSession(StringsessionId){
- 是個session數量控制邏輯,超過上限則拋異常退出
- if((maxActiveSessions>=0)&&
- (sessions.size()>=maxActiveSessions)){
- rejectedSessions++;
- thrownewIllegalStateException
- (sm.getString("standardManager.createSession.ise"));
- }
- return(super.createSession(sessionId));
- }
這個最大支援session數量maxActiveSessions是可以配置的,先不管這個安全控制邏輯,看其主邏輯,即呼叫其基類的createSession方法;
Java程式碼- publicSessioncreateSession(StringsessionId){
- //建立一個新的StandardSession物件
- Sessionsession=createEmptySession();
- //Initializethepropertiesofthenewsessionandreturnit
- session.setNew(true);
- session.setValid(true);
- session.setCreationTime(System.currentTimeMillis());
- session.setMaxInactiveInterval(this.maxInactiveInterval);
- if(sessionId==null){
- //設定jsessionid
- sessionId=generateSessionId();
- }
- session.setId(sessionId);
- sessionCounter++;
- return(session);
- }
關鍵是jsessionid的產生過程,接著看generateSessionId方法;
Java程式碼- protectedsynchronizedStringgenerateSessionId(){
- byterandom[]=newbyte[16];
- StringjvmRoute=getJvmRoute();
- Stringresult=null;
- //RendertheresultasaStringofhexadecimaldigits
- StringBufferbuffer=newStringBuffer();
- do{
- intresultLenBytes=0;
- if(result!=null){
- buffer=newStringBuffer();
- duplicates++;
- }
- while(resultLenBytes<this.sessionIdLength){
- getRandomBytes(random);
- random=getDigest().digest(random);
- for(intj=0;
- j<random.length&&resultLenBytes<this.sessionIdLength;
- j++){
- byteb1=(byte)((random[j]&0xf0)>>4);
- byteb2=(byte)(random[j]&0x0f);
- if(b1<10)
- buffer.append((char)('0'+b1));
- else
- buffer.append((char)('A'+(b1-10)));
- if(b2<10)
- buffer.append((char)('0'+b2));
- else
- buffer.append((char)('A'+(b2-10)));
- resultLenBytes++;
- }
- }
- if(jvmRoute!=null){
- buffer.append('.').append(jvmRoute);
- }
- result=buffer.toString();
- //注意這個do…while結構
- }while(sessions.containsKey(result));
- return(result);
- }
這裡主要說明的不是生成jsessionid的演算法了,而是這個do…while結構。把這個邏輯抽象出來,可以看出;
如圖所示,建立jsessionid的方式是由tomcat內建的加密演算法算出一個隨機的jsessionid,如果此jsessionid已經存在,則重新計算一個新的,直到確保現在計算的jsessionid唯一。
好了,至此一個session就這麼建立了,像上面所說的,返回時是將jsessionid以HTTP response的header:“Set-cookie”發給客戶端。
登出session- 主動登出
- 超時登出
Session建立完之後,不會一直存在,或是主動登出,或是超時清除。即是出於安全考慮也是為了節省記憶體空間等。例如,常見場景:使用者登出系統時,會主動觸發登出操作。
主動登出
主動登出時,是呼叫標準的servlet介面:
引用 session.invalidate();
看一下tomcat提供的標準session實現(StandardSession)
Java程式碼- publicvoidinvalidate(){
- if(!isValidInternal())
- thrownewIllegalStateException
- (sm.getString("standardSession.invalidate.ise"));
- //明顯的登出方法
- expire();
- }
Expire方法的邏輯稍後再說,先看看超時登出,因為它們呼叫的是同一個expire方法。
超時登出
Tomcat定義了一個最大空閒超時時間,也就是說當session沒有被操作超過這個最大空閒時間時間時,再次操作這個session,這個session就會觸發expire。
這個方法封裝在StandardSession中的isValid()方法內,這個方法在獲取這個request請求對應的session物件時呼叫,可以參看上面說的建立session環節。也就是說,獲取session的邏輯是,先從manager控制的session池中獲取對應jsessionid的session物件,如果獲取到,就再判斷是否超時,如果超時,就expire這個session了。
看一下tomcat提供的標準session實現(StandardSession)
Java程式碼- publicbooleanisValid(){
- ……
- //這就是判斷距離上次訪問是否超時的過程
- if(maxInactiveInterval>=0){
- longtimeNow=System.currentTimeMillis();
- inttimeIdle=(int)((timeNow-thisAccessedTime)/1000L);
- if(timeIdle>=maxInactiveInterval){
- expire(true);
- }
- }
- return(this.isValid);
- }
Expire方法
是時候來看看expire方法了。
Java程式碼- publicvoidexpire(booleannotify){
- synchronized(this){
- ......
- //設立標誌位
- setValid(false);
- //計算一些統計值,例如此manager下所有session平均存活時間等
- longtimeNow=System.currentTimeMillis();
- inttimeAlive=(int)((timeNow-creationTime)/1000);
- synchronized(manager){
- if(timeAlive>manager.getSessionMaxAliveTime()){
- manager.setSessionMaxAliveTime(timeAlive);
- }
- intnumExpired=manager.getExpiredSessions();
- numExpired++;
- manager.setExpiredSessions(numExpired);
- intaverage=manager.getSessionAverageAliveTime();
- average=((average*(numExpired-1))+timeAlive)/numExpired;
- manager.setSessionAverageAliveTime(average);
- }
- //將此session從manager物件的session池中刪除
- manager.remove(this);
- ......
- }
- }
不需要解釋,已經很清晰了。
這個超時時間是可以配置的,預設在tomcat的全域性web.xml下配置,也可在各個app下的web.xml自行定義;
Xml程式碼- <session-config>
- <session-timeout>30</session-timeout>
- </session-config>
單位是分鐘。
Session持久化及啟動初始化
這個功能主要是,當tomcat執行安全退出時(通過執行shutdown指令碼),會將session持久化到本地檔案,通常在tomcat的部署目錄下有個session.ser檔案。當啟動tomcat時,會從這個檔案讀入session,並新增到manager的session池中去。
這樣,當tomcat正常重啟時, session沒有丟失,對於使用者而言,體會不到重啟,不影響使用者體驗。
看一下概念圖吧,覺得不是重要實現邏輯,程式碼就不說了。
總結
由此可以看出,session的管理是容器層做的事情,應用層一般不會參與session的管理,也就是說,如果在應用層獲取到相應的session,已經是由tomcat提供的,因此如果過多的依賴session機制來進行一些操作,例如訪問控制,安全登入等就不是十分的安全,因為如果有人能得到正在使用的jsessionid,則就可以侵入系統。JNDI(Java Naming and Directory Interface,Java命名和目錄介面)是一組在Java應用中訪問命名和目錄服務的API。命名服務將名稱和物件聯絡起來,使得我們可以用名稱訪問物件。目錄服務是一種命名服務,在這種服務裡,物件不但有名稱,還有屬性。
---百度百科
通俗點說,JNDI封裝了一個簡單name到實體物件的mapping,通過字串可以方便的得到想要的物件資源。通常這種物件資源有很多種,例如資料庫JDBC,JMS,EJB等。平時用的最多的就是資料庫了。在tomcat中,這些資源都是以java:comp/env開頭的字串來繫結的。以資料庫連線為例,我們在app中的呼叫場景是;
Java程式碼- //獲得對資料來源的引用:
- Contextctx=newInitalContext();
- DataSourceds=(DataSource)ctx.lookup("java:comp/env/jdbc/myDB");
- //獲得資料庫連線物件:
- Connectionconn=ds.getConnection();
- //返回資料庫連線到連線池:
- conn.close();
因為經常看到有人問怎麼在tomcat中配置資料庫連線池等問題,這篇文章就對tomcat中的JNDI的配置做一個小結,不涉及tomcat程式碼方面。tomcat架構分析 (JNDI體系繫結)從程式碼原理角度專門說明這些配置是如何生效,及app中呼叫JNDI API獲取物件,其底層如何實現的。
Tomcat內部有一堆型別的resource配置。這些型別的resource的配置大體上可分為兩個層次來進行,這兩個層次是並列的關係,分別針對不同的開發部署方案設定的。
第一種方案
這種方案主要是對於快速部署而言,其核心是tomcat本身有一個global的resource池,新部署的app只引用其中已有的resouce,而不是建立新的resource。
先看看<tomcat>/conf/server.xml
Xml程式碼- <Serverport="8005">
- <GlobalNamingResources>
- <Resource
- name="jdbc/mysql"
- type="javax.sql.DataSource"
- username="root"
- password="root"
- driverClassName="com.mysql.jdbc.Driver"
- maxIdle="200"
- maxWait="5000"
- url="……"
- maxActive="100"/>
- </GlobalNamingResources>
- ……
- </Server>
這是一個全域性的配置,這時如果每個具體的context(webapp)中如果要引用這個resource,則需要在各個context物件中配置 resourcelink,然後在各個app的web.xml中配置<resource-ref>.
<tomcat>/conf/server.xml
Xml程式碼- <Serverport="8005">
- <Service>
- <Engine>
- <Host>
- <Context>
- <ResourceLinkglobalname="jdbc/mysql"name="myDB"type="…"/>
- </Context>
- </Host>
- </Engine>
- </Service>
- ……
- </Server>
或者在每個app的Context.xml中配置
Xml程式碼- <Context>
- <ResourceLinkglobalname="jdbc/mysql"name="myDB"type="…"/>
- </Context>
然後在app的WEB-INF/web.xml中配置
Xml程式碼- <web-app>
- <resource-ref>
- <description/>
- <res-auth/>
- <res-ref-name>myDB</res-ref-name>
- <res-sharing-scope/>
- <res-type/>
- </resource-ref>
- </web-app>
程式碼中這麼呼叫
Java程式碼- //獲得對資料來源的引用:
- Contextctx=newInitalContext();
- DataSourceds=(DataSource)ctx.lookup("java:comp/env/myDB");
- //獲得資料庫連線物件:
- Connectionconn=ds.getConnection();
- //返回資料庫連線到連線池:
- conn.close();
由此可見,context中配置的ResourceLink屬於一箇中轉的作用,這主要是為了在tomcat啟動狀態下,如果新部署一個app,可以在app中指定到相應的全域性的resource。
它們的mapping關係是;
Tomcat這種資源不限於資料庫連線,還有很多例如EJB,Web Service等,在配置中它們分別對應不同的節點。例如上面的資料庫連線,在server.xml中對應<Resource>,在web.xml中對應的是<resource-ref>,EJB連線在server.xml中對應<Ejb>,在web.xml中對應的是<ejb-ref>等,因為有些資源在現在的開發中應用的不是很多,就不一一例舉了,總結一下它們所有的對應關係;
第二種方案
沒有上述方案那麼麻煩,主要是為了需要引用一個自己獨有的資源物件的app而言。
<tomcat>/conf/server.xml
Xml程式碼- <Serverport="8005">
- <Service>
- <Engine>
- <Host>
- <Context>
- <Resource
- name="jdbc/mysql"
- type="javax.sql.DataSource"
- username="root"
- password="root"
- driverClassName="com.mysql.jdbc.Driver"
- maxIdle="200"
- maxWait="5000"
- url="……"
- maxActive="100"/>
- </Context>
- </Host>
- </Engine>
- </Service>
- ……
- </Server>
或者在每個app的Context.xml中配置
Xml程式碼- <Context>
- <Resource
- name="jdbc/mysql"
- type="javax.sql.DataSource"
- username="root"
- password="root"
- driverClassName="com.mysql.jdbc.Driver"
- maxIdle="200"
- maxWait="5000"
- url="……"
- maxActive="100"/>
- </Context>
這種方式,不需要在app的WEB-INF/web.xml中再設定resource-ref了,直接在程式中就可lookup到相應的物件。
程式碼中這麼呼叫
Java程式碼- //獲得對資料來源的引用:
- Contextctx=newInitalContext();
- DataSourceds=(DataSource)ctx.lookup("java:comp/env/jdbc/mysql");
- //獲得資料庫連線物件:
- Connectionconn=ds.getConnection();
- //返回資料庫連線到連線池:
- conn.close();
比較一下,兩種方式的配置,呼叫java:comp/env的name時還是不一樣的。以配置JDBC資料庫連線為例,介紹了tomcat中常用的JNDI配置的幾種用法。使用這種配置,在app裡可以通過JNDI API非常簡單的呼叫相應的資源物件。但是呼叫越簡單,那其背後封裝的邏輯越多。就好比汽車分為手動檔自動擋一樣。對司機而言,自動擋開起來會輕鬆很多,那是因為很多複雜的操作,已經封裝起來由機器來完成了。
本篇就是從程式碼原理角度來揭示tomcat中JNDI的配置是如何生效的,以及app中的呼叫邏輯是如何實現的。通過這些,可以看到tomcat中一塊比較重要的體系結構,同時加深對JNDI的理解。
上文介紹了兩種配置方案,一個是global的配置,在各個app中引用;一個是各個app自己配置資源物件。這兩種方案,從實現角度來看,原理一樣,只是第一種比第二種多了一層mapping關係。所以為了方便理解,先從第二種方案,即各個app配置自己的資源物件來說明。
另外,需要說明的是,本章涉及的程式碼- Tomcat原始碼
- JNDI原始碼(javax.naming.*),參考OpenJDK專案
先看一個概念圖
JNDI體系分為三個部分;- 在tomcat架構分析 (容器類)中介紹了StandardContext類,它是每個app的一個邏輯封裝。當tomcat初始化時,將根據配置檔案,對StandardContext中的NamingResources物件進行賦值,同時,將例項化一個NamingContextListener物件作為這個context作用域內的事件監聽器,它會響應一些例如系統啟動,系統關閉等事件,作出相應的操作;
- 初始化完成後,tomcat啟動,完成啟動邏輯,丟擲一個系統啟動event,由那個NamingContextListener捕獲,進行處理,將初始化時的NamingResources物件中的資料,繫結到相應的JNDI物件樹(namingContext)上,即java:comp/env分支,然後將這個根namingContext與這個app的classloader進行繫結,這樣每個app只有在自己的JNDI物件樹上呼叫,互不影響;
- 每個app中的類都由自己app的classloader載入,如果需要用到JNDI繫結物件,也是從自己classloader對應的JNDI物件樹上獲取資源物件
這裡需要說明的是,在後面會經常涉及到兩類context,一個是作為tomcat內部實現邏輯的容器StandardContext;一個是作為JNDI內部分支物件NamingContext;它們實現不同介面,互相沒有任何關係,不要混淆。
開始看看每個部分詳細情況吧。
初始化NamingResources
先看看配置;
<tomcat>/conf/server.xml
Xml程式碼- <Serverport="8005">
- <Service>
- <Engine>
- <Host>
- <Context>
- <Resource
- name="jdbc/mysql"
- type="javax.sql.DataSource"
- username="root"
- password="root"
- driverClassName="com.mysql.jdbc.Driver"
- maxIdle="200"
- maxWait="5000"
- url="……"
- maxActive="100"/>
- </Context>
- </Host>
- </Engine>
- </Service>
- ……
- </Server>
通過這個配置,可以非常清楚的看出tomcat內部的層次結構,不同的層次實現不同的作用域,同時每個層次都有相應的類進行邏輯封裝,這是tomcat面向物件思想的體現。那麼相應的,Context節點下的Resource節點也有類進行封裝;
Java程式碼- org.apache.catalina.deploy.ContextResource
上面例子中Resource節點配置的所有屬性會以鍵值對的方式存入ContextResource的一個HashMap物件中,這一步只是初始化,不會用到每個屬性,它只是為了每個真正處理的資源物件用到,例如後面會說的預設的tomcat的資料庫連線池物件BasicDataSourceFactory,如果用其他的資料庫連線池,例如c3p0,那麼其配置的屬性物件就應該按照c3p0中需要的屬性名稱來配。
但是,這些屬性中的name和type是ContextResource需要的,name是JNDI物件樹的分支節點,上面配的“jdbc/mysql”,那麼這個資料庫連線池物件就對應在“java:comp/env/jdbc/mysql”的位置。type是這個物件的型別,如果是“javax.sql.DataSource”,tomcat會有一些特殊的邏輯處理。
當tomcat初始化時,StandardContext物件內部會生成一個NamingResources物件,這個物件就是做一些預處理,儲存一些Resource物件,看一下NamingResources儲存Resource物件的邏輯;
Java程式碼- publicvoidaddResource(ContextResourceresource){
- //確保每一個資源物件的name都是唯一的
- //不僅是Resource物件之間,包括Service等所有的資源物件
- if(entries.containsKey(resource.getName())){
- return;
- }else{
- entries.put(resource.getName(),resource.getType());
- }
- //建立一個name和資源物件的mapping
- synchronized(resources){
- resource.setNamingResources(this);
- resources.put(resource.getName(),resource);
- }
- support.firePropertyChange("resource",null,resource);
- }
需要說明的是,不僅僅是Resource一種物件,還有Web Service資源物件,EJB物件等,這裡就是拿資料庫連線的Resource物件舉例。
啟動JNDI繫結
當tomcat啟動時,會丟擲一個start event,由StandardContext的NamingContextListener監聽物件捕捉到,響應start event。
Java程式碼- publicvoidlifecycleEvent(LifecycleEventevent){
- container=event.getLifecycle();
- if(containerinstanceofContext){
- //這個namingResources物件就是StandardContext的namingResources物件
- namingResources=((Context)container).getNamingResources();
- logger=log;
- }elseif(containerinstanceofServer){
- namingResources=((Server)container).getGlobalNamingResources();
- }else{
- return;
- }
- //響應startevent
- if(event.getType()==Lifecycle.START_EVENT){
- if(initialized)
- return;
- HashtablecontextEnv=newHashtable();
- try{
- //生成這個StandardContext域的JNDI物件樹根NamingContext物件
- namingContext=newNamingContext(contextEnv,getName());
- }catch(NamingExceptione){
- //Neverhappens
- }
- ContextAccessController.setSecurityToken(getName(),container);
- //將此StandardContext物件與JNDI物件樹根NamingContext物件繫結
- ContextBindings.bindContext(container,namingContext,container);
- if(log.isDebugEnabled()){
- log.debug("Bound"+container);
- }
- //Settingthecontextinread/writemode
- ContextAccessController.setWritable(getName(),container);
- try{
- //將初始化時的資源物件繫結JNDI物件樹
- createNamingContext();
- }catch(NamingExceptione){
- logger.error
- (sm.getString("naming.namingContextCreationFailed",e));
- }
- //針對Context下配置Resource物件而言
- if(containerinstanceofContext){
- //Settingthecontextinreadonlymode
- ContextAccessController.setReadOnly(getName());
- try{
- //通過此StandardContext物件獲取到JNDI物件樹根NamingContext物件
- //同時將此app的classloader與此JNDI物件樹根NamingContext物件繫結
- ContextBindings.bindClassLoader
- (container,container,
- ((Container)container).getLoader().getClassLoader());
- }catch(NamingExceptione){
- logger.error(sm.getString("naming.bindFailed",e));
- }
- }
- //針對global資源而言,這裡不用關注
- if(containerinstanceofServer){
- namingResources.addPropertyChangeListener(this);
- org.apache.naming.factory.ResourceLinkFactory.setGlobalContext
- (namingContext);
- try{
- ContextBindings.bindClassLoader
- (container,container,
- this.getClass().getClassLoader());
- }catch(NamingExceptione){
- logger.error(sm.getString("naming.bindFailed",e));
- }
- if(containerinstanceofStandardServer){
- ((StandardServer)container).setGlobalNamingContext
- (namingContext);
- }
- }
- initialized=true;
- }
- //響應stopevent
- elseif(event.getType()==Lifecycle.STOP_EVENT){
- ......
- }
- }
注意上面方法中有兩層繫結關係;
ContextBindings.bindContext()
Java程式碼- publicstaticvoidbindContext(Objectname,Contextcontext,
- Objecttoken){
- if(ContextAccessController.checkSecurityToken(name,token))
- //先是將StandardContext物件與JNDI物件樹根NamingContext物件繫結
- //注意,這裡第一個引數name是StandardContext物件
- contextNameBindings.put(name,context);
- }
ContextBindings.bindClassLoader()
Java程式碼- publicstaticvoidbindClassLoader(Objectname,Objecttoken,
- ClassLoaderclassLoader)
- throwsNamingException{
- if(ContextAccessController.checkSecurityToken(name,token)){
- //根據上面的StandardContext物件獲取剛才繫結的NamingContext物件
- Contextcontext=(Context)contextNameBindings.get(name);
- if(context==null)
- thrownewNamingException
- (sm.getString("contextBindings.unknownContext",name));
- //將classloader與NamingContext物件繫結
- clBindings.put(classLoader,context);
- clNameBindings.put(classLoader,name);
- }
- }
主要看一下將初始化時的資源物件繫結JNDI物件樹的createNamingContext()方法;
Java程式碼- privatevoidcreateNamingContext()
- throwsNamingException{
- //Creatingthecompsubcontext
- if(containerinstanceofServer){
- compCtx=namingContext;
- envCtx=namingContext;
- }else{
- //對於StandardContext而言,在JNDI物件樹的根namingContext物件上
- //建立comp樹枝,以及在comp樹枝上建立env樹枝namingContext物件
- compCtx=namingContext.createSubcontext("comp");
- envCtx=compCtx.createSubcontext("env");
- }
- ......
- //從初始化的NamingResources物件中獲取Resource物件載入到JNDI物件樹上
- ContextResource[]resources=namingResources.findResources();
- for(i=0;i<resources.length;i++){
- addResource(resources[i]);
- }
- ......
- }
看一下addResource的具體載入邏輯;
Java程式碼- publicvoidaddResource(ContextResourceresource){
- //Createareferencetotheresource.
- Referenceref=newResourceRef
- (resource.getType(),resource.getDescription(),
- resource.getScope(),resource.getAuth());
- //遍歷Resource物件的各個屬性,這些屬性存在一個HashMap中
- Iteratorparams=resource.listProperties();
- while(params.hasNext()){
- StringparamName=(String)params.next();
- StringparamValue=(String)resource.getProperty(paramName);
- //封裝成StringRefAddr,這些都是JNDI的標準API
- StringRefAddrrefAddr=newStringRefAddr(paramName,paramValue);
- ref.add(refAddr);
- }
- try{
- if(logger.isDebugEnabled()){
- logger.debug("Addingresourceref"
- +resource.getName()+""+ref);
- }
- //在上面建立的comp/env樹枝節點上,根據Resource配置的name繼續建立新的節點
- //例如配置的name=”jdbc/mysql”,則在comp/env樹枝節點下再建立一個jdbc樹枝節點
- createSubcontexts(envCtx,resource.getName());
- //繫結葉子節點,它不是namingContext物件,而是最後的Resource物件
- envCtx.bind(resource.getName(),ref);
- }catch(NamingExceptione){
- logger.error(sm.getString("naming.bindFailed",e));
- }
- //這就是上面說的對於配置type="javax.sql.DataSource"時的特殊邏輯
- //將資料庫連線池型別的資源物件註冊到tomcat全域性的JMX中,方便管理及除錯
- if("javax.sql.DataSource".equals(ref.getClassName())){
- try{
- ObjectNameon=createObjectName(resource);
- ObjectactualResource=envCtx.lookup(resource.getName());
- Registry.getRegistry(null,null).registerComponent(actualResource,on,null);
- objectNames.put(resource.getName(),on);
- }catch(Exceptione){
- logger.warn(sm.getString("naming.jmxRegistrationFailed",e));
- }
- }
- }
這就是上面配置的jdbc/mysql資料庫連線池的JNDI物件樹;
到目前為止,完成了JNDI物件樹的繫結,可以看到,每個app對應的StandardContext對應一個JNDI物件樹,並且每個app的各個classloader與此JNDI物件樹分別繫結,那麼各個app之間的JNDI可以不互相干擾,各自配置及呼叫。
需要注意的是,NamingContext物件就是JNDI物件樹上的樹枝節點,類似檔案系統中的目錄,各個Resource物件則是JNDI物件樹上的葉子節點,類似檔案系統的具體檔案,通過NamingContext物件將整個JNDI物件樹組織起來,每個Resource物件才是真正儲存資料的地方。
本篇就描述tomcat內部是如何構造JNDI物件樹的,如何通過JNDI獲取物件,涉及到JNDI API內部運作了,將在另一篇中繼續。connector元件是service容器中的一部分。它主要是接收,解析http請求,然後呼叫本service下的相關servlet。由於tomcat從架構上採用的是一個分層結構,因此根據解析過的http請求,定位到相應的servlet也是一個相對比較複雜的過程。
整個connector實現了從接收socket到呼叫servlet的全部過程。先來看一下connector的功能邏輯;- 接收socket
- 從socket獲取資料包,並解析成HttpServletRequest物件
- 從engine容器開始走呼叫流程,經過各層valve,最後呼叫servlet完成業務邏輯
- 返回response,關閉socket
可以看出,整個connector元件是tomcat執行主幹,之前介紹的各個模組都是tomcat啟動時,靜態建立好的,通過connector將這些模組串了起來。
通常在實際執行中,特別是對於一些網際網路應用而言,網路吞吐一直是整個服務的瓶頸所在,因此,connector的執行效率在一定程度上影響了tomcat的整體效能。相對來說,tomcat在處理靜態頁面方面一直有一些瓶頸,因此通常的服務架構都是前端類似nginx的web伺服器,後端掛上tomcat作為應用伺服器(當然還有些其他原因,例如負載均衡等)。Tomcat在connector的優化上做了一些特殊的處理,這些都是可選的,通過部署,配置方便完成,例如APR(Apache Portable Runtime),BIO,NIO等。
目前connector支援的協議是HTTP和AJP。AJP是Apache與其他伺服器之間的通訊協議。通常在叢集環境中,例如前端web伺服器和後端應用伺服器或servlet容器,使用AJP會比HTTP有更好的效能,這裡引述apache官網上的一段話“ If integration with the native webserver is needed for any reason, an AJP connector will provide faster performance than proxied HTTP. AJP clustering is the most efficient from the Tomcat perspective. It is otherwise functionally equivalent to HTTP clustering.”
本篇主要是針對HTTP協議的connector進行闡述。先來看一下connector的配置,在server.xml裡;
Xml程式碼- <Connectorport="80"URIEncoding="UTF-8"protocol="HTTP/1.1"
- connectionTimeout="20000"
- redirectPort="7443"/>
熟悉的80埠不必說了。“protocol”這裡是指這個connector支援的協議。針對HTTP協議而言,這個屬性可以配置的值有:- HTTP/1.1
- org.apache.coyote.http11.Http11Protocol –BIO實現
- org.apache.coyote.http11.Http11NioProtocol –NIO實現
- 定製的介面
配置“HTTP/1.1”和“org.apache.coyote.http11.Http11Protocol”的效果是一樣的,因此connector的HTTP協議實現預設是支援BIO的。無論是BIO還是NIO都是實現一個org.apache.coyote.ProtocolHandler介面,因此如果需要定製化,也必須實現這個介面。
本篇就來看看預設狀態下HTTP connector的架構及其訊息流。
可以看見connector中三大塊- Http11Protocol
- Mapper
- CoyoteAdapter
Http11Protocol
類全路徑org.apache.coyote.http11.Http11Protocol,這是支援http的BIO實現。 Http11Protocol包含了JIoEndpoint物件及Http11ConnectionHandler物件。
Http11ConnectionHandler物件維護了一個Http11Processor物件池,Http11Processor物件會呼叫CoyoteAdapter完成http request的解析和分派。
JIoEndpoint維護了兩個執行緒池,Acceptor及Worker。Acceptor是接收socket,然後從Worker執行緒池中找出空閒的執行緒處理socket,如果worker執行緒池沒有空閒執行緒,則Acceptor將阻塞。Worker是典型的執行緒池實現。Worker執行緒拿到socket後,就從Http11Processor物件池中獲取Http11Processor物件,進一步處理。除了這個比較基礎的Worker執行緒池,也可以通過基於java concurrent 系列的java.util.concurrent.ThreadPoolExecutor執行緒池實現,不過需要在server.xml中配置相應的節點,即在connector同級別配置<Executor>,配置完後,使用ThreadPoolExecutor與Worker在實現上沒有什麼大的區別,就不贅述了。
Xml程式碼- <Executorname="tomcatThreadPool"namePrefix="catalina-exec-"
- maxThreads="150"minSpareThreads="4"/>
圖中的箭頭代表了訊息流。
Mapper
類全路徑org.apache.tomcat.util.http.mapper.Mapper,此物件維護了一個從Host到Wrapper的各級容器的快照。它主要是為了,當http request被解析後,能夠將http request繫結到相應的servlet進行業務處理。前面的文章中已經說明,在載入各層容器時,會將它們註冊到JMX中。
所以當connector元件啟動的時候,會從JMX中查詢出各層容器,然後再建立這個Mapper物件中的快照。
CoyoteAdapter
全路徑org.apache.catalina.connector.CoyoteAdapter,此物件負責將http request解析成HttpServletRequest物件,之後繫結相應的容器,然後從engine開始逐層呼叫valve直至該servlet。在session管理中,已經說明,根據request中的jsessionid繫結伺服器端的相應session。這個jsessionid按照優先順序或是從request url中獲取,或是從cookie中獲取,然後再session池中找到相應匹配的session物件,然後將其封裝到HttpServletRequest物件。所有這些都是在CoyoteAdapter中完成的。看一下將request解析為HttpServletRequest物件後,開始呼叫servlet的程式碼;
Java程式碼- connector.getContainer().getPipeline().getFirst().invoke(request,response);
connector的容器就是StandardEngine,程式碼的可讀性很強,獲取StandardEngine的pipeline,然後從第一個valve開始呼叫邏輯,相應的過程請參照tomcat架構分析(valve機制)。配置的connector的內部構造及訊息流,同時此connector也是基於BIO的實現。除了BIO外,也可以通過配置快速部署NIO的connector。在server.xml中如下配置;
Xml程式碼- <Connectorport="80"URIEncoding="UTF-8"protocol="org.apache.coyote.http11.Http11NioProtocol"
- connectionTimeout="20000"
- redirectPort="7443"/>
整個tomcat是一個比較完善的框架體系,各個元件之間都是基於介面的實現,所以比較方便擴充套件和替換。像這裡的“org.apache.coyote.http11.Http11NioProtocol”和BIO的“org.apache.coyote.http11.Http11Protocol”都是統一的實現org.apache.coyote.ProtocolHandler介面,所以從整體結構上來說,NIO還是與BIO的實現保持大體一致。
首先來看一下NIO connector的內部結構,箭頭方向還是訊息流;
還是可以看見connector中三大塊- Http11NioProtocol
- Mapper
- CoyoteAdapter
基本功能與BIO的類似,參見tomcat架構分析(connector BIO實現)。重點看看Http11NioProtocol.
和JIoEndpoint一樣,NioEndpoint是Http11NioProtocol中負責接收處理socket的主要模組。但是在結構上比JIoEndpoint要複雜一些,畢竟是非阻塞的。但是需要注意的是,tomcat的NIO connector並非完全是非阻塞的,有的部分,例如接收socket,從socket中讀寫資料等,還是阻塞模式實現的,在後面會逐一介紹。
如圖所示,NioEndpoint的主要流程;
圖中Acceptor及Worker分別是以執行緒池形式存在,Poller是一個單執行緒。注意,與BIO的實現一樣,預設狀態下,在server.xml中沒有配置<Executor>,則以Worker執行緒池執行,如果配置了<Executor>,則以基於java concurrent 系列的java.util.concurrent.ThreadPoolExecutor執行緒池執行。
Acceptor
接收socket執行緒,這裡雖然是基於NIO的connector,但是在接收socket方面還是傳統的serverSocket.accept()方式,獲得SocketChannel物件,然後封裝在一個tomcat的實現類org.apache.tomcat.util.net.NioChannel物件中。然後將NioChannel物件封裝在一個PollerEvent物件中,並將PollerEvent物件壓入events queue裡。這裡是個典型的生產者-消費者模式,Acceptor與Poller執行緒之間通過queue通訊,Acceptor是events queue的生產者,Poller是events queue的消費者。
Poller
Poller執行緒中維護了一個Selector物件,NIO就是基於Selector來完成邏輯的。在connector中並不止一個Selector,在socket的讀寫資料時,為了控制timeout也有一個Selector,在後面的BlockSelector中介紹。可以先把Poller執行緒中維護的這個Selector標為主Selector。
Poller是NIO實現的主要執行緒。首先作為events queue的消費者,從queue中取出PollerEvent物件,然後將此物件中的channel以OP_READ事件註冊到主Selector中,然後主Selector執行select操作,遍歷出可以讀資料的socket,並從Worker執行緒池中拿到可用的Worker執行緒,然後將socket傳遞給Worker。整個過程是典型的NIO實現。
Worker
Worker執行緒拿到Poller傳過來的socket後,將socket封裝在SocketProcessor物件中。然後從Http11ConnectionHandler中取出Http11NioProcessor物件,從Http11NioProcessor中呼叫CoyoteAdapter的邏輯,跟BIO實現一樣。在Worker執行緒中,會完成從socket中讀取http request,解析成HttpServletRequest物件,分派到相應的servlet並完成邏輯,然後將response通過socket發回client。在從socket中讀資料和往socket中寫資料的過程,並沒有像典型的非阻塞的NIO的那樣,註冊OP_READ或OP_WRITE事件到主Selector,而是直接通過socket完成讀寫,這時是阻塞完成的,但是在timeout控制上,使用了NIO的Selector機制,但是這個Selector並不是Poller執行緒維護的主Selector,而是BlockPoller執行緒中維護的Selector,稱之為輔Selector。
NioSelectorPool
NioEndpoint物件中維護了一個NioSelecPool物件,這個NioSelectorPool中又維護了一個BlockPoller執行緒,這個執行緒就是基於輔Selector進行NIO的邏輯。以執行servlet後,得到response,往socket中寫資料為例,最終寫的過程呼叫NioBlockingSelector的write方法。
Java程式碼- publicintwrite(ByteBufferbuf,NioChannelsocket,longwriteTimeout,MutableIntegerlastWrite)throwsIOException{
- SelectionKeykey=socket.getIOChannel().keyFor(socket.getPoller().getSelector());
- if(key==null)thrownewIOException("Keynolongerregistered");
- KeyAttachmentatt=(KeyAttachment)key.attachment();
- intwritten=0;
- booleantimedout=false;
- intkeycount=1;//assumewecanwrite
- longtime=System.currentTimeMillis();//startthetimeouttimer
- try{
- while((!timedout)&&buf.hasRemaining()){
- if(keycount>0){//onlywriteifwewereregisteredforawrite
- //直接往socket中寫資料
- intcnt=socket.write(buf);//writethedata
- lastWrite.set(cnt);
- if(cnt==-1)
- thrownewEOFException();
- written+=cnt;
- //寫資料成功,直接進入下一次迴圈,繼續寫
- if(cnt>0){
- time=System.currentTimeMillis();//resetourtimeouttimer
- continue;//wesuccessfullywrote,tryagainwithoutaselector
- }
- }
- //如果寫資料返回值cnt等於0,通常是網路不穩定造成的寫資料失敗
- try{
- //開始一個倒數計數器
- if(att.getWriteLatch()==null||att.getWriteLatch().getCount()==0)att.startWriteLatch(1);
- //將socket註冊到輔Selector,這裡poller就是BlockSelector執行緒
- poller.add(att,SelectionKey.OP_WRITE);
- //阻塞,直至超時時間喚醒,或者在還沒有達到超時時間,在BlockSelector中喚醒
- att.awaitWriteLatch(writeTimeout,TimeUnit.MILLISECONDS);
- }catch(InterruptedExceptionignore){
- Thread.interrupted();
- }
- if(att.getWriteLatch()!=null&&att.getWriteLatch().getCount()>0){
- keycount=0;
- }else{
- //還沒超時就喚醒,說明網路狀態恢復,繼續下一次迴圈,完成寫socket
- keycount=1;
- att.resetWriteLatch();
- }
- if(writeTimeout>0&&(keycount==0))
- timedout=(System.currentTimeMillis()-time)>=writeTimeout;
- }//while
- if(timedout)
- thrownewSocketTimeoutException();
- }finally{
- poller.remove(att,SelectionKey.OP_WRITE);
- if(timedout&&key!=null){
- poller.cancelKey(socket,key);
- }
- }
- returnwritten;
- }
也就是說當socket.write()返回0時,說明網路狀態不穩定,這時將socket註冊OP_WRITE事件到輔Selector,由BlockPoller執行緒不斷輪詢這個輔Selector,直到發現這個socket的寫狀態恢復了,通過那個倒數計數器,通知Worker執行緒繼續寫socket動作。看一下BlockSelector執行緒的邏輯;
Java程式碼- publicvoidrun(){
- while(run){
- try{
- ......
- Iteratoriterator=keyCount>0?selector.selectedKeys().iterator():null;
- while(run&&iterator!=null&&iterator.hasNext()){
- SelectionKeysk=(SelectionKey)iterator.next();
- KeyAttachmentattachment=(KeyAttachment)sk.attachment();
- try{
- attachment.access();
- iterator.remove();;
- sk.interestOps(sk.interestOps()&(~sk.readyOps()));
- if(sk.isReadable()){
- countDown(attachment.getReadLatch());
- }
- //發現socket可寫狀態恢復,將倒數計數器置位,通知Worker執行緒繼續
- if(sk.isWritable()){
- countDown(attachment.getWriteLatch());
- }
- }catch(CancelledKeyExceptionckx){
- if(sk!=null)sk.cancel();
- countDown(attachment.getReadLatch());
- countDown(attachment.getWriteLatch());
- }
- }//while
- }catch(Throwablet){
- log.error("",t);
- }
- }
- events.clear();
- try{
- selector.selectNow();//cancelallremainingkeys
- }catch(Exceptionignore){
- if(log.isDebugEnabled())log.debug("",ignore);
- }
- }
使用這個輔Selector主要是減少執行緒間的切換,同時還可減輕主Selector的負擔。以上描述了NIO connector工作的主要邏輯,可以看到在設計上還是比較精巧的。NIO connector還有一塊就是Comet,有時間再說吧。需要注意的是,上面從Acceptor開始,有很多物件的封裝,NioChannel及其KeyAttachment,PollerEvent和SocketProcessor物件,這些不是每次都重新生成一個新的,都是NioEndpoint分別維護了它們的物件池;
Java程式碼- ConcurrentLinkedQueue<SocketProcessor>processorCache=newConcurrentLinkedQueue<SocketProcessor>()
- ConcurrentLinkedQueue<KeyAttachment>keyCache=newConcurrentLinkedQueue<KeyAttachment>()
- ConcurrentLinkedQueue<PollerEvent>eventCache=newConcurrentLinkedQueue<PollerEvent>()
- ConcurrentLinkedQueue<NioChannel>nioChannels=newConcurrentLinkedQueue<NioChannel>()
當需要這些物件時,分別從它們的物件池獲取,當用完後返回給相應的物件池,這樣可以減少因為建立及GC物件時的效能消耗。
轉載於:https://my.oschina.net/liting/blog/473921