Java I/O演進(jìn)
在JDK1.4推出Java NIO之前揣炕,基于Java的所有Socket通信都采用了同步阻塞模式(BIO),這種——請求——應(yīng)答的通信模型簡化了上層的應(yīng)用開發(fā),但是在性能和可靠性方面卻存在著巨大的瓶頸陕贮。因此颖杏,在很長一段時間里,大型的應(yīng)用服務(wù)器都采用C或者C++語言開發(fā)落剪,因?yàn)樗鼈兛梢灾苯邮褂貌僮飨到y(tǒng)提供的異步I/O(AIO)能力。當(dāng)并發(fā)訪問量增大尿庐,響應(yīng)時間延遲增大之后忠怖,采用Java BIO并發(fā)的服務(wù)端軟件只有通過硬件的不斷擴(kuò)容來滿足高并發(fā)和低延時,它極大地增加了企業(yè)的成本抄瑟,并且隨著集群規(guī)模的不斷膨脹凡泣,系統(tǒng)的可維護(hù)性也面巨大的挑戰(zhàn)枉疼,只能通過采購性能更高的硬件服務(wù)器來解決問題,這會導(dǎo)致惡性循環(huán)鞋拟。
正是由于Java傳統(tǒng)BIO的拙劣表現(xiàn)骂维,才使得Java支持非阻塞I/O的呼的呼聲日漸高漲,最終贺纲,JDK1.4版本提供了新的NIO類庫(new input/output)航闺,Java終于也可以支持非阻塞I/O了。
Java I/O 發(fā)展簡史
從JDK1.0到JDK1.3猴誊,Java的I/O類庫都非常原始潦刃,很多UNIX網(wǎng)絡(luò)編程中的概念或者接口在I/O類庫中都沒有體現(xiàn),例如:Pipe懈叹,Channel乖杠,Buffer和Selector等。2002年發(fā)布JDK1.4時澄成,NIO以JSR-51的身份正式隨JDK發(fā)布胧洒。NIO主要的類和接口如下:
- 進(jìn)行異步I/O操作的緩沖區(qū)ByteBuffer等;
- 進(jìn)行異步I/O操作的管道Pipe墨状;
- 進(jìn)行各種I/O操作(異步或者同步)的Channel卫漫,包括ServerSocketChannel和SocketChannel;
- 多種字符集的編碼能力和解碼能力肾砂;
- 實(shí)現(xiàn)非阻塞I/O操作的多路復(fù)用器Selector列赎;
- 基于流行的Perl實(shí)現(xiàn)的正則表達(dá)式類庫;
- 文件通道FileChannel通今;
新的NIO類庫的提供粥谬,極大地促進(jìn)了基于Java的異步非阻塞編程的發(fā)展和應(yīng)用,但是辫塌,它依然有不完善的地方漏策,特別是對文件系統(tǒng)的處理能力仍顯不足,主要問題如下:
- 沒有統(tǒng)一的文件屬性(例如讀寫權(quán)限)
- API能力比較弱臼氨,例如目錄的級聯(lián)創(chuàng)建和遞歸遍歷掺喻,往往需要自己實(shí)現(xiàn)。
- 底層存儲系統(tǒng)的一些高級API無法使用储矩。
- 所有的文件操作都是同步阻塞調(diào)用感耙,不支持異步文件讀寫操作。
2011年7月28日持隧,JDK1.7正式發(fā)布即硼。它的一個比較大的亮點(diǎn)就是將原來的NIO類庫進(jìn)行了升級,被稱為NIO2.0屡拨。NIO2.0由JSR-203演進(jìn)而來只酥,它主要提供了如下三個方面的改進(jìn):
- 提供能夠批量獲取文件屬性的API褥实,這些API具有平臺無關(guān)性,不與特性的文件系統(tǒng)相耦合裂允,另外它還提供了標(biāo)準(zhǔn)文件系統(tǒng)的SPI损离,供各個服務(wù)提供商擴(kuò)展實(shí)現(xiàn)
- 提供AIO功能,支持基于文件的異步I/O操作和針對網(wǎng)絡(luò)套接字的異步操作
- 完成JSR-51定義的通道功能绝编,包括對配置和多播數(shù)據(jù)報的支持等僻澎。
傳統(tǒng)BIO編程
網(wǎng)絡(luò)編程的基本模型是Client/Server模型,也就是兩個進(jìn)程之間進(jìn)行相互通信十饥,其中服務(wù)端提供位置信息(綁定的IP地址和監(jiān)聽端口)窟勃,客戶端通過連接操作向服務(wù)器監(jiān)聽的地址發(fā)起連接請求,通過三次握手建立連接绷跑,如果連接建立成功拳恋,雙方就可以通過網(wǎng)絡(luò)套接字(Socket)進(jìn)行通信凡资。
在基于傳統(tǒng)同步阻塞模型開發(fā)中砸捏,ServerSocket負(fù)責(zé)綁定IP地址,啟動監(jiān)聽端口隙赁; Socket負(fù)責(zé)發(fā)起連接操作垦藏。連接成功之后,雙方通過輸入和輸出流進(jìn)行同步阻塞式通信伞访。
BIO通信模型
BIO的服務(wù)端通信模型:
采用BIO通信模型的服務(wù)端掂骏,通常由一個獨(dú)立的Acceptor線程負(fù)責(zé)監(jiān)聽客戶端的連接,它接收到客戶端連接請求之后為每個客戶端創(chuàng)建一個新的線程進(jìn)行鏈路處理厚掷,處理完成之后弟灼,通過輸出流返回應(yīng)答給客戶端,線程銷毀冒黑。這就是典型的 一請求一應(yīng)答通信模型田绑。
該模型最大的問題就是缺乏彈性伸縮能力,當(dāng)客戶端并發(fā)訪問量增加后抡爹,服務(wù)端的線程個數(shù)和客戶端并發(fā)訪問數(shù)呈1:1的正比關(guān)系掩驱,由于線程是Java虛擬機(jī)非常寶貴的系統(tǒng)資源,當(dāng)線程數(shù)膨脹之后冬竟,系統(tǒng)的性能將急劇下降欧穴,隨著并發(fā)訪問量的繼續(xù)增大,系統(tǒng)會發(fā)生線程堆棧溢出泵殴,創(chuàng)建新線程失敗等問題涮帘,并最終導(dǎo)致進(jìn)程宕機(jī)或者僵死,不能對外提供服務(wù)笑诅。
BIO 簡單示例
Server 端
同步阻塞IO的TimeServer:
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class TimeServer {
public static void main(String[] args) throws IOException{
int port = 8080;
if(args != null && args.length > 0){
try{
port = Integer.valueOf(args[0]);
}catch (NumberFormatException e){
//采用默認(rèn)值
}
}
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("The time server is start in port : " + port);
Socket socket = null;
while(true) {
socket = server.accept(); //接收客戶端連接請求调缨,沒有的時候就阻塞
System.out.println("收到來自客戶端的請求");
new Thread(new TimeServerHandler(socket)).start();
}
} finally{
if(server != null) {
System.out.println("The time server close");
server.close();
}
}
}
}
TimeServer根據(jù)傳入的參數(shù)設(shè)置監(jiān)聽端口映屋,如果沒有入?yún)ⅲ褂媚J(rèn)值8080同蜻,20行通過構(gòu)造函數(shù)創(chuàng)建ServerSocket棚点,如果端口合法且沒有被占用,服務(wù)端監(jiān)聽成功湾蔓。23-26行通過一個無限循環(huán)來監(jiān)聽客戶端的連接瘫析,如果沒有客戶端接入,則主線程阻塞在ServerSocket的accept操作上默责。啟動TimeServer贬循,通過JvisualVM打印線程堆棧,我們可以發(fā)現(xiàn)主程序確實(shí)阻塞在accept操作上
當(dāng)有新的客戶端接入的時候桃序,執(zhí)行代碼25行杖虾,以Socket為參數(shù)構(gòu)造TimeServerHandler對象,TimeServerHandler是一個Runnable媒熊,使用它為構(gòu)造函數(shù)的參數(shù)創(chuàng)建一個新的客戶端線程處理這條Socket鏈路奇适。下面我們繼續(xù)分析TimeServerHandler的代碼。
同步阻塞IO的TimeServerHandler:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class TimeServerHandler implements Runnable {
private Socket socket;
public TimeServerHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(),true);
String currentTime = null;
String body = null;
while(true) {
body = in.readLine(); //從客戶端讀
if(body == null) break;
System.out.println("The time server receive order: " + body);
currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ?
new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";
out.print(currentTime); //向客戶端寫
}
} catch (Exception e) {
e.printStackTrace();
if(in != null){
try {
in.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
if(out != null) {
out.close();
}
if(this.socket != null) {
try {
this.socket.close();
} catch (IOException ex) {
ex.printStackTrace();
}
this.socket = null;
}
}
}
}
25行通過BufferedReader讀取一行芦鳍,如果已經(jīng)讀到了輸入流的尾部嚷往,則返回值為null,退出循環(huán)柠衅。如果讀到了非空值皮仁,則對內(nèi)容進(jìn)行判斷,如果請求消息為查詢時間的指令”QUERY TIME ORDER”則獲取當(dāng)前最新的系統(tǒng)時間菲宴,通過PrintWriter的println函數(shù)發(fā)送給客戶端贷祈,最后退出循環(huán)。代碼35-52行釋放輸入流喝峦、輸出流势誊、和Socket套接字句柄資源,最后線程自動銷毀并被虛擬機(jī)回收愈犹。
Client端
客戶端通過Socket創(chuàng)建键科,發(fā)送查詢時間服務(wù)器的”QUERY TIME ORDER”指令,然后讀取服務(wù)端的響應(yīng)并將結(jié)果打印出來漩怎,隨后關(guān)閉連接勋颖,釋放資源,程序退出執(zhí)行勋锤。
同步阻塞IO的TimeClient:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class TimeClient {
public static void main(String[] args) {
int port = 8080;
if(args != null && args.length > 0){
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
//采用默認(rèn)值
}
}
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1",port); //創(chuàng)建socket
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
out.println("QUERY TIME ORDER"); //向服務(wù)端寫出QUERY TIME ORDER
System.out.println("Send order 2 server succeed.");
String resp = in.readLine(); //從服務(wù)端讀入
System.out.println("Now is : " + resp);
} catch (Exception e) {
e.printStackTrace();
//不需要處理
} finally {
if(out != null ){
out.close();
}
if(in != null){
try {
in.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
}
第23行客戶端通過PrintWriter向服務(wù)端發(fā)送”QUERY TIME ORDER”指令饭玲,然后通過BufferedReader的readLine讀取響應(yīng)并打印。
BIO潛在的問題
BIO主要的問題在于每當(dāng)有一個新的客戶端請求接入時叁执,服務(wù)端必須創(chuàng)建一個新的線程處理新接入的客戶端鏈路茄厘,一個線程只能處理一個客戶端連接矮冬。在高性能服務(wù)器應(yīng)用領(lǐng)域,往往需要面向成千上萬個客戶端的并發(fā)連接次哈,這種模型顯然無法滿足高性能胎署,高并發(fā)接入的場景。
為了改進(jìn)一線程一連接模型窑滞,后來又演進(jìn)出了一種通過線程池或者消息隊(duì)列實(shí)現(xiàn)1個或者多個線程處理N個客戶端的模型琼牧,由于它的底層通信機(jī)制依然使用同步阻塞I/O,所以被稱為“偽異步”哀卫,下一節(jié)會講到巨坊。