本篇主要從學(xué)習(xí)角度整理java的幾個(gè)網(wǎng)絡(luò)模型,包括:
- BIO通信模型
- 偽異步通信模型
- NIO通信模型
- NIO2.0(AIO)
BIO通信模型

BIO通信模型最大的特點(diǎn)是,當(dāng)服務(wù)端程序收到一條網(wǎng)絡(luò)連接請(qǐng)求時(shí),需要單獨(dú)為其分配一個(gè)處理線程,服務(wù)端處理完成之后,將輸出流返回給客戶端,此時(shí)才銷毀線程。
例如上圖中的案例,acceptor在編程時(shí)一般就是ServerSocket,通過一個(gè)無限循環(huán)的accept操作獲取客戶端請(qǐng)求,然后分配一個(gè)線程為其進(jìn)行處理,類似的代碼如下:
//BIO 服務(wù)端示例代碼
//其中SomeHandler為具體的網(wǎng)絡(luò)業(yè)務(wù)處理器
ServerSocket server = new ServerSocket(port);
while(true){
Socket socket = server.accept();
new Thread(SomeHandler(socket)).start();
}
該模型最大的問題是缺乏彈性伸縮能力,客戶端和服務(wù)端線程個(gè)數(shù)的比例為1:1,由于線程是Java虛擬機(jī)非常寶貴的系統(tǒng)資源,當(dāng)線程數(shù)膨脹,系統(tǒng)性能也將急劇下降,隨著并發(fā)訪問量增大,系統(tǒng)會(huì)發(fā)生線程堆棧溢出,創(chuàng)建線程失敗等問題。
偽異步I/O通信模型

為了改進(jìn)BIO的一個(gè)線程一個(gè)連接的模型,引入線程池或者消息隊(duì)列來實(shí)現(xiàn)1個(gè)或者多個(gè)線程處理N個(gè)客戶端的模型,但由于底層仍然使用同步阻塞I/O,因此被稱為“偽異步”。
服務(wù)端的示例代碼如下:
//偽異步網(wǎng)絡(luò)通信服務(wù)端示例代碼
//其中SomeHandler為具體的網(wǎng)絡(luò)業(yè)務(wù)處理器
ServerSocket server = new ServerSocket(port);
ExecutorService executor = Executors.newFixedThreadPool(100);
while(true){
Socket socket = server.accept();
executor.submit(SomeHandler(socket));
}
最大的不同可以看出是在處理網(wǎng)絡(luò)請(qǐng)求的地方,偽異步使用了線程池。這樣可以避免線程的不斷銷毀和重新創(chuàng)建,但是本質(zhì)上,一條連接任然獨(dú)占一個(gè)線程,意思是如果一條連接不斷開,這個(gè)線程將被一直阻塞,不管期間有沒有數(shù)據(jù)傳輸。
NIO編程模型
NIO可以稱為非阻塞I/O(Non-block I/O)。它提供了高速的、面向塊的I/O。補(bǔ)充一下NIO的一些概念,以便作說明。
緩沖區(qū)Buffer
BIO編程中,數(shù)據(jù)的輸入輸出靠的是流。NIO通過Buffer來緩存操作期間的數(shù)據(jù),相比之下,緩沖區(qū)提供了對(duì)數(shù)據(jù)的結(jié)構(gòu)化訪問。最常用的緩沖區(qū)是ByteBuffer,它提供了一組功能來操作byte數(shù)組。(事實(shí)上,每一種Java基本類型,除了Boolean,都有對(duì)應(yīng)的一種緩沖區(qū),例如CharBuffer、ShortBuffer等)。
通道Channel
理解通道就可以認(rèn)為它像一條水管,網(wǎng)絡(luò)數(shù)據(jù)可以在Channel上任意的寫入和讀取,它是雙向的(區(qū)別于流的單向)。
多路復(fù)用器
NIO編程的基礎(chǔ)就是多路復(fù)用器Selector,它提供選擇已經(jīng)就緒的任務(wù)的能力。通常在selector上會(huì)注冊很多channel(來自于客戶端的網(wǎng)絡(luò)請(qǐng)求),selector通不過不斷輪詢,偵測哪一個(gè)channel上有數(shù)據(jù)的讀寫信號(hào),就通過SelectionKey讓該channel激活進(jìn)行相應(yīng)的讀寫操作。
示例
服務(wù)端時(shí)序圖

