WebSocket 定義在 RFC6455 文件當中, 有兩個重要的章節:
1. 交握協定: https://tools.ietf.org/html/rfc6455#section-1.3
WebSocket 使用的 tcp port 跟 http 相同, 當使用者用 http GET 方法時, 其中攜帶的標頭(header)就包含升級(Upgrade)至 websocket 的動作, 標籤(Sec-WebSocket-Key)後面附帶一串 base64 明文,每次開啟交握協定時都會不同,以一組 base64 明碼為例:
dGhlIHNhbXBsZSBub25jZQ==
如果將它解開,其實裏面是一組 16 位元組的密碼.假設客戶端所發送封包內容是:
GET /xxx HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
http 伺服器端收到封包偵測到 Sec-WebSocket-Key, 就將明碼串接一個共通識別碼(GUID)形成長字串:
dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
用雜湊函數 sha1 計算並轉成 base64 明文簡化成:
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
伺服器端產生一個封包把它放在 Sec-WebSocket-Accept: 標籤的後面, 回應用戶端切換通信協定,完成交握程序
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
2. 封包格式: https://tools.ietf.org/html/rfc6455#section-5.1
一旦交握完成, 雙方封包內就不再用 http 的標頭(header), 而是轉成 WebSocket 格式的標頭, WebSocket 客戶端送的資料必須使用 xor 加密, 所附帶 4 位元組(byte)遮蔽碼負責把資料每 4 個位元組循環作加密運算(xor)轉成亂碼,伺服器端則利用這 4 個遮蔽碼同樣每 4 個位元組循環作解密運算(xor)轉成明文.
標頭第 1 個位元組分上半部與下半部:
上半部最高位元(MSB) 標示該封包是否是最後一筆封包, 後 3 個位元保留未使用
下半部 4 個位元代表該封包的格式(opcode),分別如下:
0000 表示是上一封包的延續
0001 表示文字格式(ascII code)
0010 表示16進制(binary code)
1000 表示關閉 Websocket 通道
1001 表示 ping 封包
1010 表示 pong 封包
其他保留未使用
用 c++ 語言描述:
bool fin = p[0] & 0x80;
auto opcode = p[0] & 0xf ;
if( fin ) printf("final frame\n");
switch(opcode){
case 0 : printf("continue frame\n") ;break;
case 1 : printf("text frame\n") ;break;
case 2 : printf("binary frame\n") ;break;
case 8 : printf("close frame\n") ;throw "WebSocket: Connection close!";
case 9 : printf("ping frame\n") ;break;
case 10: printf("pong frame\n") ;break;
default: printf("reserved frame\n") ;break;
}
標頭第 2 個位元組指示隨後的資料, 最高位元(MSB)代表是否資料經過 xor 運算, 剩下7個位元是資料長度數值, 當數值小於 126, 就當成是資料長度(payload length), 如果等於 126, 那要用隨後的另兩個位元組當資料長度, 但如果大於 126(也就是等於 127)時, 就用隨後 8 個位元組當資料長度.客戶端送來的資料必須用 xor 運算過, 因此資料長度後面,必定先緊隨 4 個位元組的遮蔽碼(mask key), 其後才是資料(payload). 而伺服器傳給客戶端並不需要加密, 也就無需遮蔽碼, 因此長度後面緊跟的就是資料(payload)了, 簡單用 c++ 語言來解碼:
if (p[1] & 0x80) { // maskbit should be true
p[1] &= 0x7f; // data in buffer will be de-mask
auto wslen = p[1];// length value copy
p += 2; // don't need the first 2 bytes in header
long long payloadlen;// 8 bytes integer to get payload length
if (wslen < 126 ) payloadlen = wslen;
else { // 126: following 2bytes, 127: following 8 bytes
payloadlen = *p ++;
payloadlen = (payloadlen << 8) + *p ++;
if (wslen>126) for (auto i=0; i < 6; i++) payloadlen = (payloadlen << 8) + *p ++;
}
unsigned char maskkey[4]; // to store maskkey
for (auto i=0; i < 4; i++ ) maskkey = p[i]; // maskkey copy
for (auto i=0; i < payloadlen; i++) p[i] = p[i+4] ^ maskkey[i%4]; // decode & move
size -= 4; // size of buffer was reduced by 4 bytes of maskkey
auto &payload = p; // payload now point to data
}
3. 客戶端可以從 http 伺服器端接收一個 index.html 檔案, 編寫的內容像是:
<html><head><title>WebSocket 發送與接收示範</title><meta charset="UTF-8"></head>
<script>
const ws = new WebSocket('ws://127.0.0.1:5000');// port 必須與 http 相同
ws.addEventListener('open', () => {
msg = "Hello, thhis is a test message.";
console.log("Send: " + msg);
ws.send(msg);
});
ws.addEventListener('message', event => {
console.log("Get:: " + event.data);
ws.close();
});
</script>
<body>打開瀏覽器的 Console 視窗觀看結果</body>
</html>
內嵌的 Javascript 是一個用來開啟 WebSocket 通道的程式碼, 假設 http 伺服器使用 tcp port = 5000, 將上述的 index.html 放在伺服器的 www 根目錄底下, 用瀏覽器開啟 URL 的頁面 http://127.0.0.1:5000/index.html
沒有留言:
張貼留言