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

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

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

上圖描述了NIO編程過程中的通信時序圖,異步的網(wǎng)絡請求代碼更不易編寫,下面看看服務端的簡單示例代碼:
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)絡請求處理器
}
}
}
簡單的看這段代碼,在初始化時需要執(zhí)行的操作包括:
- open一個selector和ServerSocketChannel
- 設置非阻塞模式
- 綁定端口號
- 將channel注冊到selector上,接受accept事件
接著主循環(huán)要做的事情包括:
- 輪詢select
- 從selector中獲取觸發(fā)了信號的SelectionKey
- 將SelectionKey交給網(wǎng)絡請求處理器進行處理(處理器要完成的事情包括接受連接、接收數(shù)據(jù),解碼數(shù)據(jù),寫回數(shù)據(jù)等)
客戶端時序圖

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