java grizzly_Grizzly简介
作為Java EE Web層面的最前端,HTTP引擎是負責接收客戶請求的最開始的部分,這部分的性能在很大程度上決定了整個Java EE產品的性能和可擴展性。回顧現有的J2EE產品,大部分的HTTP引擎都不是用純Java編寫的。例如,Sun的JES應用服務器內置了一個用本地語言(C/C++)開發Web服務器,JBoss的Web Server也不是純Java的,它使用了大量與平臺相關的運行庫,只不過通過Apache的APR項目(http://apr.apache.org)來維護跨平臺的特性。而那些純Java的J2EE服務器,在部署的時候也推薦前置一個其他的Web服務器,例如(Apache、IIS等)。
使用純Java來構建具有擴展性很好的服務器軟件,一直是一個比較困難的事情,特別是在單個的Java虛擬機上(非集群的環境)。這是由Java的線程模型和網絡IO的特性所決定的。在JDK 1.4以前,Java的網絡IO的接口都是阻塞式的,這意味著網絡的阻塞會引起處理線程的停止,因此每個用戶請求的處理從開始到最后完成,需要單獨的處理線程。而Java的線程資源的分配和線程的調度都是有很大開銷的,這使得在大量請求(數千個甚至上萬個)同時到達的情況下,單個Java虛擬機很難滿足大并發性的需要。為了解決可擴展性的問題,一些解決方案使用了多個Java虛擬機或者多個機器節點進行集群來滿足大并發的請求。
JDK 1.4版本(包括之后的版本)最顯著的新特性就是增加了NIO(New IO),能夠以非阻塞的方式處理網絡的請求,這就使得在Java中只需要少量的線程就能處理大量的并發請求了。但是使用NIO不是一件簡單的技術,它的一些特點使得編程的模型比原來阻塞的方式更為復雜。
Grizzly作為GlassFish中非常重要的一個項目,就是用NIO的技術來實現應用服務器中的高性能純Java的HTTP引擎。Grizzly還是一個獨立于GlassFish的框架結構,可以單獨用來擴展和構建自己的服務器軟件。
本章重點:
l?? NIO的基本特點和編程方式
l?? Grizzly的基本結構
l?? Grizzly對NIO技術的運用手段
l?? Grizzly對性能上的考慮和優化
17.1 NIO簡介
理解NIO是學習本章的重要前提,因為Grizzly本身就是基于NIO的框架結構,所有的技術問題都是在NIO的技術上進行討論的。如果讀者對NIO不了解的話,建議首先了解NIO的基本概念。對NIO的介紹和學習指南很多,本章不會對NIO做詳細的講解。下面僅對NIO做一個簡單的介紹,并列出與本章內容相關的一些NIO特性。
17.1.1 NIO的基本概念
在JDK 1.4的新特性中,NIO無疑是最顯著和鼓舞人心的。NIO的出現事實上意味著Java虛擬機的性能比以前的版本有了較大的飛躍。在以前的JVM的版本中,代碼的執行效率不高(在最原始的版本中Java是解釋執行的語言),用Java編寫的應用程序通常所消耗的主要資源就是CPU,也就是說應用系統的瓶頸是CPU的計算和運行能力。在不斷更新的Java虛擬機版本中,通過動態編譯技術使得Java代碼執行的效率得到大幅度提高,幾乎和操作系統的本地語言(例如C/C++)的程序不相上下。在這種情況下,應用系統的性能瓶頸就從CPU轉移到IO操作了。尤其是服務器端的應用,大量的網絡IO和磁盤IO的操作,使得IO數據等待的延遲成為影響性能的主要因素。NIO的出現使得Java應用程序能夠更加緊密地結合操作系統,更加充分地利用操作系統的高級特性,獲得高性能的IO操作。
NIO在磁盤IO處理和文件處理上有很多新的特性來提高性能,本文不作詳細的解釋,而僅僅介紹NIO在處理網絡IO方面的新特點,這些特點是理解Grizzly的最基本的概念。
1. 數據緩沖(Buffer)處理
數據緩沖(Buffer)是IO操作的基本元素。其實從本質上來說,無論是磁盤IO還是網絡IO,應用程序所作的所有事情就是把數據放到相應的數據緩沖當中去(寫操作),或者從相應的數據緩沖中提取數據(讀操作)。至于數據緩沖中的數據和IO設備之間的交互,則是操作系統和硬件驅動程序所關心的事情了。因此,數據緩沖在IO操作中具有重要的作用,是操作系統與應用之間的IO橋梁。在NIO的包中,Buffer類是所有類的基礎。Buffer類當中定義數據緩沖的基本操作,包括put、get、reset、clear、flip、rewind等,這些基本操作是進行數據輸入輸出的手段。每一個基本的Java類型(boolean除外)都有相應的Buffer類,例如CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer和ByteBuffer。我們所關心的是ByteBuffer,因為操作系統與應用程序之間的數據通信最原始的類型就是Byte。
“Direct ByteBuffer”是一個值得關注的Buffer類型。在創建ByteBuffer的時候可以使用ByteBuffer.allocateDirect()來創建一塊直接(Direct)的ByteBuffer。這一塊數據緩沖和一般的緩沖不一樣。第一,它是一塊連續的空間。第二,它的實現不是純Java的代碼,而是本地代碼,它內存的分配不在Java的堆棧中,不受Java內存回收的影響。這種直接的ByteBuffer是NIO用來保證性能的重要手段。剛才提到,數據緩沖是操作系統和應用程序之間的IO接口。應用程序將需要“寫出去”的數據放到數據緩沖中,操作系統從這塊緩沖中獲得數據執行寫的操作。當IO設備數據傳進來的時候,操作系統就會將數據放到相應的數據緩沖中,應用程序從緩沖中“讀進”數據進行處理。一般的Java對象很難勝任這個直接的數據緩沖的工作。因為Java對象所占用的內存空間不一定是連續的,而且經常由于內存回收而改變地址。而操作系統需要的是一片連續的不變動的地址空間,才能完成IO操作。在原來的Java版本中需要Java虛擬機的介入,將數據進行轉換、拷貝才能被操作系統所使用。而通過“Direct ByteBuffer”,應用程序能夠直接與操作系統進行交流,大大減少了系統調用的次數,提高了執行的效率。
數據緩沖的另外一個重要的特點是可以在一個數據緩沖上再建立一個或多個視圖(View)緩沖。這個概念有些類似于數據庫視圖的概念:在數據庫的物理表(Table)結構之上可以建立多個視圖。同樣,在一個數據緩沖之上也可以建立多個邏輯的視圖緩沖。視圖緩沖的用處很多,例如可以將Byte類型的緩沖當作Int類型的視圖,來進行類型轉換。視圖緩沖也可以將一個大的緩沖看成是很多小的緩沖視圖。這對提高性能很有幫助,因為創建物理的數據緩沖(特別是直接的數據緩沖)是非常耗時的操作,而創建視圖卻非常快。在Grizzly中就有這方面的考慮。
2. 異步通道(Channel)
Channel(后文又稱頻道,譯法僅暗示存在多通道可選)是NIO的另外一個比較重要的新特點。Channel并不是對原有Java類的擴充和完善,而是完全嶄新的實現。通過Channel,Java應用程序能夠更好地與操作系統的IO服務結合起來,充分地利用上文提到的ByteBuffer,完成高性能的IO操作。Channel的實現也不是純Java的,而是和操作系統結合緊密的本地代碼。
Channel的一個重要的特點是在網絡套接字頻道(SocketChannel)中,可以將其設置為異步非阻塞的方式。
【例17.1】非阻塞方式的頻道使用:
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false); // nonblocking
...
if (!sc.isBlocking()) {
doSomething(cs);
}
通過SocketChannel.configureBlocking(false)就可以將網絡套接字頻道設置為異步非阻塞模式。一旦設置成非阻塞的方式,從Socket中讀和寫就再也不會阻塞。雖然非阻塞只是一個設置問題,但是對應用程序的結構和性能卻產生了天翻地覆的變化。
3. 有條件的選擇(Readiness Selection)
熟悉UNIX的程序員對POSIX的select()或poll()函數應該比較熟悉。在現在大多數流行的操作系統中,都支持有條件地選擇已經準備好的IO通道,這就使得只需要一個線程就能同時有效地管理多個IO通道。在JDK 1.4以前,Java語言是不具備這個功能的。
NIO通過幾個關鍵的類來實現這種有條件的選擇的功能:
(1)?? Selector
Selector類維護了多個注冊的Channel以及它們的狀態。Channel需要向Selector注冊,Selector負責維護和更新Channel的狀態,以表明哪些Channel是準備好的。
(2)?? SelectableChannel
SelectableChannel是可以被Selector所管理的Channel。FileChannel不屬于Selectable- Channel,而SocketChannel是屬于這類的Channel。因此在NIO中,只有網絡的IO操作才有可能被有條件地選擇。
(3)?? SelectionKey
SelectionKey用于維護Selector和SelectableChannel之間的映射關系。當一個Channel向Selector注冊之后,就會返回一個SelectionKey作為注冊的憑證。SelectionKey中保存了兩類狀態值,一是這個Channel中哪些操作是被注冊了的,二是有哪些操作是已經準備好的。
17.1.2 NIO之前的Server程序的架構
在NIO出現以前(甚至在NIO出現了很長時間的現在),在用Java編寫服務器端的程序時,服務請求的接收模塊大多數都會采用以下的框架(例如在Tomcat中的連接接入點:org.apache.tomcat.util.net.PoolTcpEndpoint就有相類似的結構)。
【例17.2】阻塞方式的server編程框架:
class Server implements Runnable {
public void run() {
try {
ServerSocket ss = new ServerSocket(PORT);
while (!Thread.interrupted())
new Thread(new Handler(ss.accept())).start();
} catch (IOException ex) { /* ... */ }
}
static class Handler implements Runnable {
final Socket socket;
Handler(Socket s) { socket = s; }
public void run() {
try {
byte[] input = new byte[MAX_INPUT];
socket.getInputStream().read(input);
byte[] output = process(input);
socket.getOutputStream().write(output);
} catch (IOException ex) { /* ... */ }
}
private byte[] process(byte[] cmd) { /* ... */ }
}
}
上面的結構比較簡單:在主線程的run()方法中,會有ServerSocket的accept()方法,它被循環地調用著,直到服務停止。accept()方法會被阻塞,直到新的連接請求的到來。當新的連接請求進來以后,系統會使用另外的線程來處理這個請求。處理線程在socket端口進行read()調用,讀取所有的請求數據。read()也是一個阻塞的方法,一直到讀取完所有的數據才會返回。數據經過處理以后,在同一個處理線程中將請求結果返回給客戶端。在實際情況中,會比這個結構復雜得多,例如,處理線程是從一個線程池中獲取,而不是每次都產生一個新的線程。
這種結構在大多數情況下都可以獲得很好的性能。例如Tomcat在性能指標的測試中獲得了很高的吞吐量測量值。但是在并發性很大的情況下,這種結構不具有很好的可擴展性。例如有2000個客戶請求同時到來,如果想要這2000個請求被同時處理,則需要2000個處理線程。這些線程在大多數的情況下可能都不在運行,而是阻塞在read()或write()的方法上了。在一臺機器或者一個Java虛擬機上運行上千個線程是個挑戰,線程經常會阻塞,因此CPU會在這些線程之間來回調度和切換,這會引起大量的系統調用和資源競爭,使得整個系統的擴展性能不高。
17.1.3 使用NIO來提高系統擴展性
NIO使用非阻塞的API,通過實現少量的線程就能服務于大量的并發用戶的請求。并且通過操作系統都支持的POSIX標準的select方式,來獲得系統準備就緒的資源。使用這些手段,NIO就能夠充分利用每個活動的線程來服務于大量的請求,減少系統資源的浪費。通常來說,一個NIO的服務架構會采用以下的結構。
【例17.3】使用NIO的server編程框架:
public class Server {
public static void main(String[] argv) throws Exception {
ServerSocketChannel serverCh = ServerSocketChannel.open();
Selector selector = Selector.open();
ServerSocket serverSocket = serverCh.socket();
serverSocket.bind(new InetSocketAddress(80));
serverCh.configureBlocking(false);
serverCh.register(selector,SelectionKey.OP_ACCEPT);
while(true){
selector.select();
Iterator it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey)it.next();
if (key.isAcceptable()) {
ServerSocketChannel server =
(ServerSocketChannel)key.channel();
SocketChannel channel = server.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
readDataFromSocket(key);
}
it.remove();
}
}
}
}
上面的結構比起阻塞式的框架都復雜一些。具體說明如下:
l?? 通過ServerSocketChannel.open()獲得一個Server的Channel對象。
l?? 通過Selector.open()來獲得一個Selector對象。
l?? 從Server的Channel對象上可以獲得一個Server的Socket,并讓它在80端口監聽。
l?? 通過ServerSocketChannel.configureBlocking(false)可以將當前的Channel配置成異步非阻塞的方式。如果沒有這一步,那么Channel默認的方式跟傳統的一樣,是阻塞式的。
l?? 將當前的Channel注冊到Selector對象中去,并告訴Selector當前的Channel關心的操作是OP_ACCEPT,也就是當有新的請求的時候,Selector負責更新此Channel的狀態。
l?? 在循環當中調用selector.select(),如果當前沒有任何新的請求過來,并且原來的連接也沒有新的請求數據到達,這個方法會阻塞住,一直等到新的請求數據過來為止。
l?? 如果當前都請求的數據到達,那么selector.select()就會立刻退出,這時候可以從selector.selectedKeys()獲得所有在當前selector注冊過的并且有數據到達的這些Channel的信息(SelectionKey)。
l?? 遍歷所有的這些SelectionKey來獲得相關的信息。如果某個SelectionKey的操作是OP_ACCEPT,也就是isAcceptable,那么可以判定這是那個Server Channel,并且是有新的連接請求到達了。
l?? 當有新的請求來的時候,通過accept()方法可以獲得新的channel服務于這個新來的請求。然后通過configureBlocking(false)可以將當前的Channel配置成異步非阻塞的方式。
l?? 接著將這個新的channel也注冊到selector中,并告訴Selector當前的Channel關心的操作是OP_READ,也就是當前Channel有新的數據到達的時候,Selector負責更新此Channel的狀態。
l?? 如果在循環當中發現某個SelectionKey的操作是OP_READ,也就是isReadable,那么可以判定這不是那個Server Channel,而是在循環內部注冊的連接Channel,表明當前SelectionKey對應的這個Channel有數據到達了。
l?? 有數據到達之后的處理方式是下面要詳細討論的問題,在這里,我們簡單地用一個方法readDataFromSocket(key)來表示,功能就是從這個Channel中讀取數據。
從這個框架結構中可以看到,在一個線程中可以同時服務于多個連接,包括Server的監聽服務。在同一個時刻,并不是所有的連接都會有數據到達,因此為每一個連接分配單獨的線程沒有必要。使用異步非阻塞方式,可以使用很少的線程,通過Select的方式來服務于多個連接請求,效率大大提高。
17.1.4 使用NIO來制作HTTP引擎的最大挑戰
程序實例17.3使用了configureBlocking(false)方法來將一個Channel設置成非阻塞式的。如何使用這個非阻塞的特性,請參看下面的方法調用:
count = socketChannel.read(byteBuffer)); //非阻塞的方式
阻塞式的方法調用如下:
count = socket.getInputStream().read(input); //阻塞的方式
阻塞的方式下的read,會一直等到byte[]類型的input被充滿,或者InputStream遇到EOF(socket連接被關閉)的時候,這個函數調用才會被返回。而非阻塞的方式,立刻就返回了,當前連接中有多少數據就讀多少。正因為有了這種非阻塞的模式,當前的線程在讀了某個通道的數據之后,可以接著再讀另外一個通道的數據,線程的利用率大大提高。
雖然線程的利用率提高了,卻帶來了一些其他的挑戰。最大的挑戰就在于:當一個請求過來的時候,很難判斷什么時候所有請求的數據全部讀進來了。因為每次非阻塞方式的read都可能只讀了一部分數據,甚至什么也沒有讀到。例如,一個HTTP請求:
HTTP/1.1 206 Partial content
GET http://www.w3.org/pub/WWW/TheProject.html
所有的請求數據都是以文本方式傳輸。在非阻塞的方式下,每一次對Channel進行讀取的數據量大小不可預測,也許第一次讀了“HTTP/1.1 206 Partial content”,第二次讀取了“GET http://www.w3.org/pub/WWW”,第三次什么也沒有讀到。到底什么時候能把請求全部讀完很難預測,在極端的情況下,也許最后幾個字符永遠也讀不到。在請求沒有完全讀到以前,一般不進行請求處理,因為請求還不完整。在阻塞的情況下,讀取的函數會一直等到請求的數據全部到來并且連接關閉以后才會返回,處理起來比較簡單。但是非阻塞的方式就很復雜了。因為工作線程從一個連接讀取完準備好的數據之后,又要為另一個連接服務。下次再轉到先前連接的時候,以前讀取的數據還需要恢復。還需要判斷到底所有的請求數據是否都讀完,是否可以開始對該請求的處理了。
在本章的后面各節中,我們會看到Grizzly采用了一個有限狀態機來解析HTTP請求的header信息,讀取其中的content-length數值,以便預先判斷什么時候到達請求的末尾。
總結
以上是生活随笔為你收集整理的java grizzly_Grizzly简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java flex xml_FLEX与J
- 下一篇: java string 反序列化_如何将