websocket進行通訊時,可以選擇采用字符串或者字節流的傳輸模式。但在發送與接收時,需要考慮數據的分包,即分成一個個請求與響應消息。無論是采用哪種傳輸模式,都不免要遇到這個問題。
采用字符串傳輸時,接收端可以將每次接收到的字符串拼接到一起,再檢測是否出現了某一特定子串,比如連續兩個換行,即可將一個長的字符串分隔成一個個的請求或響應消息。這種處理方式比較簡單且有效。但這里,介紹另一種模式,即傳輸字節流。
首先考慮下分包的問題,一般分為消息頭與消息體。出於簡單目的,消息頭里只存放一個消息體的長度,消息體為字節數組。
確定了數據包格式接下來可以寫實現代碼了,首先是連接:
var socket;
var uri = "ws://" + window.location.host + "/push"; // 示例地址
function Connect(uri) {
socket = new WebSocket(uri);
socket.binaryType = "arraybuffer";
socket.onopen = function (e) {
console.log("已連接至服務器");
};
socket.onclose = function (e) {
console.log("鏈接已關閉");
};
socket.onmessage = function (e) {
doReceive(e.data);
};
socket.onerror = function (e) {
console.log("出現錯誤");
};
}
這里將socket變量定義為公共的,因為后續的發送方法會用到這個變量。默認JavaScript里的WebSocket傳輸是采用字符串模式的,采用UTF-8編碼,通過將binaryType屬性設置為arraybuffer來使用字節流傳輸。
當發生onmessage事件時代表接收到數據,保存在參數e.data里。每一次接收都可能接收到一個完整的消息或部分消息,我們通過一個doReceive方法來進行消息數據包的拆分。代碼如下:
var receive = [];
var length = 0;
function doReceive(buffer) {
receive = receive.concat(Array.from(new Uint8Array(buffer)));
if (receive.length < 4) {
return;
}
length = new DataView(new Uint8Array(receive).buffer).getUint32(0);
if (receive.length < length + 4) {
return;
}
var bytes = receive.slice(4, length + 4);
doSomething(bytes);
receive = receive.slice(length + 4);
};
其中receive作為接收緩沖區,每次接收到數據時先將其存到該緩沖區里。之后檢查其長度是否大於等於4字節,即上文定義的消息頭長度。若滿足條件,則將其作為Uint32值讀取,代表消息體長度。之后檢查緩沖區是否大於消息頭加消息體長度,若滿足則讀取消息體,之后得到的bytes字節數組即為完整的消息體。最后,用剩余字節重置緩沖區以備下一次讀取。
其中buffer參數為ArrayBuffer類型,其代表原始的字節數組,本身是無意義的。JavaScript通過一個個視圖來解釋這些字節。Uint8Array即是其中一種視圖,它將ArrayBuffer中的字節作為8位無符號整數來對待,正好一字節對應一個uint8整數。類似的還有Uint16Array,它將ArrayBuffer中的字節作為16位無符號整數來對待,則每兩位對應一個uint16整數。
而DataView是JavaScript API提供的一種視圖,他將ArrayBuffer中的數據作為網絡流對待,它采用大端編碼,可以用它來讀取或寫入數據。這里我們用它讀取了一個Uint32的值。
接下來是發送方法,代碼如下:
function doSend(bytes) {
var buffer = new ArrayBuffer(bytes.length + 4);
var view = new DataView(buffer);
view.setUint32(0, bytes.length);
for (var i = 0; i < bytes.length; i++) {
view.setUint8(i + 4, bytes[i]);
}
socket.send(view);
};
其中參數bytes是已經編碼過的字節數組,這里通過DataView視圖將其存儲到ArrayBuffer對象里,以備發送。按照上文約定,需先設置Uint32型的消息體長度,再設置消息體。
最后,本文按約定的消息格式來進行請求與響應消息的傳輸,消息體為不定長度的字節序列。其本身是無意義的。我們可以通過API提供的Uint16Array、Uint32Array等視圖將其作為整數值序列,也可以自我實現其內容的解釋方式。
比如參考這里將其作為采用UTF-8編碼的字符串。之后可再將字符串打印或反序列化為JSON對象等。