Netty框架中的ByteBuf對java.nio.ByteBuffer的微創新(2)

常規的網絡通信程序引用的是TCP/IP協議的傳輸層接口,對於面向連接的TCP協議,數據的傳輸形式是流(Stream),其特點是傳輸的數據無消息邊界。套接字就像管道的兩端,數據從一端流入,從另一端流出,數據的流動是連續的。

TCP 協議提供了發送和接收數據的接口,發送操作每次發送一串字節,接收操作每次接收一串字節,TCP協議保證發送數據的順序和接收數據的順序相同,比如發送數據爲012345678910個字節),只要套接字不出現錯誤,另一端接收到的數據一定是012345678910個字節)。但接收數據的操作卻不一定要和發送數據的操作一一對應。這主要是因爲 TCP 協議是字節流形式的、無消息邊界的協議,受網絡傳輸中的不確定因素的影響,不能保證每次sendrecv操作能夠一一對應。上述情況可以形象表示爲:

情況1

發送:

012

3456789


接收:

012

3456789

情況2

發送:

012

3456789


接收:

0123456789

情況3

發送:

012

3456789


接收:

01

234

56789

......

情況N

發送:

012

3456789


接收:

......

有些時候傳輸的數據具有一定的協議語義,爲了保證接收方不出現解析錯誤, 編程時必須要考慮消息邊界問題, 否則就可能會出現命令格式解析錯誤、丟失命令等後果。實際應用中,解決 TCP 協議消息邊界問題的方式有三種:

第一種方式是發送固定長度的消息。該方法類似於CPURISC架構,每條指令的長度相同,這種處理方式最簡單,但需要仔細設計指令體系。

第二種方式是各種指令具有不同的頭標識,具體到每種指令的長度則固定,收到指令後根據頭部標識判斷指令的總長度。該方法類似於CPUCISC架構,各種指令的長度不同,但每種指令的長度是已知固定的。

第三種方式是將消息長度在頭部進行標識,相當於對第二種方式的優化,將整條指令劃分爲頭部和數據部分,頭部長度固定,包含指令類型和數據部分長度,解析後一次讀取數據部分長度。這種方式降低了指令解析的複雜度。

以第三種方式爲例,分別使用java.nio.ByteBufferio.netty.buffer.ByteBuf演示指令解析的過程。

使用java.nio.ByteBuffer演示網絡流的讀取、解析過程:

// 讀取數據.
int cnt = channel.read(src);
if(cnt < 0) {
// ...
} elseif (cnt == 0) {
// ...
}
// 解析指令循環,一次讀取可能會讀到多條指令
ByteBuffer buf = src.duplicate();
buf.flip();
while (buf.remaining() >= PDU_HEAD_LEN) {
int pduLen = buf.getInt(buf.position());
if (buf.remaining() >= pduLen) {
// 循環處理指令的各個字段
// ...
}
// buf中剩下的數據爲下一個指令的數據,下一次循環將繼續解析,直到數據不足一條指令
}
// buf中剩下的數據不足一條指令,等待下次讀取數據後再次處理
// 將數據轉移到src的頭部,供讀取添加,每次都需執行
bufTmp.clear();
bufTmp.put(buf);
bufTmp.flip();
src.clear();
src.put(bufTmp);

使用io.netty.buffer.ByteBuf演示網絡流的讀取、解析過程:

// 讀取數據.
src.clear();
int cnt = channel.read(src);
if(cnt < 0) {
// ...
} elseif (cnt == 0) {
// ...
}
src.flip();
buf.writeBytes(src);
// 解析指令循環,一次讀取可能會讀到多條指令
while (buf.readableBytes() >= PDU_HEAD_LEN) {
int pduLen = buf.getInt(buf.readerIndex());
if (buf.readableBytes() >= pduLen) {
// 循環處理指令的各個字段
// ...
}
// buf中剩下的數據爲下一個指令的數據,下一次循環將繼續解析,直到數據不足一條指令
}
// buf中剩下的數據不足一條指令,等待下次讀取數據後再次處理
// 根據必要進行buf的壓縮,根據系統內存進行權衡,可以每500次執行一次壓縮
if (buf.writerIndex() > ALLOW_CAP) {
buf.writeBytes(buf);
}

結論:從上述代碼看,使用io.netty.buffer.ByteBuf進行網絡流的讀取、解析,不僅代碼簡潔、易懂而且高效。最好的代碼不是使用了豐富的註釋,而是不需過多註釋就通俗易懂。這其中的一個重要原因在於io.netty.buffer.ByteBufreaderIndexwriterIndex分開標記,並使用了準確的接口命名帶來的。而java.nio.ByteBuffer的接口設計中,getput則使用了同一個位置標識position,用limit標識有效數據的末尾,如果是交替讀寫,其代碼反覆調用duplicateflip將讓人感到莫名其妙,或者使用getXXX(index)或者put(index, value)就更讓人摸不着頭腦。另外putget的命名也讓人奇怪,爲什麼不使用writeread更直接的單詞命名呢?同樣是開源工程,顯然netty這一小步,給網絡編程的進化帶來了一大步。Netty帶來的優勢遠不至於此,後續將花時間逐步介紹。在此祝願會有更多更好的像netty這樣的開源工程涌現出來!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章