Jetty 服務器架構分析

http://blog.csdn.net/lovingprince/article/details/6202669

 以 jetty7 作爲分析目標, Jetty 是由一個或多個 connector 核心組件以及一些列 handler 組件和一個線程池組成,看一下結構圖:

 

 

           Connector 負責監聽接收客戶連接請求,而 handler 組件則負責處理請求並給予響應,前面兩個組件工作所需要的線程資源都直接從線程池 ThreadPool 中獲取。 Jetty  Server 可以有多個 connector 在不同的端口上監聽客戶請求,而每個 connector根據具體的使用場景不同可以有不同的實現,例如採用非阻塞 NioConnector 、阻塞 SocketConnector 等等,而對於請求處理的handler 組件,也根據具體需要可以使用不同的 handler ,此種設計提高了 jetty 的靈活性,需要 servlet ,則可以使用servletHandler ,需要 session ,則再增加一個 sessionHandler ,也就是說我們完全可以不使用 servlet 或者 session ,只要不配置這個 handler 就行了。

    要啓動和協調上訴核心組件工作, Jetty 提供了一個 Server 類來做這個事情 , 也就是說 Server 是應用的起始點,他負責創建並初始化 connector 、 handler 、 ThreadPool 組件,然後調用 start 方法啓動他們,讓所有組件都處於待命狀態,因此Server 類是一個比較重要的 Façade, 值得注意的 Server 類本身也是一個 handler.

 

一、       組件生命週期

 

 

對於 jetty 來說,每個組件都有其生命週期, jetty 採用了統一的 LifeCyle 接口來控制,我們來看下,類圖結構:

connector,handler 等組件全部都直接或間接實現了 LifeCyle 接口,剛纔說了 Server 也是 Handler ,同時他也是啓動或協調組件工作的類,也就是說 Server 可以通過 LifeCyle 接口控制其他組件的生命週期,通過 start 方法可以啓動 server, 通過 stop 則關閉了 server 。

 

 二、       Connector 組件

 

 

  Connnetor 在實現上有NIO 、BIO 兩種實現方式,並且支持AJP 協議、和SSL 。

三、       Handler 組件

所有的 handler 組件都實現了 Handler 接口,可以看到, Handler 是可以以鏈表的形式相互組合的, Server 作爲服務入口,本身也是 handler ,他繼承了HandlerWrapper 接口,我們看以看到他帶了一個 handler 的引用變量,我們可以注入 ServletHandler 支持 servlet, 注入 WebAppContext 則支持我們的 webapp應用。

 

 四、       啓動過程  

 

  先看下 jetty 的目錄結構

 

 

看幾個主要目錄的含義,

Bin 目錄定義了啓動 jetty 需要的 sh 文件,主要用在 linux 中, windows 中可以直接 java start.jar 啓動服務器,

  Contexts 目錄主要放置跟應用相關的 context 配置文件,跟應用相關

etc 目錄放置跟服務器相關的配置文件,其中會定義 contexts 目錄所在的位置

lib 是服務器所需要的 jar 包

webapps 是放置應用程序的位置,當然也可以通過在 contexts 中或者 etc 中自定義

 

我們從外部啓動一個 jetty 服務器的過程:

首先從 Start.jar 開始,這個 jar 定義瞭解析命令行的 Main 這個類, Main 主要負責解析 start.ini 配置文件 ,start.ini 中定義了 JVM 需要的參數以及 etc目錄中用到的 xml 配置文件,如下圖:

 

 

 

 

 

 

 

 

然後由 Config 類解析出 stat.ini 中 OPTIONS 選項指定的模塊的包的位置用來加入到 classpath 中,這些模塊的包都定義在 start.config 文件中 ( 該文件可以在 start.jar 包中找到 ) ,截取一個片段給大家看下:  

 

 

 

 

這個文件的配置是需要有一定的語法在裏面的,有興趣的可以研究一下,也就是說,通過在 start.ini 中定義 OPTIONS 以及在start.config 中定義模塊路徑就可以確定把哪些 jar 加入到環境變量中。

         以上準備工作做完之後,就可以真正開始服務器的處理了,這時你有兩種選擇,第一種是在本進程中通過反射方式啓動,但是缺點是 start.ini 中配置的 JVM 參數就形同虛設了,因爲 java 進程已經起來了,不能再按照新的堆參數等重新設置了;第二種方式就是重新啓動一個進程,就可以重新設置參數,前面說了, start.ini 中得到了啓動參數, start.config 中有了MainClass 和 Classpath 需要的 jar 包,則可以直接用 java xxx 方式啓動了,要使用這種方式啓動,只需要在 start.ini 中配置–exec 參數即可。

         MainClass 默認是 XmlConfigration 類,當然自定義的話,可以在 start.config 中去更改, XmlConfigration 做幾件事情:1 、根據 start.ini 中的定義的配置文件進行解析 , 例如 etc/jetty.xml 等 2 、通過自己的 IOC 將這些服務組件組裝在一起 3 、最後調用 start 方法啓動這些組件。


