IO API的可伸縮性對Web應用有著極其重要的意義
Java
版以前的API中
阻塞I/O令許多人失望
從J
SE
版本開始
Java終於有了可伸縮的I/O API
本文分析並計算了新舊I/O API在可伸縮性方面的差異
一概述 IO API的可伸縮性對Web應用有著極其重要的意義
Java
版以前的API中
阻塞I/O令許多人失望
從J
SE
版本開始
Java終於有了可伸縮的I/O API
本文分析並計算了新舊IO API在可伸縮性方面的差異
Java向Socket寫入數據時必須調用關聯的OutputStream的write()方法
只有當所有的數據全部寫入時
write()方法調用才會返回
倘若發送緩沖區已滿且連接速度很低
這個調用可能需要一段時間才能完成
如果程序只使用單一的線程
其他連接就必須等待
即使那些連接已經做好了調用write()的准備也一樣
為了解決這個問題
你必須把每一個Socket和一個線程關聯起來
采用這種方法之後
當一個線程由於I/O相關的任務被阻塞時
另一個線程仍舊能夠運行
盡管線程的開銷不如進程那麼大
但是
考慮到底層的操作平台
線程和進程都屬於消耗大量資源的程序結構
每一個線程都要占用一定數量的內存
而且除此之外
多個線程還意味著線程上下文的切換
而這種切換也需要昂貴的資源開銷
因此
Java需要一個新的API來分離Socket與線程之間過於緊密的聯系
在新的Java I/O API(java
nio
*)中
這個目標終於實現了
本文分析和比較了用新
舊兩種I/O API編寫的簡單Web服務器
由於作為Web協議的HTTP不再象原來那樣只用於一些簡單的目的
因此這裡介紹的例子只包含關鍵的功能
或者說
它們既不考慮安全因素
也不嚴格遵從協議規范
二用舊API編寫的HTTP服務器 首先我們來看看用舊式API編寫的HTTP服務器
這個實現只使用了一個類
main()方法首先創建了一個綁定到
端口的ServerSocket
public static void main() throws IOException {
ServerSocket serverSocket = new ServerSocket(
);
for (int i=
; i < Integer
parseInt(args[
]); i++) {
new Httpd(serverSocket);
}
}
接下來
main()方法創建了一系列的Httpd對象
並用共享的ServerSocket初始化它們
在Httpd的構造函數中
我們保證每一個實例都有一個有意義的名字
設置默認協議
然後通過調用其超類Thread的start()方法啟動服務器
此舉導致對run()方法的一次異步調用
而run()方法包含一個無限循環
在run()方法的無限循環中
ServerSocket的阻塞性accpet()方法被調用
當客戶程序連接服務器的
端口
accept()方法將返回一個Socket對象
每一個Socket關聯著一個InputStream和一個OutputStream
兩者都要在後繼的handleRequest()方法調用中用到
這個方法將讀取客戶程序的請求
經過檢查和處理
然後把合適的應答發送給客戶程序
如果客戶程序的請求合法
通過sendFile()方法返回客戶程序請求的文件
否則
客戶程序將收到相應的錯誤信息(調用sendError())方法
while (true) {
socket = serverSocket
accept();
handleRequest();
socket
close();
}
現在我們來分析一下這個實現
它能夠出色地完成任務嗎?答案基本上是肯定的
當然
請求分析過程還可以進一步優化
因為在性能方面StringTokenizer的聲譽一直不佳
但這個程序至少已經關閉了TCP延遲(對於短暫的連接來說它很不合適)
同時為外發的文件設置了緩沖
而且更重要的是
所有的線程操作都相互獨立
新的連接請求由哪一個線程處理由本機的(因而也是速度較快的)accept()方法決定
除了ServerSocket對象之外
各個線程之間不共享可能需要同步的任何其他資源
這個方案速度較快
但令人遺憾的是
它不具有很好的可伸縮性
其原因就在於
很顯然地
線程是一種有限的資源
三非阻塞的HTTP服務器 下面我們來看看另一個使用非阻塞的新I/O API的方案
新的方案要比原來的方案稍微復雜一點
而且它需要各個線程的協作
它包含下面四個類
NIOHttpd
Acceptor
Connection
ConnectionSelector
NIOHttpd的主要任務是啟動服務器
就象前面的Httpd一樣
一個服務器Socket被綁定到
端口
兩者主要的區別在於
新版本的服務器使用java
nio
channels
ServerSocketChannel而不是ServerSocket
在利用bind()方法顯式地把Socket綁定到端口之前
必須先打開一個管道(Channel)
然後
main()方法實例化了一個ConnectionSelector和一個Acceptor
這樣
每一個ConnectionSelector都可以用一個Acceptor注冊
另外
實例化Acceptor時還提供了ServerSocketChannel
public static void main() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel
open();
ssc
socket()
bind(new InetSocketAddress(
));
ConnectionSelector cs = new ConnectionSelector();
new Acceptor(ssc
cs);
}
為了理解這兩個線程之間的交互過程
首先我們來仔細地分析一下Acceptor
Acceptor的主要任務是接受傳入的連接請求
並通過ConnectionSelector注冊它們
Acceptor的構造函數調用了超類的start()方法
run()方法包含了必需的無限循環
在這個循環中
一個阻塞性的accept()方法被調用
它最終將返回一個Socket對象——這個過程幾乎與Httpd的處理過程一樣
但這裡使用的是ServerSocketChannel的accept()方法
而不是ServerSocket的accept()方法
最後
以調用accept()方法獲得的socketChannel對象為參數創建一個Connection對象
並通過ConnectionSelector的queue()方法注冊它
while (true) {
socketChannel = serverSocketChannel
accept();
connectionSelector
queue(new Connection(socketChannel));
}
總而言之
Acceptor只能在一個無限循環中接受連接請求和通過ConnectionSelector注冊連接
與Acceptor一樣
ConnectionSelector也是一個線程
在構造函數中
它構造了一個隊列
並用Selector
open()方法打開了一個java
nio
channels
Selector
Selector是整個服務器中最重要的部分之一
它使得程序能夠注冊連接
能夠獲取已經允許讀取和寫入操作的連接的清單
構造函數調用start()方法之後
run()方法裡面的無限循環開始執行
在這個循環中
程序調用了Selector的select()方法
這個方法一直阻塞
直到已經注冊的連接之一做好了I/O操作的准備
或Selector的wakeup()方法被調用
while (true) {
int i = selector
select();
registerQueuedConnections();
// 處理連接
}
當ConnectionSelector線程執行select()時
沒有一個Acceptor線程能夠用該Selector注冊連接
因為對應的方法是同步方法
理解這一點是很重要的
因此這裡使用了隊列
必要時Acceptor線程向隊列加入連接
public void queue(Connection connection) {
synchronized (queue) {
queue
add(connection);
}
selector
wakeup();
}
緊接著把連接放入隊列的操作
Acceptor調用Selector的wakeup()方法
這個調用導致ConnectionSelector線程繼續執行
從正在被阻塞的select()調用返回
由於Selector不再被阻塞
ConnectionSelector現在能夠從隊列注冊連接
在registerQueuedConnections()方法中
其實施過程如下
if (!queue
isEmpty()) {
synchronized (queue) {
while (!queue
isEmpty()) {
Connection connection =
(Connection)queue
remove(queue
size()
);
connection
register(selector);
}
}
}
From:http://tw.wingwit.com/Article/program/Java/gj/201311/27392.html