轉(zhuǎn)自:https://blog.csdn.net/mrliuzhao/article/details/89453082#_2
簡介
在Java的Socket編程中娃善,若使用阻塞式(BIO)鲁沥,則往往通過ServerSocket的accept()方法獲取到客戶端Socket之后务漩,再使用客戶端Socket的InputStream和OutputStream進行讀寫够话。Socket.getInputstream.read(byte[] b)和Socket.getOutputStream.write(byte[] b)的方法中的參數(shù)都是字節(jié)數(shù)組箭养。這種阻塞式的Socket編程顯然已經(jīng)遠(yuǎn)遠(yuǎn)不能滿足目前的并發(fā)式訪問需求蒿褂。
所以最近在項目中學(xué)習(xí)使用了Java原生NIO潘拨,這時則需要通過ServerSocketChannel的accept()方法獲取到客戶端的SocketChannel绩衷,再使用客戶端SocketChannel直接進行讀寫蹦魔。但SocketChannel.read(ByteBuffer dst)和SocketChannel.write(ByteBuffer src)的方法中的參數(shù)則都變?yōu)榱薺ava.nio.ByteBuffer,該類型就是JavaNIO對byte數(shù)組的一種封裝咳燕,其中包括了很多基本的操作勿决,在此記錄一下備忘。
ByteBuffer包含幾個基本的屬性:
position:當(dāng)前的下標(biāo)位置招盲,表示進行下一個讀寫操作時的起始位置低缩;
limit:結(jié)束標(biāo)記下標(biāo),表示進行下一個讀寫操作時的(最大)結(jié)束位置宪肖;
capacity:該ByteBuffer容量表制;
mark: 自定義的標(biāo)記位置;
無論如何控乾,這4個屬性總會滿足如下關(guān)系:mark <= position <= limit <= capacity么介。目前對mark屬性了解的不多,故在此暫不做討論蜕衡。其余3個屬性可以分別通過ByteBuffer.position()壤短、ByteBuffer.limit()、ByteBuffer.capacity()獲瓤隆久脯;其中position和limit屬性也可以分別通過ByteBuffer.position(int newPos)、ByteBuffer.limit(int newLim)進行設(shè)置镰吆,但由于ByteBuffer在讀取和寫出時是非阻塞的帘撰,讀寫數(shù)據(jù)的字節(jié)數(shù)往往不確定,故通常不會使用這兩個方法直接進行修改万皿。
首先無論讀寫摧找,均需要初始化一個ByteBuffer容器。如上所述牢硅,ByteBuffer其實就是對byte數(shù)組的一種封裝蹬耘,所以可以使用靜態(tài)方法wrap(byte[] data)手動封裝數(shù)組,也可以通過另一個靜態(tài)的allocate(int size)方法初始化指定長度的ByteBuffer减余。初始化后综苔,ByteBuffer的position就是0;其中的數(shù)據(jù)就是初始化為0的字節(jié)數(shù)組;limit = capacity = 字節(jié)數(shù)組的長度如筛;用戶還未自定義標(biāo)記位置堡牡,所以mark = -1,即undefined狀態(tài)妙黍。下圖就表示初始化了一個容量為16個字節(jié)的ByteBuffer悴侵,其中每個字節(jié)用兩位16進制數(shù)表示:
可以手動通過put(byte b)或put(byte[] b)方法向ByteBuffer中添加一個字節(jié)或一個字節(jié)數(shù)組。ByteBuffer也方便地提供了幾種寫入基本類型的put方法:putChar(char val)拭嫁、putShort(short val)、putInt(int val)抓于、putFloat(float val)做粤、putLong(long val)、putDouble(double val)捉撮。執(zhí)行這些寫入方法之后怕品,就會以當(dāng)前的position位置作為起始位置,寫入對應(yīng)長度的數(shù)據(jù)巾遭,并在寫入完畢之后將position向后移動對應(yīng)的長度肉康。下圖就表示了分別向ByteBuffer中寫入1個字節(jié)的byte數(shù)據(jù)和4個字節(jié)的Integer數(shù)據(jù)的結(jié)果:
但是當(dāng)想要寫入的數(shù)據(jù)長度大于ByteBuffer當(dāng)前剩余的長度時,則會拋出BufferOverflowException異常灼舍,剩余長度的定義即為limit與position之間的差值(即 limit - position)吼和。如上述例子中,若再執(zhí)行buffer.put(new byte[12]);就會拋出BufferOverflowException異常骑素,因為剩余長度為11炫乓。可以通過調(diào)用ByteBuffer.remaining();查看該ByteBuffer當(dāng)前的剩余可用長度献丑。
從SocketChannel中讀入數(shù)據(jù)至ByteBuffer
在實際應(yīng)用中末捣,往往是調(diào)用SocketChannel.read(ByteBuffer dst),從SocketChannel中讀入數(shù)據(jù)至指定的ByteBuffer中箩做。由于ByteBuffer常常是非阻塞的,所以該方法的返回值即為實際讀取到的字節(jié)長度。假設(shè)實際讀取到的字節(jié)長度為 n,ByteBuffer剩余可用長度為 r俘陷,則二者的關(guān)系一定滿足:0 <= n <= r。繼續(xù)接上述的例子,假設(shè)調(diào)用read方法夭禽,從SocketChannel中讀入了4個字節(jié)的數(shù)據(jù)缠劝,則buffer的情況如下:
現(xiàn)在ByteBuffer容器中已經(jīng)存有數(shù)據(jù)沃饶,那么現(xiàn)在就要從ByteBuffer中將這些數(shù)據(jù)取出來解析氓鄙。由于position就是下一個讀寫操作的起始位置,故在讀取數(shù)據(jù)后直接寫出數(shù)據(jù)肯定是不正確的噩茄,要先把position復(fù)位到想要讀取的位置。
首先看一個rewind()方法,該方法僅僅是簡單粗暴地將position直接復(fù)原到0,limit不變获黔。這樣進行讀取操作的話蚀苛,就是從第一個字節(jié)開始讀取了。如下圖:
該方法雖然復(fù)位了position玷氏,可以從頭開始讀取數(shù)據(jù)枉阵,但是并未標(biāo)記處有效數(shù)據(jù)的結(jié)束位置。如本例所述预茄,ByteBuffer總?cè)萘繛?6字節(jié),但實際上只讀取了9個字節(jié)的數(shù)據(jù)侦厚,因此最后的7個字節(jié)是無效的數(shù)據(jù)耻陕。故rewind()方法常常用于字節(jié)數(shù)組的完整拷貝。
實際應(yīng)用中更常用的是flip()方法刨沦,該方法不僅將position復(fù)位為0诗宣,同時也將limit的位置放置在了position之前所在的位置上,這樣position和limit之間即為新讀取到的有效數(shù)據(jù)想诅。如下圖:
在將position復(fù)位之后召庞,我們便可以從ByteBuffer中讀取有效數(shù)據(jù)了。類似put()方法来破,ByteBuffer同樣提供了一系列g(shù)et方法篮灼,從position開始讀取數(shù)據(jù)。get()方法讀取1個字節(jié)扎狱,getChar()睬捶、getShort()孕似、getInt()、getFloat()娘荡、getLong()、getDouble()則讀取相應(yīng)字節(jié)數(shù)的數(shù)據(jù)驶沼,并轉(zhuǎn)換成對應(yīng)的數(shù)據(jù)類型炮沐。如getInt()即為讀取4個字節(jié),返回一個Int回怜。在調(diào)用這些方法讀取數(shù)據(jù)之后大年,ByteBuffer還會將position向后移動讀取的長度,以便繼續(xù)調(diào)用get類方法讀取之后的數(shù)據(jù)。
這一系列g(shù)et方法也都有對應(yīng)的接收一個int參數(shù)的重載方法鲜戒,參數(shù)值表示從指定的位置讀取對應(yīng)長度的數(shù)據(jù)专控。如getDouble(2)則表示從下標(biāo)為2的位置開始讀取8個字節(jié)的數(shù)據(jù),轉(zhuǎn)換為double返回遏餐。不過實際應(yīng)用中往往對指定位置的數(shù)據(jù)并不那么確定伦腐,所以帶int參數(shù)的方法也不是很常用。get()方法則有兩個重載方法:
get(byte[] dst, int offset, int length):表示嘗試從 position 開始讀取 length 長度的數(shù)據(jù)拷貝到 dst 目標(biāo)數(shù)組 offset 到 offset + length 位置失都,相當(dāng)于執(zhí)行了
for(inti=off;i<off+len;i++)dst[i]=buffer.get();
1
2
get(byte[] dst):嘗試讀取 dst 目標(biāo)數(shù)組長度的數(shù)據(jù)柏蘑,拷貝至目標(biāo)數(shù)組,相當(dāng)于執(zhí)行了
buffer.get(dst,0,dst.length);
1
此處應(yīng)注意讀取數(shù)據(jù)后粹庞,已讀取的數(shù)據(jù)也不會被清零咳焚。下圖即為從例子中連續(xù)讀取1個字節(jié)的byte和4個字節(jié)的int數(shù)據(jù):
此處同樣要注意,當(dāng)想要讀取的數(shù)據(jù)長度大于ByteBuffer剩余的長度時庞溜,則會拋出 BufferUnderflowException 異常革半。如上例中,若再調(diào)用buffer.getLong()就會拋出 BufferUnderflowException 異常流码,因為 remaining 僅為4又官。
為了防止出現(xiàn)上述的 BufferUnderflowException 異常,最好要在讀取數(shù)據(jù)之前確保 ByteBuffer 中的有效數(shù)據(jù)長度足夠漫试。在此記錄一下我的做法:
private void checkReadLen(
long reqLen,
ByteBuffer buffer,
SocketChannel dataSrc
) throws IOException {
? int readLen;
? if (buffer.remaining() < reqLen) { // 剩余長度不夠六敬,重新讀取
? buffer.compact(); // 準(zhǔn)備繼續(xù)讀取
? ? System.out.println("Buffer remaining is less than" + reqLen + ". Read Again...");
? ? while (true) {
? ? ? readLen = dataSrc.read(buffer);
? ? ? System.out.println("Read Again Length: " + readLen + "; Buffer Position: " + buffer.position());
? ? ? if (buffer.position() >= reqLen) { // 可讀的字節(jié)數(shù)超過要求字節(jié)數(shù)
? ? ? ? break;
? ? ? }
? ? }
? ? buffer.flip();
? ? System.out.println("Read Enough Data. Remaining bytes in buffer: " + buffer.remaining());
? }
}
基本類型的值在內(nèi)存中的存儲形式還有字節(jié)序的問題,這種問題在不同CPU的機器之間進行網(wǎng)絡(luò)通信時尤其應(yīng)該注意驾荣。同時在調(diào)用ByteBuffer的各種get方法獲取對應(yīng)類型的數(shù)值時外构,ByteBuffer也會使用自己的字節(jié)序進行轉(zhuǎn)換。因此若ByteBuffer的字節(jié)序與數(shù)據(jù)的字節(jié)序不一致播掷,就會返回不正確的值审编。如對于int類型的數(shù)值8848,用16進制表示叮趴,大字節(jié)序為:0x 00 00 22 90割笙;小字節(jié)序為:0x 90 22 00 00。若接收到的是小字節(jié)序的數(shù)據(jù)眯亦,但是卻使用大字節(jié)序的方式進行解析伤溉,獲取的就不是8848,而是-1876819968妻率,也就是大字節(jié)序表示的有符號int類型的 0x 90 22 00 00乱顾。
JavaNIO提供了java.nio.ByteOrder枚舉類來表示機器的字節(jié)序,同時提供了靜態(tài)方法ByteOrder.nativeOrder()可以獲取到當(dāng)前機器使用的字節(jié)序宫静,使用ByteBuffer中的order()方法即可獲取該buffer所使用的字節(jié)序走净。同時也可以在該方法中傳遞一個ByteOrder枚舉類型來為ByteBuffer指定相應(yīng)的字節(jié)序券时。如調(diào)用buffer.order(ByteOrder.LITTLE_ENDIAN)則將buffer的字節(jié)序更改為小字節(jié)序。
一開始并不知道還可以這樣操作伏伯,比較愚蠢地手動將讀取到的數(shù)據(jù)進行字節(jié)序的轉(zhuǎn)換橘洞。不過覺得還是可以記下來,也許在別的地方用得到说搅。JDK中的 Integer 和 Long 都提供了一個靜態(tài)方法reverseBytes()來將對應(yīng)的 int 或 long 數(shù)值的字節(jié)序進行翻轉(zhuǎn)炸枣。而若想讀取 float 或 double,也可以先讀取 int 或 long弄唧,然后調(diào)用?Float.intBitsToFloat(int val)?或?Double.longBitsToDouble(long val)?方法將對應(yīng)的 int 值或 long 值進行轉(zhuǎn)換适肠。當(dāng)ByteBuffer中的字節(jié)序與解析的字節(jié)序相反時,可以使用如下方法讀群蛞:
int i = Integer.reverseBytes(buffer.getInt());
float f = Float.intBitsToFloat(Integer.reverseBytes(buffer.getInt()));
long l = Long.reverseBytes(buffer.getLong());
double d = Double.longBitsToDouble(buffer.getLong());
由于ByteBuffer往往是非阻塞式的侯养,故不能確定新的數(shù)據(jù)是否已經(jīng)讀完,但這時候依然可以調(diào)用ByteBuffer的compact()方法切換到讀取模式澄干。該方法就是將 position 到 limit 之間還未讀取的數(shù)據(jù)拷貝到 ByteBuffer 中數(shù)組的最前面逛揩,然后再將 position 移動至這些數(shù)據(jù)之后的一位,將 limit 移動至 capacity麸俘。這樣 position 和 limit 之間就是已經(jīng)讀取過的老的數(shù)據(jù)或初始化的數(shù)據(jù)息尺,就可以放心大膽地繼續(xù)寫入覆蓋了。仍然使用之前的例子疾掰,調(diào)用?compact()?方法后狀態(tài)如下:
總之ByteBuffer的基本用法就是:
初始化(allocate)–> 寫入數(shù)據(jù)(read / put)–> 轉(zhuǎn)換為寫出模式(flip)–> 寫出數(shù)據(jù)(get)–> 轉(zhuǎn)換為寫入模式(compact)–> 寫入數(shù)據(jù)(read / put)…
參考資料
java字節(jié)序、主機字節(jié)序和網(wǎng)絡(luò)字節(jié)序掃盲貼:https://blog.csdn.net/aitangyong/article/details/23204817