知道了怎麼握手只是讓客戶端和服務器創建鏈接而已,WebSocket真正麻煩的地方是在數據的傳輸上!爲了環保,它使用了特定格式的數據幀,這個數據幀須要本身去解析(固然也有別人編寫好的庫能夠用)。雖然官方文檔描述的很詳細,可是看起來仍是蛋疼。
當客戶端向服務器發送一個數據時服務器收到一個數據幀,好比下面的程序 //客戶端程序
var ws=new WebSocket("ws://127.0.0.1:8000");
ws.onopen=function(e){
ws.send("次碳酸鈷"); //發送數據
};//服務器程序
這裏是直接把接收到的數據輸出了,獲得這樣一個東西
var crypto=require('crypto');
var WS='258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
var key;
o.on('data',function(e){
if(!key){ //握手
key=e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
key=crypto.createHash('sha1').update(key+WS).digest('base64');
o.write('HTTP/1.1 101 Switching Protocols\r\n');
o.write('Upgrade: websocket\r\n');
o.write('Connection: Upgrade\r\n');
o.write('Sec-WebSocket-Accept: '+key+'\r\n');
o.write('\r\n');
}else onmessage(e); //接收並交給處理函數
});
}).listen(8000);
function onmessage(e){
console.log(e); //把數據輸出到控制檯
};
這就是一個完整的數據幀,直接的16進制數據咱們固然沒法直接閱讀,須要按照數據幀的格式把它裏面的數據取出來才行。對於這個數據幀,官方文檔提供了一個結構圖 0 1 2 3
光拿出這個實在很難看懂,頂部數字用十進制而不是八進制太讓人蛋疼了。固然官方文檔在後面的描述中也有詳細介紹,看完後再回頭來看圖表才能看明白。其實WebSocket目前還不太完善,不少實驗性的東西,因此徹底按照官方文檔來理解是蛋疼的。這裏就說我本身的理解。
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
如今再看左上角上面的圖標,左上角的四個小列,也就是4位,第一位是FIN,後面三位是RSV1到3。官方文檔上說RSV是預留的空間,正常爲0,這就意味着,正常狀況下他們能夠當作0填充,那麼前4位只有第一位的FIN須要設置,FIN表示幀結束,因爲這篇中它不重要就不特別介紹了。接着後面的四位是儲存opcode的值,這個opcode是標識數據類型的。這樣數據的第一個字節咱們就能理解它的含義了,看上面16進制的數據的第一個字節81換成二進制是1000001,第一個1是FIN的值,最後一個1是opcode的值。
接着是第二個字節的數據,它由1位的MASK和7位的PayloadLen組成,MASK標識這個數據幀的數據是否使用掩碼,PayloadLen表示數據部分的長度。可是PayloadLen只有7位,換成無符號整型的話只有0到127的取值,這麼小的數值固然沒法描述較大的數據,所以規定當數據長度小於或等於125時候它才做爲數據長度的描述,若是這個值爲126,則時候後面的兩個字節來儲存儲存數據長度,若是爲127則用後面四個字節來儲存數據長度。因此上面的圖片第一行的最右側那塊和第二行看起來有些頹然。從咱們的示例數據來看,第二個字節的8C中80是最高位爲1,這意味着MASK爲1,後面的C表示這個數據部分有12個字節。
再接着是上面圖表中的MaskingKey,它佔四個字節,儲存掩碼的實體部分。可是隻有在前面的MASK被設置爲1時候才存在這個數據,不然不使用掩碼也就沒有這個數據了。看咱們的示例數據,因爲前面的MASK爲1,因此3到6字節的「79 77 3d 41」是數據的掩碼實體。
最後是數據部分,若是掩碼存在,那麼全部數據都須要與掩碼作一次異或運算,四個字節的掩碼與全部數據字節輪流發生性關係。若是不存在掩碼,那麼後面的數據就能夠直接使用。
這樣數據幀就解析完了。下面是我寫的數據幀解析的程序,請不要吐槽代碼沒優化 function decodeDataFrame(e){
既然有了解析程序,那麼咱們就能夠把上面實例服務器端的onmessage方法修改一下
var i=0,j,s,frame={
//解析前兩個字節的基本數據
FIN:e[i]>>7,Opcode:e[i++]&15,Mask:e[i]>>7,
PayloadLength:e[i++]&0x7F
};
//處理特殊長度126和127
if(frame.PayloadLength==126)
frame.length=(e[i++]<<8)+e[i++];
if(frame.PayloadLength==127)
frame.length=(e[i++]<<24)+(e[i++]<<16)+(e[i++]<<8)+e[i++];
//判斷是否使用掩碼
if(frame.Mask){
//獲取掩碼實體
frame.MaskingKey=[e[i++],e[i++],e[i++],e[i++]];
//對數據和掩碼作異或運算
for(j=0,s=[];j<frame.PayloadLength;j++)
s.push(e[i+j]^frame.MaskingKey[j%4]);
}else s=e.slice(i,frame.PayloadLength); //不然直接使用數據
//數組轉換成緩衝區來使用
s=new Buffer(s);
//若是有必要則把緩衝區轉換成字符串來使用
if(frame.Opcode==1)s=s.toString();
//設置上數據部分
frame.PayloadData=s;
//返回數據幀
return frame;
};function onmessage(e){
e=decodeDataFrame(e); //解析數據幀
console.log(e); //把數據幀輸出到控制檯
};
這樣服務器接收客戶端穿過了的數據就沒問題了。嘛,這篇文章就只說接收,至於從服務器發送到客戶的狀況會有更復雜的狀況出現,咱下一篇再說。web