上圖描述了NIO編程過程中的通信時(shí)序圖,異步的網(wǎng)絡(luò)請(qǐng)求代碼更不易編寫,下面看看服務(wù)端的簡單示例代碼:
private Selector selector;
private ServerSocketChannel serverChannel;
public void init(){
selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.configurateBlocking(false);
serverChannel.socket.bind(port);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
public void run(){
while(true){
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
handleInput(key); // 網(wǎng)絡(luò)請(qǐng)求處理器
}
}
}
簡單的看這段代碼,在初始化時(shí)需要執(zhí)行的操作包括:
- open一個(gè)selector和ServerSocketChannel
- 設(shè)置非阻塞模式
- 綁定端口號(hào)
- 將channel注冊到selector上,接受accept事件
接著主循環(huán)要做的事情包括:
- 輪詢select
- 從selector中獲取觸發(fā)了信號(hào)的SelectionKey
- 將SelectionKey交給網(wǎng)絡(luò)請(qǐng)求處理器進(jìn)行處理(處理器要完成的事情包括接受連接、接收數(shù)據(jù),解碼數(shù)據(jù),寫回?cái)?shù)據(jù)等)
客戶端時(shí)序圖

客戶端的代碼編寫邏輯也很類似,基本的原理就是創(chuàng)建一個(gè)channel,將其注冊到selector上,等待輪詢信號(hào)。示例代碼如下:
private Selector selector;
private SocketChannel socketChannel;
public void init(){
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configurateBlocking(false);
}
public void run(){
doConnect();//執(zhí)行連接服務(wù)器的操作
while(true){
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
handleInput(key); // 網(wǎng)絡(luò)請(qǐng)求處理器
}
}
}
public void doConnect(){
if(socketChannel.connect(port)){
socketChannel.register(selector, SelectionKey.OP_READ);
}else{
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
可以對(duì)比與服務(wù)端的代碼,不同的地方包括:服務(wù)端會(huì)注冊O(shè)P_ACCEPT事件,用于接受客戶端的連接,客戶端會(huì)注冊O(shè)P_CONNECT事件,用于連接服務(wù)端;此外,他們使用的channel也不一樣,服務(wù)端使用的是ServerSocketChannel,客戶端使用的是SocketChannel。
除了這兩個(gè)特殊事件,他們還都能夠注冊O(shè)P_READ和OP_WRITE事件,用于網(wǎng)絡(luò)數(shù)據(jù)的讀和寫。
我在這里更偏向于網(wǎng)絡(luò)模型的對(duì)比,因此網(wǎng)絡(luò)數(shù)據(jù)的實(shí)際讀寫代碼不在這里編寫,需要注意的是,網(wǎng)絡(luò)數(shù)據(jù)的讀寫需要程序員自己操作buffer對(duì)象,同時(shí)還要面對(duì)“半包讀寫問題”
優(yōu)勢
使用NIO編程的優(yōu)勢主要有:
- 客戶端發(fā)起的連接是異步的,可以通過在多路復(fù)用器注冊O(shè)P_CONNECT等待后續(xù)結(jié)果,不需要像之前的客戶端那樣被同步阻塞。
- SocketChannel的讀寫操作都是異步的,沒有可讀寫的數(shù)據(jù)時(shí),它不會(huì)同步等待,I/O通信線程就可以處理其他的連接。
- 線程模型得到優(yōu)化,一個(gè)seletor線程可以同時(shí)處理成千上萬條連接。
AIO編程
在JDK1.7以后升級(jí)了NIO類庫,被稱為NIO2.0。它提供了與UNIX網(wǎng)絡(luò)編程事件驅(qū)動(dòng)I/O相對(duì)應(yīng)的AIO。AIO不需要通過多路復(fù)用器(selector)對(duì)注冊的通道進(jìn)行輪詢操作即可實(shí)現(xiàn)異步讀寫,簡化了NIO的編程模型。事實(shí)上,它傳遞的是一個(gè)信號(hào)變量。
AIO編程的示例代碼我這里就不再列舉,接下來主要看一下,四種方式的對(duì)比。
模型對(duì)比
| 同步阻塞I/O(BIO) | 偽異步I/O | 非阻塞I/O(NIO) | 異步I/O(AIO) | |
|---|---|---|---|---|
| 客戶端個(gè)數(shù) | 1:1 | M:N | M:1 | M:0 |
| I/O類型 | 阻塞I/O | 阻塞I/O | 非阻塞I/O | 非阻塞I/O |
| I/O類型 | 同步 | 同步 | 異步 | 異步 |
| API使用難度 | 簡單 | 簡單 | 非常復(fù)雜 | 復(fù)雜 |
| 調(diào)試難度 | 簡單 | 簡單 | 復(fù)雜 | 復(fù)雜 |
| 可靠性 | 非常差 | 差 | 高 | 高 |
| 吞吐量 | 低 | 中 | 高 | 高 |
雖然我們本系列主要是學(xué)習(xí)NIO框架Netty,但是并非意味著所有的Java網(wǎng)絡(luò)編程都得用NIO和Netty。通過對(duì)比我們可以看出,BIO、偽異步I/O也有自己的優(yōu)勢:簡單,因此根據(jù)業(yè)務(wù)應(yīng)用場景,如果客戶端并發(fā)數(shù)不多,服務(wù)器負(fù)載也低,那就完全可以考慮直接使用較為低級(jí)的網(wǎng)絡(luò)編程模型。
下一篇開始,我們真正開始學(xué)習(xí)netty。