Websocket 是HTML5中的一種新的Web通信技術(shù)商架,它實(shí)現(xiàn)了瀏覽器與服務(wù)器之間的雙向通信(full-duplex).
背景
在Websocket之前浓利,實(shí)現(xiàn)雙向通信的技術(shù)有輪詢, Comet
技術(shù) | |
---|---|
輪詢 | 客戶端定時(shí)向服務(wù)器發(fā)送Ajax請(qǐng)求秫舌,服務(wù)器接到請(qǐng)求后馬上返回響應(yīng)信息并關(guān)閉連接 |
優(yōu)點(diǎn) | 后端容易實(shí)現(xiàn) |
缺點(diǎn) | 大部分是無用的請(qǐng)求梦重,浪費(fèi)服務(wù)器資源和帶寬 |
長(zhǎng)輪詢 | 客戶端向服務(wù)器發(fā)送Ajax請(qǐng)求赏壹,服務(wù)器接到請(qǐng)求后hold住連接,直到有新消息才返回響應(yīng)信息并關(guān)閉連接泥彤,客戶端處理完響應(yīng)信息后再向服務(wù)器發(fā)送新的請(qǐng)求 |
優(yōu)點(diǎn) | 在無消息的情況下不會(huì)頻繁的請(qǐng)求欲芹,耗費(fèi)資源小 |
缺點(diǎn) | 服務(wù)器保持連接會(huì)消耗資源,返回?cái)?shù)據(jù)順序無保證 |
iframe | 在頁面里嵌入一個(gè)隱蔵iframe吟吝,將這個(gè)隱蔵iframe的src屬性設(shè)為對(duì)一個(gè)長(zhǎng)連接的請(qǐng)求或是采用xhr請(qǐng)求菱父,服務(wù)器端就能源源不斷地往客戶端輸入數(shù)據(jù) |
優(yōu)點(diǎn) | 消息即時(shí)到達(dá),不發(fā)無用請(qǐng)求剑逃;管理起來也相對(duì)方便 |
缺點(diǎn) | 服務(wù)器維護(hù)一個(gè)長(zhǎng)連接會(huì)增加開銷 |
什么是Websocket
Websocket協(xié)議是基于TCP的一種新的通信協(xié)議浙宜,可以在瀏覽器和服務(wù)器之間建立“套接字(Socket)”連接,簡(jiǎn)單地說:客戶端和服務(wù)器之間存在持久的連接蛹磺,而且雙方都可以隨時(shí)開始發(fā)送數(shù)據(jù)粟瞬。
Websocket協(xié)議有兩部分,握手和數(shù)據(jù)傳輸
Websocket握手
一個(gè)典型的Websocket握手請(qǐng)求如下:
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
服務(wù)器回應(yīng)
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/
- Connection必須設(shè)置Upgrade萤捆,表示客戶端希望連接升級(jí)裙品。
- Upgrade字段必須設(shè)置Websocket,表示希望升級(jí)到Websocket協(xié)議俗或。
- Sec-WebSocket-Key是隨機(jī)的字符串市怎,服務(wù)器端會(huì)用這些數(shù)據(jù)來構(gòu)造出一個(gè)SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一個(gè)特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”辛慰,然后計(jì)算SHA-1摘要区匠,之后進(jìn)行BASE-64編碼,將結(jié)果做為“Sec-WebSocket-Accept”頭的值帅腌,返回給客戶端驰弄。如此操作,可以盡量避免普通HTTP請(qǐng)求被誤認(rèn)為Websocket協(xié)議速客。
- Sec-WebSocket-Version 表示支持的Websocket版本戚篙。RFC6455要求使用的版本是13,之前草案的版本均應(yīng)當(dāng)棄用挽封。
- Origin字段是可選的已球,通常用來表示在瀏覽器中發(fā)起此Websocket連接所在的頁面,類似于Referer辅愿。但是智亮,與Referer不同的是,Origin只包含了協(xié)議和主機(jī)名稱点待。
- 其他一些定義在HTTP協(xié)議中的字段阔蛉,如Cookie等,也可以在Websocket中使用癞埠。
在Node中可以使用http
模塊實(shí)現(xiàn)一個(gè)簡(jiǎn)單的服務(wù)器來完成Websocket的握手状原。
服務(wù)器要監(jiān)聽upgrade的請(qǐng)求聋呢。
server.on('upgrade', (req, socket, head) => {
});
完成Sec-WebSocket-Key -> Sec-WebSocket-Accept
const guid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
const key = crypto
.createHash('sha1')
.update(`${req.headers['sec-websocket-key']}${guid}`)
.digest('base64');
返回必要的頭信息
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: webSocket\r\n' +
'Connection: upgrade\r\n' +
`Sec-WebSocket-Accept: ${key}\r\n` +
'\r\n'
);
Websocket數(shù)據(jù)傳輸(frame)
在Websocket協(xié)議中,客戶端和服務(wù)器端都可以互相發(fā)送數(shù)據(jù)颠区。
發(fā)送和接受的數(shù)據(jù)如下面
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: 1bit
是否為最后的frame標(biāo)記 - FSV: 3bits
保留 - opcode: 4bits
payload數(shù)據(jù)說明 - MASK: 1bit
是否有mask標(biāo)記 - Payload len: 7bits
如果payload長(zhǎng)度126削锰,延長(zhǎng)16bits,如果payload長(zhǎng)度是127,延長(zhǎng)到64bits - Masking-key: 4 bits
- Payload Data
opcode的類型
|Opcode | Meaning | Reference |
-+--------+-------------------------------------+-----------|
| 0 | Continuation Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 1 | Text Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 2 | Binary Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 8 | Connection Close Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 9 | Ping Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 10 | Pong Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
在Node中用net
這個(gè)模塊讀取socket中frame的數(shù)據(jù)
socket.on('data', (buf: Buffer) => {
const fro = buf[0]; // 讀取第一個(gè)字節(jié)
const fin = (fro & 0x80) === 0x80;
const opcode = fro & 0x0f;
console.log("fin: ", fin);
console.log("opcode: ", opcode);
const mp = buf[1]; // 讀取第二個(gè)字節(jié)
const mask = (mp & 0x80) === 0x80;
const payloadLen = mp & 0x7f;
console.log("mask: ", mask);
console.log("payloadLen: ", payloadLen);
// 這里做了簡(jiǎn)化處理毕莱,實(shí)際過程中需要判斷mask和payloadLen
const maskKey = buf.slice(2, 6);
const payload = buf.slice(6, 6+payloadLen);
const data = payload.map((p, i) => {
return p ^ maskKey[i%4] // 利用掩碼解析數(shù)據(jù)
});
console.log(data.toString('utf8'));
});
發(fā)送數(shù)據(jù)也是同樣的道理器贩,先組裝一個(gè)frame,然后寫入到socket中去
const text = Buffer.from("Hello there");
const finfo = Buffer.allocUnsafe(2);
finfo[0] = 0b10000001;
finfo[1] = text.length;
const ret = Buffer.concat([finfo, text]);
socket.write(ret);
完整的代碼
瀏覽器端代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
var ws = new WebSocket("ws://localhost:2345");
ws.addEventListener('open', function(e) {
ws.send("can you hear me?");
})
ws.addEventListener('message', function(e) {
console.log(e.data);
});
</script>
</body>
</html>
服務(wù)器端代碼(用ts實(shí)現(xiàn)的)
import * as http from 'http';
import * as net from 'net';
import * as crypto from 'crypto';
const server = http.createServer();
server.on('upgrade', (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
const key = req.headers['sec-websocket-key'];
let accept;
if (key && typeof key !== 'undefined') {
accept = webSocketAccept(key as string);
}
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: webSocket\r\n' +
'Connection: upgrade\r\n' +
`Sec-WebSocket-Accept: ${accept}\r\n` +
'\r\n'
);
socket.on('data', (buf: Buffer) => {
const fro = buf[0];
const fin = (fro & 0x80) === 0x80;
const opcode = fro & 0x0f;
console.log("fin: ", fin);
console.log("opcode: ", opcode);
const mp = buf[1];
const mask = (mp & 0x80) === 0x80;
const payloadLen = mp & 0x7f;
console.log("mask: ", mask);
console.log("payloadLen: ", payloadLen);
const maskKey = buf.slice(2, 6);
const payload = buf.slice(6, 6+payloadLen);
const data = payload.map((p, i) => {
return p ^ maskKey[i%4]
});
console.log(data.toString('utf8'));
const text = Buffer.from("Hello there");
const finfo = Buffer.allocUnsafe(2);
finfo[0] = 0b10000001;
finfo[1] = text.length;
const ret = Buffer.concat([finfo, text]);
let i = 3;
do {
socket.write(ret);
i = i -1;
} while (i < 3);
});
});
function webSocketAccept(key: string): string {
const hash = crypto.createHash('sha1');
hash.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
return hash.digest('base64');
}
server.listen(2345, () => {
console.log("server started at 2345");
});