Jetty架構特點之Connector組件
和Tomcat一樣,Jetty也是一個“HTTP服務器 + Servlet容器”。 Jetty中的Connector組件和Handler組件分別來實現這兩個功能,而這兩個組件工作時所需要的線程資源都直接從一個全局線程池ThreadPool中獲取。
Jetty Server可以有多個Connector在不同的端口上監聽客戶請求,而對於請求處理的Handler組件,也可以根據具體場景使用不同的Handler。這樣的設計提高了Jetty的靈活性,需要支持Servlet,則可以使用ServletHandler;需要支持Session,則再增加一個SessionHandler。也就是說我們可以不使用Servlet或者Session,只要不配置這個Handler就行了。
爲了啓動和協調上面的核心組件工作,Jetty提供了一個Server類來做這個事情,它負責創建並初始化Connector、Handler、ThreadPool組件,然後調用start方法啓動它們。
二者區別
第一個區別是Jetty中沒有Service的概念,Tomcat中的Service包裝了多個連接器和一個容器組件,一個Tomcat實例可以配置多個Service,不同的Service通過不同的連接器監聽不同的端口;而Jetty中Connector是被所有Handler共享的。
第二個區別是,在Tomcat中每個連接器都有自己的線程池,而在Jetty中所有的Connector共享一個全局的線程池。
Connector組件
跟Tomcat一樣,Connector的主要功能是對I/O模型和應用層協議的封裝。I/O模型方面,最新的Jetty 9版本只支持NIO,因此Jetty的Connector設計有明顯的Java NIO通信模型的痕跡。至於應用層協議方面,跟Tomcat的Processor一樣,Jetty抽象出了Connection組件來封裝應用層協議的差異。
Java NIO回顧
Java NIO的核心組件是Channel、Buffer和Selector。Channel表示一個連接,可以理解爲一個Socket,通過它可以讀取和寫入數據,但是並不能直接操作數據,需要通過Buffer來中轉。
Selector可以用來檢測Channel上的I/O事件,比如讀就緒、寫就緒、連接就緒,一個Selector可以同時處理
多個Channel,因此單個線程可以監聽多個Channel,這樣會大量減少線程上下文切換的開銷。下面我們通
過一個典型的服務端NIO程序來回顧一下如何使用這些組件。
首先,創建服務端Channel,綁定監聽端口並把Channel設置爲非阻塞方式。
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(port));
server.configureBlocking(false);
然後,創建Selector,並在Selector中註冊Channel感興趣的事件OP_ACCEPT,告訴Selector如果客戶端有新的連接請求到這個端口就通知我。
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
接下來,Selector會在一個死循環裏不斷地調用select()去查詢I/O狀態,select()會返回一個SelectionKey列表,Selector會遍歷這個列表,看看是否有“客戶”感興趣的事件,如果有,就採取相應的動作。
比如下面這個例子,如果有新的連接請求,就會建立一個新的連接。連接建立後,再註冊Channel的可讀事件到Selector中,告訴Selector我對這個Channel上是否有新的數據到達感興趣。
while (true) {
selector.select();//查詢I/O事件
for (Iterator<SelectionKey> i = selector.selectedKeys().iterator(); i.hasNext();) {
SelectionKey key = i.next();
i.remove();
if (key.isAcceptable()) {
// 建立一個新連接
SocketChannel client = server.accept();
client.configureBlocking(false);
//連接建立後, 告訴Selector, 我現在對I/O可讀事件感興趣
client.register(selector, SelectionKey.OP_READ);
}
}
}
服務端在I/O通信上主要完成了三件事情:監聽連接、I/O事件查詢以及數據讀寫。因此Jetty設計了Acceptor、SelectorManager和Connection來分別做這三件事情,下面分別來說說這三個組件。
Acceptor
Acceptor用於接受請求,跟Tomcat一樣,Jetty也有獨立的Acceptor線程組用於處理連接請求。在Connector的實現類ServerConnector中,有一個_acceptors的數組,在Connector啓動的時候, 會根據_acceptors數組的長度創建對應數量的Acceptor,而Acceptor的個數可以配置。
for (int i = 0; i < _acceptors.length; i++){
Acceptor a = new Acceptor(i);
getExecutor().execute(a);
}
Acceptor是ServerConnector中的一個內部類,同時也是一個Runnable,Acceptor線程是通過getExecutor()得到的線程池來執行的,前面提到這是一個全局的線程池。
Acceptor通過阻塞的方式來接受連接,這一點跟Tomcat也是一樣的。
public void accept(int acceptorID) throws IOException{
ServerSocketChannel serverChannel = _acceptChannel;
if (serverChannel != null && serverChannel.isOpen()){
// 這⾥是阻塞的
SocketChannel channel = serverChannel.accept();
// 執⾏到這⾥時說明有請求進來了
accepted(channel);
}
}
接受連接成功後會調用accepted()函數,accepted()函數中會將SocketChannel設置爲非阻塞模式,然後交給Selector去處理,因此這也就到了Selector的地界了。
private void accepted(SocketChannel channel) throws IOException{
channel.configureBlocking(false);
Socket socket = channel.socket();
configure(socket);
// _manager是SelectorManager實例, ⾥⾯管理了所有的Selector實例
_manager.accept(channel);
}
SelectorManager
Jetty的Selector由SelectorManager類管理,而被管理的Selector叫作ManagedSelector。SelectorManager內部有一個ManagedSelector數組,真正幹活的是ManagedSelector。
public void accept(SelectableChannel channel, Object attachment){
//選擇一個ManagedSelector來處理Channel
final ManagedSelector selector = chooseSelector();
//提交一個任務Accept給ManagedSelector
selector.submit(selector.new Accept(channel, attachment));
}
SelectorManager從本身的Selector數組中選擇一個Selector來處理這個Channel,並創建一個任務Accept交給ManagedSelector,ManagedSelector在處理這個任務主要做了兩步:
第一步,調用Selector的register方法把Channel註冊到Selector上,拿到一個SelectionKey。
_key = _channel.register(selector, SelectionKey.OP_ACCEPT, this);
第二步,創建一個EndPoint和Connection,並跟這個SelectionKey(Channel)綁在一起:
private void createEndPoint(SelectableChannel channel, SelectionKey selectionKey) throws IOException{
//1. 創建Endpoint
EndPoint endPoint = _selectorManager.newEndPoint(channel, this, selectionKey);
//2. 創建Connection
Connection connection = _selectorManager.newConnection(channel, endPoint, selectionKey.attachment());
//3. 把Endpoint、 Connection和SelectionKey綁在一起
endPoint.setConnection(connection);
selectionKey.attach(endPoint);
}
你到餐廳喫飯,先點菜(註冊I/O事件),服務員(ManagedSelector)給你一個單子(SelectionKey),等菜做好了(I/O事件到了),服務員根據單子就知道是哪桌點了這個菜,於是喊一嗓子某某桌的菜做好了(調用了綁定在SelectionKey上的EndPoint的方法)。
ManagedSelector並沒有調用直接EndPoint的方法去處理數據,而是通過調用EndPoint的方法返回一個Runnable,然後把這個Runnable扔給線程池執行,這個Runnable纔會去真正讀數據和處理請求。
Connection
Runnable是EndPoint的一個內部類,它會調用Connection的回調方法來處理請求。Jetty的Connection組件類比就是Tomcat的Processor,負責具體協議的解析,得到Request對象,並調用Handler容器進行處理。下面我簡單介紹一下它的具體實現類HttpConnection對請求和響應的處理過程。
請求處理:HttpConnection並不會主動向EndPoint讀取數據,而是向在EndPoint中註冊一堆回調方法:
getEndPoint().fillInterested(_readCallback);
告訴EndPoint,數據到了你就調我這些回調方法_readCallback吧,有點異步I/O的感覺,也就是說Jetty在應用層面模擬了異步I/O模型。
而在回調方法_readCallback裏,會調用EndPoint的接口去讀數據,讀完後讓HTTP解析器去解析字節流,HTTP解析器會將解析後的數據,包括請求行、請求頭相關信息存到Request對象裏。
響應處理:Connection調用Handler進行業務處理,Handler會通過Response對象來操作響應流,向流裏面寫入數據,HttpConnection再通過EndPoint把數據寫到Channel,這樣一次響應就完成了。