定義一個API,用以在網頁瀏覽器和服務器之間創建socket鏈接,這個鏈接是持久的,兩邊能夠在任意時間開始發送數據web
優勢算法
支持雙向通訊,實時性強跨域
更好的二進制支持瀏覽器
沒有同源限制,客戶端能夠與任意服務器通訊bash
較少的控制開銷(建立後,ws客戶端\服務端進行數據交換時,協議控制的數據包頭部較小)服務器
let socket = new WebSocket('ws://localhost:9999');
socket.onopen = () => { // 鏈接成功後的回調
socket.send('hello')
}
socket.onmessage = (event) => { };// 接收到服務器數據時的回調
socket.onclose = function(event) { };// 鏈接關閉時的回調
socket.onerror = function(event) { };// 報錯時的回調
複製代碼
let webSocketServer = require('ws').Server;
let server = new webSocketServer({port: 8888}); // 支持跨域 端口號不能衝突
server.on('connection', (socket) => { // 鏈接成功回調
socket.on('message', (msg) => { // 監聽客戶端發送的消息
socket.send(msg); // 向客戶端返回消息
});
});
複製代碼
客戶端經過HTTP請求與WebSocket服務端協商升級協議。協議升級完成後,後續的數據交換則遵守WebSocket的協議。websocket
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade 表示要升級協議
Upgrade: websocket 要升級的協議
Sec-WebSocket-Version: 13 協議版本
Sec-WebSocket-Key: IHfMdf8a0aQXbwQO1pkGdA== 與服務端響應首部的Sec-WebSocket-Accept是配套的,提供基本的防禦,確保服務端理解websocket鏈接, 避免惡意\無心等非法鏈接。
複製代碼
HTTP/1.1 101 Switching Protocols // 101標識協議轉換
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: aWAY+V/uyz5ILZEoWuWdxjnlb7E=
到此完成協議升級,後續的數據交互都按照新的協議來。
複製代碼
Sec-WebSocket-Accept計算公式網絡
const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; // 常量
const webSocketKey = 'IHfMdf8a0aQXbwQO1pkGdA==';
let websocketAccept = require('crypto').createHash('sha1').update(webSocketKey + CODE).digest('base64'); // crypto提供通用的加密和哈希算法
複製代碼
客戶端和服務器端通訊的最小單位是幀, 由1或多個幀組成一條完整的消息 客戶端: 將消息切割爲多個幀發送服務器端 服務器端:接收消息幀, 並將關聯的幀從新組裝成完整的消息socket
單位是比特
0 1 2 3
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 ... |
複製代碼
FIN:1個比特 表示是不是消息(message)的最後一個分片(fragment),1表明是,0表明不是tcp
RSV1, RSV2, RSV3:各佔1個比特。通常狀況下全爲0。只有當客戶端、服務端協商採用WebSocket擴展時,這三個標誌位能夠非0,且值的含義由擴展進行定義,不然鏈接出錯。
Opcode: 4個比特。操做代碼,即如何解析後續的數據載荷(data payload)。若是操做代碼是不認識的,那麼接收端應該斷開鏈接(fail the connection)
%x0:延續幀。表示本次數據傳輸採用了數據分片,當前收到的數據幀爲其中一個數據分片。
%x1:文本幀(frame)
%x2:二進制幀(frame)
%x3-7:保留的操做代碼,用於後續定義的非控制幀。
%x8:鏈接斷開。
%x9:一個ping操做。
%xA:一個pong操做。
%xB-F:保留的操做代碼,用於後續定義的控制幀。
Mask: 1個比特。表示是否要對數據載荷進行掩碼操做
從客戶端向服務端發送數據時,Mask都是1,即須要對數據進行掩碼操做;從服務端向客戶端發送數據時,不須要對數據進行掩碼操做
若是服務端接收到的數據沒有進行過掩碼操做,服務端須要斷開鏈接。
若是Mask是1,那麼在Masking-key中會定義一個掩碼鍵(masking-key),並用這個掩碼鍵來對數據載荷進行反掩碼。
Payload length:數據載荷的長度,單位是字節。爲7位,或7+16位,或7+64位。
x爲0~125:數據的長度爲x字節。
x爲126:後續2個字節表明一個16位的無符號整數,該無符號整數的值爲數據的長度。
x爲127:後續8個字節表明一個64位的無符號整數(最高位爲0),該無符號整數的值爲數據的長度。
若是payload length佔用了多個字節的話,payload length的二進制表達採用網絡序(big endian,重要的位在前)
Masking-key:0或4字節(32位),全部從客戶端傳送到服務端的數據幀,數據載荷都進行了掩碼操做,Mask爲1,且攜帶了4字節的Masking-key。若是Mask爲0,則沒有Masking-key。載荷數據的長度,不包括mask key的長度
Payload data:(x+y) 字節 載荷數據:包括了擴展數據、應用數據。其中,擴展數據x字節,應用數據y字節。
擴展數據:若是沒有協商使用擴展的話,擴展數據數據爲0字節。全部的擴展都必須聲明擴展數據的長度,或者能夠如何計算出擴展數據的長度。此外,擴展如何使用必須在握手階段就協商好。若是擴展數據存在,那麼載荷數據長度必須將擴展數據的長度包含在內。
應用數據:任意的應用數據,在擴展數據以後(若是存在擴展數據),佔據了數據幀剩餘的位置。載荷數據長度 減去 擴展數據長度,就獲得應用數據的長度。
let net = require('net'); // 實現tcp協議
const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const crypto = require('crypto');
let server = net.createServer((socket) => {
socket.once('data', (data) => { // once只會執行一次回調
data = data.toString(); // data爲請求流
if (data.match(/Connection: Upgrade/)) { // 說明須要請求升級協議
let rows = data.split('\r\n'); //按分割符分開
rows = rows.slice(1, -2); //去掉請求行和尾部的二個分隔符
// 獲取請求頭
let headers = {};
rows.reduce((memo, item) => {
let [key, value] = item.split(': ');
memo[key] = value;
return memo;
}, headers);
// console.log(headers, 'headers');
if(headers['Sec-WebSocket-Version'] === '13'){ // 須要升級爲13版本
let SecWebSocketKey = headers['Sec-WebSocket-Key'];
let SecWebSocketAccept = crypto.createHash('sha1').update(SecWebSocketKey + CODE).digest('base64');
let response = [
'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${SecWebSocketAccept}`,
'\r\n', // 響應頭和響應體之間有兩個\r\n
].join('\r\n');
socket.write(response); // 返回響應頭給客戶端 代表握手成功
// 後面格式基於websocket協議
socket.on('data', (buffers) => { // data默認爲buffer類型
let fin = buffers[0]&0b10000000 == 0b10000000; // 獲取第一個字節的第一位 即結束位的值
let opcode = buffers[0]&0b00001111; // 獲取第一個字節的後四位 即操做碼
let ismask = buffers[1]&0b10000000; // 是否進行掩碼
let payloadLength = buffers[1]&0b01111111;
let payload;
if (payloadLength<=125) {
if (ismask) {
let mask = buffers.slice(2,6); // 掩碼鍵
payload = buffers.slice(6); // 攜帶的真實數據
// console.log(payload, 'before unmask');
unmask(payload, mask); // 對數據進行反掩碼
// console.log(payload.toString(), 'unmask');
} else {
payload = buffers.slice(2);
}
} else if (payloadLength<=126) {
// ....
}
// 拼接響應幀
let res = Buffer.alloc(2+payload.length);
res[0] = 0b10000000|opcode;
res[1] = payloadLength;
payload.copy(res, 2);
socket.write(res);
});
}
}
});
});
function unmask(payload,mask){ // mask爲4個字節長度
const length = payload.length;
for (let i=0;i<length;i++) {
payload[i]^=mask[i&3]; // i&3等價於1%4
}
}
server.listen(9999);
複製代碼