http://blog.csdn.net/lovingprince/article/details/6202828

接上一篇,說到XmlConfiguration ,XmlConfiguration 利用自己實現的 IOC 組裝 Server 的全過程如下圖所示:

這裏可以看到 3 個關鍵的配置文件, jetty.xml 、 jetty-deploy.xml 、以及 contexts/xxx.xml

l  Jetty.xml 文件中定義了入口類 Server, 以及其所需要的線程池、 Connector 、 Handler 。

l  Jetty-deploy.xml 中則定義了部署 web 應用用到部署工具,在其中指定了部署 web 應用的兩種方式,類似於 tomcat, 如果採用 webappProvider ,則表示將 web 應用放在 webapp 下即可生效,如果採用 ContextProvider ,則需要定義 Contexts目錄所在位置,只要在該目錄下放置任何應用的 context 配置文件就可以生效。

l  Xxx.xml 這是一個用戶自定義文件,表示採用 ContextProvider 時,在其中定義一個 WebAppContext 的 handler, 它指定了我們應用所在的位置,便於加載。

 

XmlConfiguration 解析裝配完畢之後,就開始啓動服務, Jetty 的啓動是從 Server 開始的,我們來看一下服務器真正的啓動過程。

 

 

 

 

 

 

從上圖中我們能大概看出服務器啓動過程,先是由用戶設置好需要的核心組件,然後調用 Server.start() 開始啓動服務,其中會首先啓動 handler 處理器,之後啓動用戶自定義組件,這個自定義組件需要實現 LifeCyle 接口,最後才啓動 Connector 接受請求。可以想到,關閉過程恰好是反過來,首先關閉接受請求的 connector ,然後再關閉用戶自定義組件,最後關閉 handler.

         我們再來詳細看一下 Server 源代碼的核心實現過程,當調用 start 方法時,其實是調用其祖先類 AbstractLifeCycle 中方法,該方法在這裏有一個模板實現,如下:

  1. public final void start() throws Exception  
  2.    {  
  3.        synchronized (_lock)  
  4.        {  
  5.            try  
  6.            {  
  7.                if (_state == __STARTED || _state == __STARTING)  
  8.                    return;  
  9.                setStarting();  
  10.                doStart();  
  11.                setStarted();  
  12.            }  
  13.            catch (Exception e)  
  14.            {  
  15.                setFailed(e);  
  16.                throw e;  
  17.            }  
  18.            catch (Error e)  
  19.            {  
  20.                setFailed(e);  
  21.                throw e;  
  22.            }  
  23.        }  

 

  • Connector 啓動過程

 

看下 Connector 的詳細啓動過程: (  NIO 爲例 )

 

NIOConnector 啓動過程中,先創建了多個 SelectSet 對象,每個 SelectSet 負責一個 NIO  Selector ,專門用於監聽 read 事件 ( 這裏利用的多線程 Reactor模式, http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf ,當然這裏僅僅是創建了對象,並沒有啓動,後面會提到。

SelectorManager 

 

 

然後再調用 open 創建了一個 blocking 的阻塞 channel ,專門用於接受用戶的新連接,我們看下:

 

  1. public void open() throws IOException  
  2.    {  
  3.        synchronized(this)  
  4.        {  
  5.            if (_acceptChannel == null)  
  6.            {  
  7.                // Create a new server socket  
  8.                _acceptChannel = ServerSocketChannel.open();  
  9.                // Set to blocking mode  
  10.                _acceptChannel.configureBlocking(true);  
  11.                // Bind the server socket to the local host and port  
  12.                _acceptChannel.socket().setReuseAddress(getReuseAddress());  
  13.                InetSocketAddress addr = getHost()==null?new InetSocketAddress(getPort()):new InetSocketAddress(getHost(),getPort());  
  14.          _acceptChannel.socket().bind(addr,getAcceptQueueSize());  
  15.                _localPort=_acceptChannel.socket().getLocalPort();  
  16.                if (_localPort<=0)  
  17.                    throw new IOException("Server channel not bound");  
  18.            }  
  19.        }  

 

隨後從線程池中分配了 N  ( 可以在配置文件中配置 ) 線程用於啓動 SelectSet 監聽 read 事件。

 

 

 

 

 

 

 

 

  1. synchronized (this)  
  2.         {  
  3.             _acceptorThread = new Thread[getAcceptors()];  
  4.             for (int i = 0; i < _acceptorThread.length; i++)  
  5.                 _threadPool.dispatch(new Acceptor(i));  
  6.             if (_threadPool.isLowOnThreads())  
  7.                 Log.warn("insufficient threads configured for {}",this);  
  8.         }  

最後再分配 1 個線程用於 accept 用戶的新連接,新連接來之後,會將其設置爲 nonblocking 模式,之後就將其 Register 給某個 SelectSet 去監聽 read 事件,然後又返回來繼續監聽新連接:

 

  1. _manager.dispatch(new Runnable()  
  2.         {  
  3.             public void run()  
  4.             {  
  5.                 final ServerSocketChannel server=_acceptChannel;  
  6.                 while (isRunning() && _acceptChannel==server && server.isOpen())  
  7.                 {  
  8.                     try  
  9.                     {  
  10.                         SocketChannel channel = server.accept();  
  11.                         channel.configureBlocking(false);  
  12.                         Socket socket = channel.socket();  
  13.                         configure(socket);  
  14.                         _manager.register(channel);  
  15.                     }  
  16.                     catch(IOException e)  
  17.                     {  
  18.                         Log.ignore(e);  
  19.                     }  
  20.                 }  
  21.             }  
  22.         });  

 

 

 

  • Handler 啓動過程

 

Jetty 將所有的真正處理請求的動作都抽象成了 Handler ,因此做事情的組件都是實現了這個接口的,包括上圖所示的WebAppContext 等等,需要做什麼樣的工作,那麼就添加什麼樣的 Handler ,這裏 SessionHandler 不是必須的,但是默認是創建好的。 ServletHandler 主要負責處理 web 應用的 Servlet 、 Filter 等工作,最後將請求直接交給 Servlet 、 Filter 都是在這裏完成。

   這裏展示的 Handler 的啓動過程其實是在準備 web 應用環境,例如解析 web 應用的 web.xml 等等工作,做好一切準備工作。


http://blog.csdn.net/lovingprince/article/details/6202859

說過了服務器啓動,最後來看一下請求處理過程, 服務器啓動好後,處於待命狀態,請求來了,請求處理過程由分兩個建階段:

 

  • 請求連接建立過程 (  NIO 爲例 )

     前面有提到,從線程池中固定分配了一個線程專門用於等待新連接,就是上圖的監聽線程,沒有請求來時,該線程是阻塞在 accept () 方法上的,當新連接來建立連接時, accept 方法分配了一個 socket ,並將其設置爲 nonblocking, 最後要做的就是將該 socket 丟給某個 Acceptor線程 ( 基本上機會均等 ) 處理,然後立馬返回繼續處於接受狀態,可以這個線程的工作是相當的簡單的,效率那也是相當的高。

         Acceptor 線程有很多個 ( 全部來自於線程池,並且固定分配出來,基於 jetty.xml 配置中的 Acceptors 配置數量 ) ,每個線程都維護了一個 SelectSet, 每個 SelectSet 又對應了一個 Selector, 這些線程會檢測當前是否有任務來,例如檢測 changes隊列中是否有任務,有並且是新連接,那麼就迅速建立一個 endpoint 點負責管理這個 socket ,並註冊 read 事件,後續該selector 就會負責該連接的 read 事件監聽。

         對於連接很多的情況,這裏分很多個 Selector 來分別監聽,提高了效率。

 

 

 

  • 請求數據處理過程 (  NIO 爲例 )

當數據發送過來時, Selector 檢測到 read 事件,會立馬調用 endpoint 的 schedule() 方法,該方法目的就是從線程池分配一個 worker 線程專門來處理這個 read 事件,而自己卻立馬返回繼續監聽,可見,這裏也是一個高效的處理方式。

業務線程分配成功後,負責請求的讀取以及解析,如果請求是完整的,那麼就開始調用 Server 的 handle 方法 (server 本身就是一個 handler) ,開始 handler 的處理,最後調用到 SerlvetHandler ,最終交給 Servlet 、 Filter ,開始了我們的自己應用。

 

      後記

 

1、  Jetty 的模塊化做得非常好,可以隨時替換其中的絕大部分關鍵部件,也可以拆掉,例如不需要處理 Session ,可以簡單配置一下即可搞定,不需要處理Servlet, 可以不用配置 ServletHandler.

2、  jetty 採用非阻塞 IO 時,我們可以看到從頭到尾的幾次線程池分配情況,第一次 分配一個固定線程監聽新連接,第二次 分配 N 個固定線程監聽 read 事件(這裏的 N 個線程在 7.3 版本中配置文件中配置 acceptors 數量即可,也就是說會從線程池固定分配 N 個線程出來),第三次 分配線程就是 read 事件到來之後,立即分配一個業務線程 ( 這個是臨時的,用了要回收 ) 處理數據直到我們應用返回結果。最後有一個地方 上面都沒有說到,那就是超時等原因要關閉連接時,是分配了臨時線程來處理這些事情

3、  模塊化、切分 task

4、  小,真的很小



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章