Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 506, comments - 13, trackbacks - 0, articles - 1

          本文原題“Node.js - 200 多行代碼實(shí)現(xiàn) Websocket 協(xié)議”,為了提升內(nèi)容品質(zhì),有較大修訂。

          1、引言

          最近正在研究 WebSocket 相關(guān)的知識,想著如何能自己實(shí)現(xiàn) WebSocket 協(xié)議。到網(wǎng)上搜羅了一番資料后用 Node.js 實(shí)現(xiàn)了一個(gè)WebSocket協(xié)議服務(wù)器,倒也沒有想象中那么復(fù)雜,除去注釋語句和 console 語句后,大約 200 行代碼左右。

          本文分享了自已開發(fā)一個(gè)WebSocket服務(wù)端實(shí)現(xiàn)過程中需要的知識儲備,以及具體的代碼實(shí)現(xiàn)含義等,非常適合想在短時(shí)間內(nèi)對WebSocket協(xié)議從入門到精通的Web端即時(shí)通訊開發(fā)者閱讀。

          如果你想要寫一個(gè)WebSocket 服務(wù)器,首先需要讀懂對應(yīng)的網(wǎng)絡(luò)協(xié)議 RFC6455,不過這對于一般人來說有些 “晦澀”,英文且不說,還得咬文嚼字理解 網(wǎng)絡(luò)編程 含義。

          好在 WebSocket 技術(shù)出現(xiàn)比較早,所以早就有人翻譯了完整的 RFC6455中文版,網(wǎng)上也有很多針對該協(xié)議的剖析文章,很多文章里還有現(xiàn)成的實(shí)現(xiàn)代碼可以參考,所以說實(shí)現(xiàn)一個(gè)簡單的 WebSocket 服務(wù)并非難事。

          本文更偏向?qū)崙?zhàn)(in action),會從知識儲備、具體代碼分析以及注意事項(xiàng)角度去講解如何用 Node.js 實(shí)現(xiàn)一個(gè)簡單的 WebSocket 服務(wù),至于 WebSocket 概念、定義、解釋和用途等基礎(chǔ)知識不會涉及,因?yàn)檫@些知識在本文所列的參考文章中輕松找到。

          友情提示:本文對應(yīng)的源碼,請從文末“11、代碼下載”一節(jié)下載之。

          學(xué)習(xí)交流:

          - 即時(shí)通訊技術(shù)交流群:215477170 [推薦]

          - 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM

          - 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK

          (本文同步發(fā)布于:http://www.52im.net/thread-3175-1-1.html

          2、關(guān)于作者

          3、基本常識

          在學(xué)習(xí)本文內(nèi)容之前,我認(rèn)為很有必要簡單了解一下Web端即時(shí)通訊技術(shù)的“過去”和“現(xiàn)在”,因?yàn)樾聲r(shí)代的開發(fā)者(沒有經(jīng)歷過短輪詢、長輪詢、Comet技術(shù)的這波人),很難理解WebSocket對于Web端的即時(shí)通訊技術(shù)來說,意味著什么。

          所謂“憶苦思甜”,了解了Web端即時(shí)通訊技術(shù)的過去,方知WebSocket這種技術(shù)的珍貴。。。

          3.1 舊時(shí)代的Web端即時(shí)通訊技術(shù)

          自從Web端即時(shí)通訊的概念提出后,“實(shí)時(shí)”性便成為了Web開發(fā)者們津津樂道的話題。實(shí)時(shí)化的Web應(yīng)用,憑借其響應(yīng)迅速、無需刷新、節(jié)省網(wǎng)絡(luò)流量的特性,不僅讓開發(fā)者們眼前一亮,更是為用戶帶來絕佳的網(wǎng)絡(luò)體驗(yàn)。

          但很多開發(fā)者可能并不清楚,舊時(shí)代的Web端“實(shí)時(shí)”通信,主要基于 Ajax的拉取和Comet的推送

          大家都知道Ajax,這是一種借助瀏覽器端JavaScript實(shí)現(xiàn)的異步無刷新請求功能:要客戶端按需向服務(wù)器發(fā)出請求,并異步獲取來自服務(wù)器的響應(yīng),然后按照邏輯更新當(dāng)前頁面的相應(yīng)內(nèi)容。

          但是這僅僅是拉取啊,這并不是真正的“實(shí)時(shí)”:缺少服務(wù)器端的自動推送!

          因此,我們不得不使用另一種略復(fù)雜的技術(shù) Comet,只有當(dāng)這兩者配合起來,這個(gè)Web應(yīng)用才勉強(qiáng)算是個(gè)“實(shí)時(shí)”的Web端應(yīng)用!

          ▲ Ajax和Comet技術(shù)原理(圖片引用自《Web端即時(shí)通訊技術(shù)盤點(diǎn)》)

          3.2 WebSocket協(xié)議出現(xiàn)

          隨著HTML5標(biāo)準(zhǔn)的出現(xiàn),WebSocket技術(shù)橫空出世,隨著HTML5標(biāo)準(zhǔn)的廣泛普及,越來越多的現(xiàn)代瀏覽器開始全面支持WebSocket技術(shù)了。

          至于WebSocket,我想大家或多或少都聽說過。

          WebSocket是一種全新的協(xié)議。它將TCP的Socket(套接字)應(yīng)用在了web page上,從而使通信雙方建立起一個(gè)保持在活動狀態(tài)連接通道,并且屬于全雙工(雙方同時(shí)進(jìn)行雙向通信)。

          事實(shí)是:WebSocket協(xié)議是借用HTTP協(xié)議的 101 switch protocol 來達(dá)到協(xié)議轉(zhuǎn)換的,從HTTP協(xié)議切換成WebSocket通信協(xié)議。

          再簡單點(diǎn)來說,它就好像將 Ajax 和 Comet 技術(shù)的特點(diǎn)結(jié)合到了一起,只不過性能要高并且使用起來要方便的多(方便當(dāng)然是之指在客戶端方面了)。

          4、WebSocket知識儲備

          如果要自己寫一個(gè) WebSocket 服務(wù),主要有兩個(gè)難點(diǎn):

          • 1)熟練掌握 WebSocket 的協(xié)議,這個(gè)需要多讀現(xiàn)有的解讀類文章(下面會給出參考文章);
          • 2)操作二進(jìn)制數(shù)據(jù)流,在 Node.js 中需要對 Buffer 這個(gè)類稍微熟悉些。

          同時(shí)還需要具備兩個(gè)基礎(chǔ)知識點(diǎn):

          具體的做法如下,推薦先閱讀以下幾篇參考文章:

          然后開始寫代碼。

          在實(shí)現(xiàn)過程中的大部分代碼可以從下面幾篇文章中找到并借鑒(copy):

          閱讀完上面的文章,你會有發(fā)現(xiàn)一個(gè)共同點(diǎn),就是在實(shí)現(xiàn) WebSockets 過程中,最最核心的部分就是 解析 或者 生成 Frame(幀)。

          就是下面這結(jié)構(gòu):

          ▲ 截圖來自《rfc6455 - Base Framing Protocol

          想要理解 frame 各個(gè)字段的含義,可參考《WebSocket詳解(三):深入WebSocket通信協(xié)議細(xì)節(jié)》,文中作者繪制了一副圖來解釋這個(gè) frame 結(jié)構(gòu)。

          而在代碼層面,frame 的解析或生成可以在 RocketEngine - parser 或者 _processBuffer 中找到。

          在完成上面幾個(gè)方面的知識儲備之后,而且大多有現(xiàn)成的代碼,所以自己邊抄邊寫一個(gè) Websocket 服務(wù)端實(shí)現(xiàn)并不算太難。

          對于 WebSocket 初學(xué)者,請務(wù)必閱讀以上參考文章,對 Websocket 協(xié)議有大概的了解之后再繼續(xù)本文剩下部分的閱讀,否則很有可能會覺得我寫得云里霧里,不知所云。

          5、實(shí)戰(zhàn)效果預(yù)覽

          本次的實(shí)現(xiàn)代碼可以從文末“11、代碼下載”章節(jié)下載到:

          (請從原文鏈接下載:http://www.52im.net/thread-3175-1-1.html)

          下載后本地運(yùn)行即可,執(zhí)行:

          node index.js

          運(yùn)行成功后,將會在 http://127.0.0.1:3000 創(chuàng)建服務(wù)。

          運(yùn)行服務(wù)之后,打開控制臺就能看到效果:

          動圖中瀏覽器 console 所執(zhí)行的 js 代碼步驟如下:

          1)先建立連接:

          var ws = new WebSocket("ws://127.0.0.1:3000");

          ws.onmessage = function(evt) {

            console.log( "Received Message: "+ evt.data);

          };

          2)然后發(fā)送消息:(注意一定要在建立連接之后再執(zhí)行該語句,否則發(fā)不出消息的)

          ws.send('hello world');

          從效果可見,我們已經(jīng)實(shí)現(xiàn) WebSocket 最基本的通訊功能了。

          接下來我們詳細(xì)看一下具體實(shí)現(xiàn)的細(xì)節(jié)。

          6、代碼解讀1:調(diào)用所寫的 WebSocket 類

          站在使用者的角度,假設(shè)我們已經(jīng)完成 WebSocket 類了,那么應(yīng)該怎么使用?

          客戶端通過 HTTP Upgrade 請求,即 101 Switching Protocol 到 HTTP 服務(wù)器,然后由服務(wù)器進(jìn)行協(xié)議轉(zhuǎn)換。

          在 Node.js 中我們通過 http.createServer 獲取 http.server 實(shí)例,然后監(jiān)聽 upgrade 事件,在處理這個(gè)事件。

          如下面的代碼所示:

          // HTTP服務(wù)器部分

          var server = http.createServer(function(req, res) {

            res.end('websocket test\r\n');

          });

          // Upgrade請求處理

          server.on('upgrade', function(req, socket, upgradeHead){

            // 初始化 ws

            var ws = new WebSocket(req, socket, upgradeHead);

            // ... ws 監(jiān)聽 data、error 的邏輯等

          });

          這里監(jiān)聽 upgrade 事件的回調(diào)函數(shù)中第二個(gè)參數(shù) socket 是 net.Socket實(shí)例,這個(gè)類是 TCP 或 UNIX Socket 的抽象,同時(shí)一個(gè) net.Socket 也是一個(gè) duplex stream,所以它能被讀或?qū)懀⑶宜彩且粋€(gè) EventEmitter

          我們就利用這個(gè) socket 對象上進(jìn)行 Websocket 類實(shí)例的初始化工作;

          7、代碼解讀2:構(gòu)造函數(shù)

          所以不難理解 Websocket 的構(gòu)造函數(shù)就是下面這個(gè)樣子:

          class WebSocket extends EventEmitter {

            constructor(req, socket, upgradeHead){

              super(); // 調(diào)用 EventEmitter 構(gòu)造函數(shù)

              // 1. 構(gòu)造響應(yīng)頭 resHeaders 部分

              // 2. 監(jiān)聽 socket 的 data 事件,以及 error 事件

              // 3. 初始化成員屬性

            }

          }

          注意:我們需要繼承內(nèi)置的 EventEmitter ,這樣生成的實(shí)例才能監(jiān)聽、綁定事件。

          Node.js 采用事件驅(qū)動、異步編程,天生就是為了網(wǎng)絡(luò)服務(wù)而設(shè)計(jì)的,繼承 EventEmitter 就能享受到非阻塞模式的 IO 處理。

          這里特別講一下其中 響應(yīng)頭的構(gòu)造 和 事件監(jiān)聽 部分。

          7.1 返回響應(yīng)頭(Response Header)

          根據(jù)協(xié)議規(guī)范,我們能寫出響應(yīng)頭的內(nèi)容:

          • 1)將 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
          • 2)通過 SHA1 計(jì)算出摘要,并轉(zhuǎn)成 base64 字符串。

          具體代碼如下:

          var resKey = hashWebSocketKey(req.headers['sec-websocket-key']);

          // 構(gòu)造響應(yīng)頭

          var resHeaders = [

            'HTTP/1.1 101 Switching Protocols',

            'Upgrade: websocket',

            'Connection: Upgrade',

            'Sec-WebSocket-Accept: '+ resKey

          ]

            .concat('', '')

            .join('\r\n');

          socket.write(resHeaders);

          當(dāng)執(zhí)行 socket.write(resHeaders); 到后就和客戶端建立起 WebSocket 連接了,剩下去就是數(shù)據(jù)的處理。

          7.2 監(jiān)聽事件

          socket 就是 TCP 協(xié)議的抽象,直接在上面監(jiān)聽已有的 data 事件和 close 事件這兩個(gè)事件。

          還有其他事件,比如 error、end 等,詳細(xì)參考 net.Socket 文檔。

          socket.on('data', data => {

            this.buffer = Buffer.concat([this.buffer, data]);

            while(this._processBuffer()) {} // 循環(huán)處理返回的 data 數(shù)據(jù)

          });

          socket.on('close', had_error => {

            if(!this.closed) {

              this.emit('close', 1006);

              this.closed = true;

            }

          });

          close 的事件邏輯比較簡單,比較重要的是 data 的事件監(jiān)聽部分。核心就是 this._processBuffer() 這個(gè)方法,用于處理客戶端傳送過來的數(shù)據(jù)(即 Frame 數(shù)據(jù))。

          注意:該方法是放在 while 循環(huán)語句里,處理好邊界情況,防止死循環(huán)。

          8、代碼解讀3:Frame 幀數(shù)據(jù)的處理

          WebSocket 客戶端、服務(wù)端通信的最小單位是幀(frame),由1個(gè)或多個(gè)幀組成一條完整的消息(message)。

          這 this._processBuffer() 部分代碼邏輯就是用來解析幀數(shù)據(jù)的,所以它是實(shí)現(xiàn) WebSocket 代碼的關(guān)鍵;(該方法里面用到了大量的位操作符以及 Buffer 類的操作)

          幀數(shù)據(jù)結(jié)構(gòu)詳細(xì)定義可參考 RFC6455 5.2節(jié)(英文不好的話,去下載中文翻譯版《WebSocket標(biāo)準(zhǔn)協(xié)議手冊(稀缺中文版+英文原版)》),上面羅列的參考文章都有詳細(xì)的解讀,我在這兒也不啰嗦講細(xì)節(jié)了,直接看代碼比聽我用文字講要好。

          這里就其中兩個(gè)細(xì)節(jié)需要鋪墊一下,方便更好地理解代碼。

          8.1 操作碼(Opcode)

          Opcode 即 操作代碼,Opcode 的值決定了應(yīng)該如何解析后續(xù)的數(shù)據(jù)載荷(data payload)

          根據(jù) Opcode 我們可以大致將數(shù)據(jù)幀分成兩大類:數(shù)據(jù)幀 和 控制幀。

          數(shù)據(jù)幀,目前只有 3 種,對應(yīng)的 opcode 是:

          • 0x0:數(shù)據(jù)延續(xù)幀
          • 0x1:utf-8文本
          • 0x2:二進(jìn)制數(shù)據(jù);
          • 0x3 - 0x7:目前保留,用于后續(xù)定義的非控制幀。

          控制幀,除了上述 3 種數(shù)據(jù)幀之外,剩下的都是控制幀:

          • 0x8:表示連接斷開
          • 0x9:表示 ping 操作
          • 0xA:表示 pong 操作
          • 0xB - 0xF:目前保留,用于后續(xù)定義的控制幀

          在代碼里,我們會先從幀數(shù)據(jù)中提取操作碼:

          var opcode = byte1 & 0x0f; //截取第一個(gè)字節(jié)的后 4 位,即 opcode 碼

          然后根據(jù)協(xié)議獲取到真正的數(shù)據(jù)載荷(data payload),然后將這兩部分傳給 _handleFrame 方法:

          this._handleFrame(opcode, payload); // 處理操作碼

          該方法會根據(jù)不同的 opcode 做出不同的操作:

          _handleFrame(opcode, buffer) {

              var payload;

              switch(opcode) {

                case OPCODES.TEXT:

                  payload = buffer.toString('utf8'); //如果是文本需要轉(zhuǎn)化為utf8的編碼

                  this.emit('data', opcode, payload); //Buffer.toString()默認(rèn)utf8 這里是故意指示的

                  break;

                case OPCODES.BINARY: //二進(jìn)制文件直接交付

                  payload = buffer;

                  this.emit('data', opcode, payload);

                  break;

                case OPCODES.PING: // 發(fā)送 pong 做響應(yīng)

                  this._doSend(OPCODES.PONG, buffer);

                  break;

                case OPCODES.PONG: //不做處理

                  console.log('server receive pong');

                  break;

                case OPCODES.CLOSE: // close有很多關(guān)閉碼

                  let code, reason; // 用于獲取關(guān)閉碼和關(guān)閉原因

                  if(buffer.length >= 2) {

                    code = buffer.readUInt16BE(0);

                    reason = buffer.toString('utf8', 2);

                  }

                  this.close(code, reason);

                  this.emit('close', code, reason);

                  break;

                default:

                  this.close(1002, 'unhandle opcode:'+ opcode);

              }

            }

          8.2 分片(Fragment)

          本節(jié)代碼對應(yīng)的標(biāo)準(zhǔn)文檔:5.4 - Fragmentation(英文不好的話,去下載中文翻譯版《WebSocket標(biāo)準(zhǔn)協(xié)議手冊(稀缺中文版+英文原版)》)。

          一旦 WebSocket 客戶端、服務(wù)端建立連接后,后續(xù)的操作都是基于數(shù)據(jù)幀的傳遞。理論上來說,每個(gè)幀(Frame)的大小是沒有限制的。

          對于大塊的數(shù)據(jù),WebSocket 協(xié)議建議對數(shù)據(jù)進(jìn)行分片(Fragment)操作。

          分片的意義主要是兩方面:

          • 1)主要目的是允許當(dāng)消息開始但不必緩沖該消息時(shí)發(fā)送一個(gè)未知大小的消息。如果消息不能被分片,那么端點(diǎn)將不得不緩沖整個(gè)消息以便在首字節(jié)發(fā)生之前統(tǒng)計(jì)出它的長度。對于分片,服務(wù)器或中間件可以選擇一個(gè)合適大小的緩沖,當(dāng)緩沖滿時(shí),再寫一個(gè)片段到網(wǎng)絡(luò);
          • 2)另一方面分片傳輸也能更高效地利用多路復(fù)用提高帶寬利用率,一個(gè)邏輯通道上的一個(gè)大消息獨(dú)占輸出通道是不可取的,因此多路復(fù)用需要可以分割消息為更小的分段來更好的共享輸出通道。參考文檔《I/O多路復(fù)用(multiplexing)是什么?》。

          WebSocket 協(xié)議提供的分片方法,是將原本一個(gè)大的幀拆分成數(shù)個(gè)小的幀。

          下面是把一個(gè)大的Frame分片的圖示: 

          由圖可知,第一個(gè)分片的 FIN 為 0,Opcode 為非0值(0x1 或 0x2),最后一個(gè)分片的FIN為1,Opcode為 0。中間分片的 FIN 和 opcode 二者均為 0。

          根據(jù) FIN 的值來判斷,是否已經(jīng)收到消息的最后一個(gè)數(shù)據(jù)幀:

          • 1)FIN=1 表示當(dāng)前數(shù)據(jù)幀為消息的最后一個(gè)數(shù)據(jù)幀,此時(shí)接收方已經(jīng)收到完整的消息,可以對消息進(jìn)行處理;
          • 2)FIN=0,則接收方還需要繼續(xù)監(jiān)聽接收其余的數(shù)據(jù)幀。

          opcode在數(shù)據(jù)交換的場景下,表示的是數(shù)據(jù)的類型:

          • 1)0x01 表示文本,永遠(yuǎn)是 utf8 編碼的;
          • 2)0x02 表示二進(jìn)制;
          • 3)0x00 比較特殊,表示 延續(xù)幀(continuation frame),顧名思義,就是完整消息對應(yīng)的數(shù)據(jù)幀還沒接收完。

          代碼里,我們需要檢測 FIN 的值,如果為 0 說明有分片,需要記錄第一個(gè) FIN 為 0 時(shí)的 opcode 值,緩存到 this.frameOpcode 屬性中,將載荷緩存到 this.frames 屬性中。

          如下所示:

          var FIN = byte1 & 0x80; // 如果為0x80,則標(biāo)志傳輸結(jié)束,獲取高位 bit

          // 如果是 0 的話,說明是延續(xù)幀,需要保存好 opCode

          if(!FIN) {

            this.frameOpcode = opcode || this.frameOpcode; // 確保不為 0;

          }

          //....

          // 有可能是分幀,需要拼接數(shù)據(jù)

          this.frames = Buffer.concat([this.frames, payload]); // 保存到 frames 中

          當(dāng)接收到最后一個(gè) FIN 幀的時(shí)候,就可以組裝后給 _handleFrame 方法:

          if(FIN) {

            payload = this.frames.slice(0); // 獲取所有拼接完整的數(shù)據(jù)

            opcode = opcode || this.frameOpcode; // 如果是 0 ,則保持獲取之前保存的 code

            this.frames = Buffer.alloc(0); // 清空 frames

            this.frameOpcode = 0; // 清空 opcode

            this._handleFrame(opcode, payload); // 處理操作碼

          }

          8.3 發(fā)送數(shù)據(jù)幀

          上面講的都是接收并解析來自客戶端的數(shù)據(jù)幀,當(dāng)我們想給客戶端發(fā)送數(shù)據(jù)幀的時(shí)候,也得按協(xié)議來。

          這部分操作相當(dāng)于是上述 _processBuffer 方法的逆向操作,在代碼里我們使用 encodeMessage 方法(為了簡單起見,我們發(fā)送給客戶端的數(shù)據(jù)沒有經(jīng)過掩碼處理)將發(fā)送的數(shù)據(jù)分裝成數(shù)據(jù)幀的格式,然后調(diào)用 socket.write 方法發(fā)送給客戶端。

          如下所示:

          _doSend(opcode, payload) {

            // 1. 考慮數(shù)據(jù)分片

            this.socket.write(

              encodeMessage(count > 0 ? OPCODES.CONTINUE : opcode, payload)

            ); //編碼后直接通過socket發(fā)送

          為了考慮分片場景,特意設(shè)置 MAX_FRAME_SIZE 來對每次發(fā)送的數(shù)據(jù)長度做截?cái)嘧龇制?/strong>

            // ...

            var len = Buffer.byteLength(payload);

            // 分片的距離邏輯

            var count = 0;

            // 這里可以針對 payload 的長度做分片

            while(len > MAX_FRAME_SIZE) {

              var framePayload = payload.slice(0, MAX_FRAME_SIZE);

              payload = payload.slice(MAX_FRAME_SIZE);

              this.socket.write(

                encodeMessage(

                  count > 0 ? OPCODES.CONTINUE : opcode,

                  framePayload,

                  false

                )

              ); //編碼后直接通過socket發(fā)送

              count++;

              len = Buffer.byteLength(payload);

            }

          // ...

          至此已經(jīng)實(shí)現(xiàn) WebSocket 協(xié)議的關(guān)鍵部分,所組裝起來的代碼就能和客戶端建立 WebSocket 連接并進(jìn)行數(shù)據(jù)交互了。

          9、有關(guān)WebSocket的常見疑問

          9.1 字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” 怎么來的?

          這個(gè)標(biāo)志性字符串是專門標(biāo)示 WebSocket 協(xié)議的 UUID;UUID 是長度為 16-byte(128-bit)的ID,一般以形如f81d4fae-7dec-11d0-a765-00a0c91e6bf6的字符串作為 URN(Uniform Resource Name,統(tǒng)一資源名稱)。

          UUID 可以移步到《UUID原理》和 RFC 4122 獲取更多知識。

          為啥選擇這個(gè)字符串?

          在WebSocket標(biāo)準(zhǔn)協(xié)議文檔的第七頁已經(jīng)有明確的說明了:

          英文不好的話,見中文翻譯版《WebSocket標(biāo)準(zhǔn)協(xié)議手冊(稀缺中文版+英文原版)

          之所以選用這個(gè) UUID ,主要該 ID 極大不太可能被其他不了解 WebSocket 協(xié)議的網(wǎng)絡(luò)終端所使用。

          我也不曉得該怎么翻譯。總之,就說這個(gè) ID 就相當(dāng)于 WebSocket 協(xié)議的 “身份證號” 了。

          9.2 Websocket 和 HTTP 什么關(guān)系?

          HTTP、WebSocket 等應(yīng)用層協(xié)議,都是基于 TCP 協(xié)議來傳輸數(shù)據(jù)的,我們可以把這些高級協(xié)議理解成對 TCP 的封裝。

          既然大家都使用 TCP 協(xié)議,那么大家的連接和斷開,都要遵循 TCP 協(xié)議中的三次握手和四次握手 ,只是在連接之后發(fā)送的內(nèi)容不同,或者是斷開的時(shí)間不同。

          對于 WebSocket 來說,它必須依賴 HTTP 協(xié)議進(jìn)行一次握手 ,握手成功后,數(shù)據(jù)就直接從 TCP 通道傳輸,與 HTTP 無關(guān)了。

          更詳細(xì)的解釋,可以移步:

          9.3 瀏覽器中 Websocket 會自動分片么?

          答案是:看具體瀏覽器的實(shí)現(xiàn)。

          WebSocket是一個(gè) message based 的協(xié)議,它可以自動將數(shù)據(jù)分片,并且自動將分片的數(shù)據(jù)組裝。每個(gè) message 可以是一個(gè)或多個(gè)分片。message 不記錄長度,分片才記錄長度。

          根據(jù)協(xié)議 websocket 協(xié)議中幀長度上限為 2^63 byte(為 8388608 TB),可以認(rèn)為沒有限制,很明顯按協(xié)議的最大上限來傳輸數(shù)據(jù)是不靠譜的。所以在實(shí)際使用中 websocket 消息長度限制取決于具體的實(shí)現(xiàn)。

          關(guān)于這方面,找了兩篇參考文章:

          在文章《WebSocket探秘》中,作者就做了一個(gè)實(shí)驗(yàn),作者發(fā)送 27378 個(gè)字節(jié),結(jié)果被迫分包了;如果是大數(shù)據(jù)量,就會被socket自動分包發(fā)送。

          而經(jīng)過我本人試驗(yàn),發(fā)現(xiàn) Chrome 瀏覽器(版本 68.0.3440.106 - 64bit)會針對 131072(=2^17)bytes 大小進(jìn)行自動分包。

          我是通過以下測試代碼驗(yàn)證:

          var ws = new WebSocket("ws://127.0.0.1:3000");

          ws.onmessage = function(evt) {

            console.log( "Received Message: "+ evt.data);

          };

          var myArray = new ArrayBuffer(131072 * 2 + 1);

          ws.send(myArray);

          服務(wù)端日志:

          server detect fragment, sizeof payload: 131072

          server detect fragment, sizeof payload: 131072

          receive data: 2 262145

          客戶端日志:

          Received Message: good job

          截圖如下:

          而以同樣的方式去測試一些自己機(jī)器上的瀏覽器:

          • 1)Firefox(62.0,64bit);
          • 2)safari (11.1.2 - 13605.3.8);
          • 3)IE 11。

          這些客戶端上的 WebSocket 幾乎沒有大小的分片(隨著數(shù)據(jù)量增大,發(fā)送會減緩,但并沒有發(fā)現(xiàn)分片現(xiàn)象)。

          10、本文小結(jié)

          從剛開始決定閱讀 WebSocket 協(xié)議,到自己使用 Node.js 實(shí)現(xiàn)一套簡單的 WebSocket 協(xié)議,到這篇文章的產(chǎn)出,前后耗費(fèi)大約 1 個(gè)月時(shí)間(拖延癥。。。)。

          感謝文中所提及的參考文獻(xiàn)所給予的幫助,讓我實(shí)現(xiàn)過程中事半功倍。

          之所以能夠使用較少的代碼實(shí)現(xiàn) WebSocket,是因?yàn)?Node.js 體系本身了很好的基礎(chǔ),比如其所提供的 EventEmitter 類自帶事件循環(huán),http 模塊讓你直接使用封裝好的 socket 對象,我們只要按照 WebSocket 協(xié)議實(shí)現(xiàn) Frame(幀)的解析和組裝即可。

          在實(shí)現(xiàn)一遍 WebSocket 協(xié)議后,就能較為深刻地理解以下知識點(diǎn)(一切都是那么自然而然):

          • 1)Websocket 是一種應(yīng)用層協(xié)議,是為了提供 Web 應(yīng)用程序和服務(wù)端全雙工通信而專門制定的;
          • 2)WebSocket 和 HTTP 都是基于 TCP 協(xié)議實(shí)現(xiàn)的;
          • 3)WebSocket和 HTTP 的唯一關(guān)聯(lián)就是 HTTP 服務(wù)器需要發(fā)送一個(gè) “Upgrade” 請求,即 101 Switching Protocol 到 HTTP 服務(wù)器,然后由服務(wù)器進(jìn)行協(xié)議轉(zhuǎn)換。
          • 4)WebSocket使用 HTTP 來建立連接,但是定義了一系列新的 header 域,這些域在 HTTP 中并不會使用;
          • 5)WebSocket 可以和 HTTP Server 共享同一 port
          • 6)WebSocket 的 數(shù)據(jù)幀有序
          • ...

          本文僅僅是協(xié)議的簡單實(shí)現(xiàn),對于 WebSocket 的其實(shí)還有很多事情可以做(比如支持 命名空間、流式 API 等),有興趣的可以參考業(yè)界流行的 WebSocket 倉庫,去練習(xí)鍛造一個(gè)健壯的 WebSocket 工具庫輪子。

          比如下面這些:

          • 1)socketio/socket.io:43.5k star,不多說,業(yè)界權(quán)威龍頭老大。(不過這實(shí)際上不是一個(gè) WebSocket 庫,而是一個(gè)實(shí)時(shí) pub/sub 框架。簡單地說,Socket.IO 只是包含 WebSocket 功能的一個(gè)框架,如果要使用該庫作為 server 端的服務(wù),則 client 也必須使用該庫,因?yàn)樗皇菢?biāo)準(zhǔn)的 WebSocket 協(xié)議,而是基于 WebSocket 再包裝的消息通信協(xié)議)
          • 2)websockets/ws:9k star,強(qiáng)大易用的 websocket 服務(wù)端、客戶端實(shí)現(xiàn),還有提供很多強(qiáng)大的特性
          • 3)uNetworking/uWebSockets:9.5k star,小巧高性能的 WebSocket實(shí)現(xiàn),C++ 寫的,想更多了解 WebSocket 的底層實(shí)現(xiàn),該庫是不錯的案例。
          • 4)theturtle32/WebSocket-Node:2.3k star,大部分使用 JavaScript,性能關(guān)鍵部分使用 C++ node-gyp 實(shí)現(xiàn)的庫。其所列的 測試用例 有挺好的參考價(jià)值。

          11、代碼下載

          因無法上傳源碼附件,如有需要,請從此鏈接下載:http://www.52im.net/thread-3175-1-1.html)

          12、參考資料

          [1]《新手入門貼:史上最全Web端即時(shí)通訊技術(shù)原理詳解

          [2]《Web端即時(shí)通訊技術(shù)盤點(diǎn):短輪詢、Comet、Websocket、SSE

          [3]《SSE技術(shù)詳解:一種全新的HTML5服務(wù)器推送事件技術(shù)

          [4]《Comet技術(shù)詳解:基于HTTP長連接的Web端實(shí)時(shí)通信技術(shù)

          [5]《新手快速入門:WebSocket簡明教程

          [6]《WebSocket詳解(一):初步認(rèn)識WebSocket技術(shù)

          [7]《WebSocket詳解(二):技術(shù)原理、代碼演示和應(yīng)用案例

          [8]《WebSocket詳解(三):深入WebSocket通信協(xié)議細(xì)節(jié)

          [9]《WebSocket詳解(四):刨根問底HTTP與WebSocket的關(guān)系(上篇)

          [10]《WebSocket詳解(五):刨根問底HTTP與WebSocket的關(guān)系(下篇)

          [11]《WebSocket詳解(六):刨根問底WebSocket與Socket的關(guān)系

          [12]《Web端即時(shí)通訊技術(shù)的發(fā)展與WebSocket、Socket.io的技術(shù)實(shí)踐

          [13]《使用WebSocket和SSE技術(shù)實(shí)現(xiàn)Web端消息推送

          [14]《詳解Web端通信方式的演進(jìn):從Ajax、JSONP 到 SSE、Websocket

          [15]《MobileIMSDK-Web的網(wǎng)絡(luò)層框架為何使用的是Socket.io而不是Netty?

          [16]《理論聯(lián)系實(shí)際:從零理解WebSocket的通信原理、協(xié)議格式、安全性

          [17]《微信小程序中如何使用WebSocket實(shí)現(xiàn)長連接(含完整源碼)

          [18]《八問WebSocket協(xié)議:為你快速解答WebSocket熱門疑問

          [19]《Web端即時(shí)通訊實(shí)踐干貨:如何讓你的WebSocket斷網(wǎng)重連更快速?

          [20]《WebSocket從入門到精通,半小時(shí)就夠!

          (本文同步發(fā)布于:http://www.52im.net/thread-3175-1-1.html

          本文將同步發(fā)布于“即時(shí)通訊技術(shù)圈”公眾號,歡迎關(guān)注:

          ▲ 本文在公眾號上的鏈接是:點(diǎn)此進(jìn)入



          作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時(shí)通訊開發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時(shí)是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級移動端即時(shí)通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 元朗区| 美姑县| 大丰市| 灵川县| 桑植县| 开阳县| 连城县| 绥阳县| 天镇县| 黎川县| 通道| 涟水县| 汝南县| 资中县| 大英县| 大理市| 昭通市| 慈利县| 潜江市| 诸暨市| 望谟县| 兴义市| 枝江市| 门头沟区| 桃园县| 称多县| 宿松县| 静乐县| 米易县| 富阳市| 岳普湖县| 花莲县| 莲花县| 陆丰市| 辽阳市| 桂林市| 万宁市| 彝良县| 城固县| 麟游县| 凤山市|