前言
前幾天初步接觸瞭解了NIO,發現效能方面完爆BIO。因此決定將之前一個專案的服務端改造成NIO。但是NIO學習難度比BIO大,因此在網上查詢相關資料,然後將我自己理解並簡化的NIO伺服器搭建思路在這裡做個記錄分享。因此會有一些錯誤和忽略的地方,若要更加詳細請點選下方的原文。
思路來源:《Java NIO文件》非阻塞式伺服器,原文:Java NIO Tutorial.
NIO相關的這裡就不做相關介紹,畢竟不能算真正的教程。
大致思路如下:
整個系統主要為兩個執行緒:AccepterThread,ProcessorThread。分別用於接收成功連線的Socket物件以及對訊息的接收處理。
兩個執行緒之間使用佇列通訊,用於傳遞Socket物件
在訊息的處理中又對Channel中的操作分為讀操作和寫操作,這兩個操作分別建立兩個不同的類用來執行,原作者對此的解釋是為了防止讀取資料的時候資料缺失以及寫資料的時候未完全寫入。
讀寫操作分別又兩個對應的Selector對其進行監聽。
請求的接收
對請求的接收就和一般的ServerSocket一樣,while迴圈直到有請求進來,這裡原文將準備好的SocketChannel包裝進另外編寫的Socket類中,這個Socket類相關定義如下:
public class Socket {
public long socketId;
public SocketChannel socketChannel;
public MessageReader reader;
public MessageWriter writer;
public Socket(SocketChannel channel) {
this.socketChannel = channel;
}
public int read(Bytebuffer buffer) {
//讀方法實現
}
public int write(Bytebuffer buffer) {
//寫方法實現
}
}
複製程式碼
這裡將最重要的讀寫操作進行包裝,並且使用自己的訊息讀/寫器來對Channel中的位元組進行讀寫操作。
Accepter在包裝好Socket物件後將該物件add進佇列中。
請求的處理
在Processor中,存在一個while(true)迴圈,該迴圈遍歷連線AccepterThread的佇列,一旦佇列中有物件,則進行資料的處理操作。
Processor中存在兩個Selector,一個負責讀,另一個負責寫。
包裝並註冊
在Accepter中包裝的Socket物件只有SocketChannel,因此在進行資訊的讀寫之前要將對應的讀寫器以及SocketId裝入Socket物件中。這裡原作者對此的操作要規範複雜的多,翻譯的原文是:
一個Message Reader一定滿足特定的協議。Message Reader需要知道它嘗試讀取的訊息的訊息格式。如果我們的伺服器可以通過協議來複用,那它需要有能夠插入Message Reader實現的功能 – 可能通過接收一個Message Reader工廠作為配置引數。
- 但是我這個系統不需要如此規範,因此只要能夠對訊息進行讀寫以及確定訊息完全傳送完畢即可。
在這裡進行包裝的好處是能給每個SocketChannel都有讀/寫器,甚至可以讓每個SocketChannel的讀寫器不同。
再將資訊包裝好後將Socket中的SocketChannel註冊進讀Selector中,同時將Socket物件作為附件裝入。
等待訊息。
讀取和處理
當訊息到達後觸發Selector,將讀就緒的SocketChannel獲取出來。其中的附件是包裝了的Socket物件,裡面有這個SocketChannel對應的讀寫器。將資訊完整讀取後就是對資訊的處理了,在處理這裡採用的是函式處理。即在程式的入口處就將處理程式設定完畢,然後將對應的函式傳遞給Processor,這樣當我們需要對處理處理進行修改的時候不需要進入程式的主體,而只要在入口處修改即可。同時讀寫之間也是採用佇列進行通訊。資訊處理完成後如果需要返回相關細心或者要將資訊傳送給其他SocketChannel,就可以將該操作offer進寫佇列當中。
寫入
對資訊處理完成如果要進行寫操作,就將SocketChannel註冊進寫Selector中。這裡一開始覺得為什麼不直接從佇列中獲取到物件就就進行寫操作,仔細考慮後發現無法保證這個SocketChannel在執行寫的時候Client是沒有資料傳送過來的,因此要將該SocketChannel註冊進寫Selector中保證在寫就緒狀態進行資料寫入。 同時在寫Selector中也不能保證所有都是對於流程來說可以寫就緒的,畢竟空閒的時候就處於寫就緒狀態,但是此時伺服器根本不會主動傳送資訊給客戶端,因此要檢測所有在寫Selector中註冊的SocketChannel中的Message是否寫入完畢,如果寫入完畢則將其移除Selector如果沒有則寫入。
不同Channel間通訊
只要在獲取到包裝好的Socket物件後將其儲存在Hash表中則可以隨時根據Socket的Id獲取到對應的SocketChannel然後根據將要進行的操作將該Socket新增到對應的佇列中等待註冊。