0依賴解析Websocket協(xié)議(Node)

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");
});

引用

  1. https://zh.wikipedia.org/wiki/WebSocket
  2. https://en.wikipedia.org/wiki/OSI_model
  3. https://en.wikipedia.org/wiki/Transmission_Control_Protocol
  4. https://en.wikipedia.org/wiki/Duplex_(telecommunications)
  5. http://www.52im.net/thread-224-1-1.html
  6. https://www.html5rocks.com/zh/tutorials/websockets/basics/
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末朋截,一起剝皮案震驚了整個(gè)濱河市蛹稍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌部服,老刑警劉巖唆姐,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異廓八,居然都是意外死亡奉芦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門瘫想,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仗阅,“玉大人昌讲,你說我怎么就攤上這事国夜。” “怎么了短绸?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵车吹,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我醋闭,道長(zhǎng)窄驹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任证逻,我火速辦了婚禮乐埠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘囚企。我一直安慰自己丈咐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布龙宏。 她就那樣靜靜地躺著棵逊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪银酗。 梳的紋絲不亂的頭發(fā)上辆影,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天徒像,我揣著相機(jī)與錄音,去河邊找鬼蛙讥。 笑死锯蛀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的次慢。 我是一名探鬼主播谬墙,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼经备!你這毒婦竟也來了拭抬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤侵蒙,失蹤者是張志新(化名)和其女友劉穎造虎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纷闺,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡算凿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了犁功。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氓轰。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖浸卦,靈堂內(nèi)的尸體忽然破棺而出署鸡,到底是詐尸還是另有隱情,我是刑警寧澤限嫌,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布靴庆,位于F島的核電站,受9級(jí)特大地震影響怒医,放射性物質(zhì)發(fā)生泄漏炉抒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一稚叹、第九天 我趴在偏房一處隱蔽的房頂上張望焰薄。 院中可真熱鬧,春花似錦扒袖、人聲如沸塞茅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凡桥。三九已至,卻和暖如春蚀同,著一層夾襖步出監(jiān)牢的瞬間缅刽,已是汗流浹背啊掏。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衰猛,地道東北人迟蜜。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像啡省,于是被迫代替她去往敵國和親娜睛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容