第一篇 Java網(wǎng)絡(luò)編程

本篇主要從學(xué)習(xí)角度整理java的幾個(gè)網(wǎng)絡(luò)模型,包括:

  1. BIO通信模型
  2. 偽異步通信模型
  3. NIO通信模型
  4. NIO2.0(AIO)

BIO通信模型

BIO同步阻塞I/O通信模型

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通信模型

偽異步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服務(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í)行的操作包括:

  1. open一個(gè)selector和ServerSocketChannel
  2. 設(shè)置非阻塞模式
  3. 綁定端口號(hào)
  4. 將channel注冊到selector上,接受accept事件

接著主循環(huán)要做的事情包括:

  1. 輪詢select
  2. 從selector中獲取觸發(fā)了信號(hào)的SelectionKey
  3. 將SelectionKey交給網(wǎng)絡(luò)請(qǐng)求處理器進(jìn)行處理(處理器要完成的事情包括接受連接、接收數(shù)據(jù),解碼數(shù)據(jù),寫回?cái)?shù)據(jù)等)

客戶端時(shí)序圖

NIO客戶端時(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)勢主要有:

  1. 客戶端發(fā)起的連接是異步的,可以通過在多路復(fù)用器注冊O(shè)P_CONNECT等待后續(xù)結(jié)果,不需要像之前的客戶端那樣被同步阻塞。
  2. SocketChannel的讀寫操作都是異步的,沒有可讀寫的數(shù)據(jù)時(shí),它不會(huì)同步等待,I/O通信線程就可以處理其他的連接。
  3. 線程模型得到優(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。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容