摘要:最終形成服務(wù)器端的握手響應(yīng)必需。出于安全考慮和避免網(wǎng)絡(luò)截獲,客戶端發(fā)送的數(shù)據(jù)幀必須進(jìn)行掩碼處理后才能發(fā)送到服務(wù)器,不論是否是在安全協(xié)議上都要進(jìn)行掩碼處理。服務(wù)器如果沒(méi)有收到掩碼處理的數(shù)據(jù)幀時(shí)應(yīng)該關(guān)閉連接,發(fā)送一個(gè)的狀態(tài)碼。
首先
長(zhǎng)連接:一個(gè)連接上可以連續(xù)發(fā)送多個(gè)數(shù)據(jù)包,在連接期間,如果沒(méi)有數(shù)據(jù)包發(fā)送,需要雙方發(fā)鏈路檢查包。
TCP/IP:TCP/IP屬于傳輸層,主要解決數(shù)據(jù)在網(wǎng)絡(luò)中的傳輸問(wèn)題,只管傳輸數(shù)據(jù)。但是那樣對(duì)傳輸?shù)臄?shù)據(jù)沒(méi)有一個(gè)規(guī)范的封裝、解析等處理,使得傳輸?shù)臄?shù)據(jù)就很難識(shí)別,所以才有了應(yīng)用層協(xié)議對(duì)數(shù)據(jù)的封裝、解析等,如HTTP協(xié)議。
HTTP:HTTP是應(yīng)用層協(xié)議,封裝解析傳輸?shù)臄?shù)據(jù)。
從HTTP1.1開(kāi)始其實(shí)就默認(rèn)開(kāi)啟了長(zhǎng)連接,也就是請(qǐng)求header中看到的Connection:Keep-alive。但是這個(gè)長(zhǎng)連接只是說(shuō)保持了(服務(wù)器可以告訴客戶端保持時(shí)間Keep-Alive:timeout=200;max=20;)這個(gè)TCP通道,直接Request - Response,而不需要再創(chuàng)建一個(gè)連接通道,做到了一個(gè)性能優(yōu)化。但是HTTP通訊本身還是Request - Response。
socket:與HTTP不一樣,socket不是協(xié)議,它是在程序?qū)用嫔蠈?duì)傳輸層協(xié)議(可以主要理解為TCP/IP)的接口封裝。
我們知道傳輸層的協(xié)議,是解決數(shù)據(jù)在網(wǎng)絡(luò)中傳輸?shù)?,那么socket就是傳輸通道兩端的接口。所以對(duì)于前端而言,socket也可以簡(jiǎn)單的理解為對(duì)TCP/IP的抽象協(xié)議。
WebSocket:
WebSocket是包裝成了一個(gè)應(yīng)用層協(xié)議作為socket,從而能夠讓客戶端和遠(yuǎn)程服務(wù)端通過(guò)web建立全雙工通信。websocket提供ws和wss兩種URL方案。協(xié)議英文文檔和中文翻譯
使用WebSocket構(gòu)造函數(shù)創(chuàng)建一個(gè)WebSocket連接,返回一個(gè)websocket實(shí)例。通過(guò)這個(gè)實(shí)例我們可以監(jiān)聽(tīng)事件,這些事件可以知道什么時(shí)候簡(jiǎn)歷連接,什么時(shí)候有消息被推過(guò)來(lái)了,什么時(shí)候發(fā)生錯(cuò)誤了,時(shí)候連接關(guān)閉。我們可以使用node搭建一個(gè)WebSocket服務(wù)器來(lái)看看,源碼。同樣也可以調(diào)用websocket.org網(wǎng)站的demo服務(wù)器http://demos.kaazing.com/echo/。
事件//創(chuàng)建WebSocket實(shí)例,可以使用ws和wss。第二個(gè)參數(shù)可以選填自定義協(xié)議,如果多協(xié)議,可以以數(shù)組方式 var socket = new WebSocket("ws://demos.kaazing.com/echo");
open
服務(wù)器相應(yīng)WebSocket連接請(qǐng)求觸發(fā)
socket.onopen = (event) => { socket.send("Hello Server!"); };
message
服務(wù)器有 響應(yīng)數(shù)據(jù) 觸發(fā)
socket.onmessage = (event) => { debugger; console.log(event.data); };
error
出錯(cuò)時(shí)觸發(fā),并且會(huì)關(guān)閉連接。這時(shí)可以根據(jù)錯(cuò)誤信息進(jìn)行按需處理
socket.onerror = (event) => { console.log("error"); }
close
連接關(guān)閉時(shí)觸發(fā),這在兩端都可以關(guān)閉。另外如果連接失敗也是會(huì)觸發(fā)的。 針對(duì)關(guān)閉一般我們會(huì)做一些異常處理,關(guān)于異常參數(shù): 1. socket.readyState 2 正在關(guān)閉 3 已經(jīng)關(guān)閉 2. event.wasClean [Boolean] true 客戶端或者服務(wù)器端調(diào)用close主動(dòng)關(guān)閉 false 反之 3. event.code [Number] 關(guān)閉連接的狀態(tài)碼。socket.close(code, reason) 4. event.reason [String] 關(guān)閉連接的原因。socket.close(code, reason) socket.onclose = (event) => { debugger; }方法
send
send(data) 發(fā)送方法
data 可以是String/Blob/ArrayBuffer/ByteBuffer等
需要注意,使用send發(fā)送數(shù)據(jù),必須是連接建立之后。一般會(huì)在onopen事件觸發(fā)后發(fā)送:
socket.onopen = (event) => { socket.send("Hello Server!"); };
如果是需要去響應(yīng)別的事件再發(fā)送消息,也就是將WebSocket實(shí)例socket交給別的方法使用,因?yàn)樵诎l(fā)送時(shí)你不一定知道socket是否還連接著,所以可以檢查readyState屬性的值是否等于OPEN常量,也就是查看socket是否還連接著。
btn.onclick = function startSocket(){ //判斷是否連接是否還存在 if(socket.readyState == WebSocket.OPEN){ var message = document.getElementById("message").value; if(message != "") socket.send(message); } }
close
使用close([code[,reason]])方法可以關(guān)閉連接。code和reason均為選填
// 正常關(guān)閉 socket.close(1000, "closing normally");常量
常量名 | 值 | 描述 |
---|---|---|
CONNECTING | 0 | 連接還未開(kāi)啟 |
OPEN | 1 | 連接開(kāi)啟可以通信 |
CLOSING | 2 | 連接正在關(guān)閉中 |
CLOSED | 3 | 連接已經(jīng)關(guān)閉 |
屬性名 | 值類型 | 描述 |
---|---|---|
binaryType | String | 表示連接傳輸?shù)亩M(jìn)制數(shù)據(jù)類型的字符串。默認(rèn)為"blob"。 |
bufferedAmount | Number | 只讀。如果使用send()方法發(fā)送的數(shù)據(jù)過(guò)大,雖然send()方法會(huì)馬上執(zhí)行,但數(shù)據(jù)并不是馬上傳輸。瀏覽器會(huì)緩存應(yīng)用流出的數(shù)據(jù),你可以使用bufferedAmount屬性檢查已經(jīng)進(jìn)入隊(duì)列但還未被傳輸?shù)臄?shù)據(jù)大小。在一定程度上可以避免網(wǎng)絡(luò)飽和。 |
protocol | String/Array | 在構(gòu)造函數(shù)中,protocol參數(shù)讓服務(wù)端知道客戶端使用的WebSocket協(xié)議。而在實(shí)例socket中就是連接建立前為空,連接建立后為客戶端和服務(wù)器端確定下來(lái)的協(xié)議名稱。 |
readyState | String | 只讀。連接當(dāng)前狀態(tài),這些狀態(tài)是與常量相對(duì)應(yīng)的。 |
extensions | String | 服務(wù)器選擇的擴(kuò)展。目前,這只是一個(gè)空字符串或通過(guò)連接協(xié)商的擴(kuò)展列表。 |
WebSocket 協(xié)議有兩部分:握手、數(shù)據(jù)傳輸。
其中,握手無(wú)疑是關(guān)鍵,是一切的先決條件。
握手
客戶端握手請(qǐng)求
//創(chuàng)建WebSocket實(shí)例,可以使用ws和wss。第二個(gè)參數(shù)可以選填自定義協(xié)議,如果多協(xié)議,可以以數(shù)組方式 var socket = new WebSocket("ws://localhost:8081", [protocol]);
出于WebSocket的產(chǎn)生原因是為了瀏覽器能實(shí)現(xiàn)同服務(wù)器的全雙工通信和HTTP協(xié)議在瀏覽器端的廣泛運(yùn)用(當(dāng)然也不全是為了瀏覽器,但是主要還是針對(duì)瀏覽器的)。所以WebSocket的握手是HTTP請(qǐng)求的升級(jí)。
WebSocket客戶端請(qǐng)求頭示例:
GET /chat HTTP/1.1 //必需。 Host: server.example.com // 必需。WebSocket服務(wù)器主機(jī)名 Upgrade: websocket // 必需。并且值為" websocket"。有個(gè)空格 Connection: Upgrade // 必需。并且值為" Upgrade"。有個(gè)空格 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 必需。其值采用base64編碼的隨機(jī)16字節(jié)長(zhǎng)的字符序列。 Origin: http://example.com //瀏覽器必填。頭域(RFC6454)用于保護(hù)WebSocket服務(wù)器不被未授權(quán)的運(yùn)行在瀏覽器的腳本跨源使用WebSocket API。 Sec-WebSocket-Protocol: chat, superchat //選填??捎眠x項(xiàng)有子協(xié)議選擇器。 Sec-WebSocket-Version: 13 //必需。版本。
WebSocket客戶端將上述請(qǐng)求發(fā)送到服務(wù)器。如果是調(diào)用瀏覽器的WebSocket API,瀏覽器會(huì)自動(dòng)完成完成上述請(qǐng)求頭。
服務(wù)端握手響應(yīng)
服務(wù)器得向客戶端證明它接收到了客戶端的WebSocket握手,為使服務(wù)器不接受非WebSocket連接,防止攻擊者通過(guò)XMLHttpRequest發(fā)送或表單提交精心構(gòu)造的包來(lái)欺騙WebSocket服務(wù)器。服務(wù)器把兩塊信息合并來(lái)形成響應(yīng)。第一塊信息來(lái)自客戶端握手頭域Sec-WebSocket-Key,如Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==。
對(duì)于這個(gè)頭域,服務(wù)器取頭域的值(需要先消除空白符),以字符串的形式拼接全局唯一的(GUID,[RFC4122])標(biāo)識(shí):258EAFA5-E914-47DA-95CA-C5AB0DC85B11,此值不大可能被不明白WebSocket協(xié)議的網(wǎng)絡(luò)終端使用。然后進(jìn)行SHA-1 hash(160位)編碼,再進(jìn)行base64編碼,將結(jié)果作為服務(wù)器的握手返回。具體如下:
請(qǐng)求頭:Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ== 取值,字符串拼接后得到:"dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; SHA-1后得到: 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb20xbe 0xc4 0xea Base64后得到: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 最后的結(jié)果值作為響應(yīng)頭Sec-WebSocket-Accept 的值。
最終形成WebSocket服務(wù)器端的握手響應(yīng):
HTTP/1.1 101 Switching Protocols //必需。響應(yīng)頭。狀態(tài)碼為101。任何非101的響應(yīng)都為握手未完成。但是HTTP語(yǔ)義是存在的。 Upgrade: websocket // 必需。升級(jí)類型。 Connection: Upgrade //必需。本次連接類型為升級(jí)。 Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo= //必需。表明服務(wù)器是否愿意接受連接。如果接受,值就必須是通過(guò)上面算法得到的值。
當(dāng)然響應(yīng)頭還存在一些可選字段。主要的可選字段為Sec-WebSocket-Protocol,是對(duì)客戶端請(qǐng)求中所提供的Sec-WebSocket-Protocol子協(xié)議的選擇結(jié)果的響應(yīng)。當(dāng)然cookie什么的也是可以的。
//handshaking.js const crypto = require("crypto"); const cryptoKey = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 計(jì)算握手響應(yīng)accept-key let challenge = (reqKey) => { reqKey += cryptoKey; // crypto.vetHashes()可以獲得支持的hash算法數(shù)組,我這里得到46個(gè) reqKey = reqKey.replace(/s/g,""); // crypto.createHash("sha1").update(reqKey).digest()得到的是一個(gè)Uint8Array的加密數(shù)據(jù),需要將其轉(zhuǎn)為base64 return crypto.createHash("sha1").update(reqKey).digest().toString("base64"); } exports.handshaking = (req, socket, head) => { let _headers = req.headers, _key = _headers["sec-websocket-key"], resHeaders = [], br = " "; resHeaders.push( "HTTP/1.1 101 WebSocket Protocol Handshake is OK", "Upgrade: websocket", "Connection: Upgrade", "Sec-WebSocket-Origin: " + _headers.origin, "Sec-WebSocket-Location: ws://" + _headers.host + req.url, ); let resAccept = challenge(_key); resHeaders.push("Sec-WebSocket-Accept: "+ resAccept + br, head); socket.write(resHeaders.join(br), "binary"); }
握手關(guān)閉
關(guān)閉握手可用使用TCP直接關(guān)閉連接的方法來(lái)關(guān)閉握手。但是TCP關(guān)閉握手不總是端到端可靠的,特別是出現(xiàn)攔截代理和其他的中間設(shè)施。也可以任何一端發(fā)送帶有指定控制序號(hào)(比如說(shuō)狀態(tài)碼1002,協(xié)議錯(cuò)誤)的數(shù)據(jù)的幀來(lái)開(kāi)始關(guān)閉握手,當(dāng)另一方接收到這個(gè)關(guān)閉幀,就必須關(guān)閉連接。
數(shù)據(jù)傳輸在WebSocket協(xié)議中,數(shù)據(jù)傳輸階段使用frame(數(shù)據(jù)幀)進(jìn)行通信,frame分不同的類型,主要有:文本數(shù)據(jù),二進(jìn)制數(shù)據(jù)。出于安全考慮和避免網(wǎng)絡(luò)截獲,客戶端發(fā)送的數(shù)據(jù)幀必須進(jìn)行掩碼處理后才能發(fā)送到服務(wù)器,不論是否是在TLS安全協(xié)議上都要進(jìn)行掩碼處理。服務(wù)器如果沒(méi)有收到掩碼處理的數(shù)據(jù)幀時(shí)應(yīng)該關(guān)閉連接,發(fā)送一個(gè)1002的狀態(tài)碼。服務(wù)器不能將發(fā)送到客戶端的數(shù)據(jù)進(jìn)行掩碼處理,如果客戶端收到掩碼處理的數(shù)據(jù)幀必須關(guān)閉連接。
那我們服務(wù)器端接收到的數(shù)據(jù)幀是怎樣的呢?
數(shù)據(jù)幀
WebSocket的數(shù)據(jù)傳輸是要遵循特定的數(shù)據(jù)格式-數(shù)據(jù)幀(frame).
每一列代表一個(gè)字節(jié),一個(gè)字節(jié)8位,每一位又代表一個(gè)二進(jìn)制數(shù)。
fin: 標(biāo)識(shí)這一幀數(shù)據(jù)是否是該分塊的最后一幀。
1 為最后一幀 0 不是最后一幀。需要分為多個(gè)幀傳輸
rsv1-3: 默認(rèn)為0.接收協(xié)商擴(kuò)展定義為非0設(shè)定。
opcode: 操作碼,也就是定義了該數(shù)據(jù)是什么,如果不為定義內(nèi)的值則連接中斷。占四個(gè)位,可以表示0~15的十進(jìn)制,或者一個(gè)十六進(jìn)制。
%x0 表示一個(gè)繼續(xù)幀 %x1 表示一個(gè)文本幀 %x2 表示一個(gè)二進(jìn)制幀 %x3-7 為以后的非控制幀保留 %x8 表示一個(gè)連接關(guān)閉 %x9 表示一個(gè)ping %x10 表示一個(gè)pong %x11-15 為以后的控制幀保留
masked: 占第二個(gè)字節(jié)的一位,定義了masking-key是否存在。并且使用masking-key掩碼解析Payload data。
1 客戶端發(fā)送數(shù)據(jù)到服務(wù)端 0 服務(wù)端發(fā)送數(shù)據(jù)到客戶端
payload length: 表示Payload data的總長(zhǎng)度。占7位,或者7+2個(gè)字節(jié)、或者7+8個(gè)字節(jié)。
0-125,則是payload的真實(shí)長(zhǎng)度 126,則后面2個(gè)字節(jié)形成的16位無(wú)符號(hào)整型數(shù)的值是payload的真實(shí)長(zhǎng)度,125<數(shù)據(jù)長(zhǎng)度<65535 127,則后面8個(gè)字節(jié)形成的64位無(wú)符號(hào)整型數(shù)的值是payload的真實(shí)長(zhǎng)度,數(shù)據(jù)長(zhǎng)度>65535
masking key: 0或4字節(jié),當(dāng)masked為1的時(shí)候才存在,為4個(gè)字節(jié),否則為0,用于對(duì)我們需要的數(shù)據(jù)進(jìn)行解密
payload data: 我們需要的數(shù)據(jù),如果masked為1,該數(shù)據(jù)會(huì)被加密,要通過(guò)masking key進(jìn)行異或運(yùn)算解密才能獲取到真實(shí)數(shù)據(jù)。
關(guān)于數(shù)據(jù)幀
因?yàn)閃ebSocket服務(wù)端接收到的數(shù)據(jù)有可能是連續(xù)的數(shù)據(jù)幀,一個(gè)message可能分為多個(gè)幀發(fā)送。但如果使用fin來(lái)做消息邊界是有問(wèn)題的。
我發(fā)送了一個(gè)27378個(gè)字節(jié)的字符串,服務(wù)器端共接收到2幀,兩幀的fin都為1,而且根據(jù)規(guī)范計(jì)算出來(lái)的兩幀的payload data的長(zhǎng)度為27372少了6個(gè)字節(jié)。這缺少的6個(gè)字節(jié)其實(shí)剛好等于2個(gè)固有字節(jié)加上maskingKey的4個(gè)字節(jié),也就是說(shuō)第二幀就是一個(gè)純粹的數(shù)據(jù)幀。這又是怎么回事呢??
從結(jié)果推測(cè)實(shí)現(xiàn),我們接收到的第2幀的數(shù)據(jù)格式不是幀格式,說(shuō)明數(shù)據(jù)沒(méi)有先分幀(分片)后再發(fā)送的。而是將一幀分包后發(fā)送的。
分片
分片的主要目的是允許當(dāng)消息開(kāi)始但不必緩沖該消息時(shí)發(fā)送一個(gè)未知大小的消息。如果消息不能被分片,那么端點(diǎn)將不得不緩沖整個(gè)消息以便在首字節(jié)發(fā)生之前統(tǒng)計(jì)出它的長(zhǎng)度。對(duì)于分片,服務(wù)器或中間件可以選擇一個(gè)合適大小的緩沖,當(dāng)緩沖滿時(shí),寫一個(gè)片段到網(wǎng)絡(luò)。
我們27378個(gè)字節(jié)的消息明顯是知道m(xù)essage長(zhǎng)度,那么就算這個(gè)message很大,根據(jù)規(guī)范1幀的數(shù)據(jù)長(zhǎng)度理論上是0<數(shù)據(jù)長(zhǎng)度<65535的,這種情況下應(yīng)該1幀搞定,他也只是當(dāng)做一幀來(lái)發(fā)送,但是由于傳輸限制,所以這一個(gè)幀(我們收到的像是好幾幀一樣)會(huì)被拆分成幾塊發(fā)送,除了第一塊是帶有fin、opcode、masked等標(biāo)識(shí)符,之后收到的塊都是純粹的數(shù)據(jù)(也就是第一塊的payload data 的后續(xù)部分),這個(gè)就是socket的將WebSocket分好的一幀數(shù)據(jù)進(jìn)行了分包發(fā)送。那么這種一幀被socket分包發(fā)送,導(dǎo)致像是分幀(分片)發(fā)送的情況(服務(wù)器端本應(yīng)該只就收一幀),在服務(wù)器端我暫時(shí)還沒(méi)有想到怎樣獲取狀態(tài)來(lái)處理。
總結(jié),客戶端發(fā)送數(shù)據(jù),在實(shí)現(xiàn)時(shí)還是需要手動(dòng)進(jìn)行分幀(分片),不然就按照一幀發(fā)送,小數(shù)據(jù)量無(wú)所謂;如果是大數(shù)據(jù)量,就會(huì)被socket自動(dòng)分包發(fā)送。這個(gè)與WebSocket協(xié)議規(guī)范所標(biāo)榜的自動(dòng)分幀(分片),存在的差異應(yīng)該是各個(gè)瀏覽器在對(duì)WebSocket協(xié)議規(guī)范的實(shí)現(xiàn)上偷工減料所造成的。所以我們看見(jiàn)socket.io等插件會(huì)有一個(gè)客戶端接口,應(yīng)該就是為了重新是實(shí)現(xiàn)WebSocket協(xié)議規(guī)范。從原理出發(fā),我們接下來(lái)還是以小數(shù)據(jù)量(單幀)數(shù)據(jù)傳輸為例了。
解析數(shù)據(jù)幀
//dataHandler.js // 收集本次message的所有數(shù)據(jù) getData(data, callback) { this.getState(data); // 如果狀態(tài)碼為8說(shuō)明要關(guān)閉連接 if(this.state.opcode == 8) { this.OPEN = false; this.closeSocket(); return; } // 如果是心跳pong,回一個(gè)ping if(this.state.opcode == 10) { this.OPEN = true; this.pingTimes = 0;// 回了pong就將次數(shù)清零 return; } // 收集本次數(shù)據(jù)流數(shù)據(jù) this.dataList.push(this.state.payloadData); // 長(zhǎng)度為0,說(shuō)明當(dāng)前幀位最后一幀。 if(this.state.remains == 0){ let buf = Buffer.concat(this.dataList, this.state.payloadLength); //使用掩碼maskingKey解析所有數(shù)據(jù) let result = this.parseData(buf); // 數(shù)據(jù)接收完成后回調(diào)回業(yè)務(wù)函數(shù) callback(this.socket, result); //重置狀態(tài),表示當(dāng)前message已經(jīng)解析完成了 this.resetState(); }else{ this.state.index++; } } // 收集本次message的所有數(shù)據(jù) getData(data, callback) { this.getState(data); // 收集本次數(shù)據(jù)流數(shù)據(jù) this.dataList.push(this.state.payloadData); // 長(zhǎng)度為0,說(shuō)明當(dāng)前幀位最后一幀。 if(this.state.remains == 0){ let buf = Buffer.concat(this.dataList, this.state.payloadLength); //使用掩碼maskingKey解析所有數(shù)據(jù) let result = this.parseData(buf); // 數(shù)據(jù)接收完成后回調(diào)回業(yè)務(wù)函數(shù) callback(this.socket, result); //重置狀態(tài),表示當(dāng)前message已經(jīng)解析完成了 this.resetState(); }else{ this.state.index++; } } // 解析本次message所有數(shù)據(jù) parseData(allData, callback){ let len = allData.length, i = 0;
for(; i < len; i++){ allData[i] = allData[i] ^ this.state.maskingKey[ i % 4 ];// 異或運(yùn)算,使用maskingKey四個(gè)字節(jié)輪流進(jìn)行計(jì)算 } // 判斷數(shù)據(jù)類型,如果為文本類型 if(this.state.opcode == 1) allData = allData.toString(); return allData; }
組裝需要發(fā)送的數(shù)據(jù)幀
// 組裝數(shù)據(jù)幀,發(fā)送是不需要掩碼加密 createData(data){ let dataType = Buffer.isBuffer(data);// 數(shù)據(jù)類型 let dataBuf, // 需要發(fā)送的二進(jìn)制數(shù)據(jù) dataLength,// 數(shù)據(jù)真實(shí)長(zhǎng)度 dataIndex = 2; // 數(shù)據(jù)的起始長(zhǎng)度 let frame; // 數(shù)據(jù)幀 if(dataType) dataBuf = data; else dataBuf = Buffer.from(data); // 也可以不做類型判斷,直接Buffer.form(data) dataLength = dataBuf.byteLength; // 計(jì)算payload data在frame中的起始位置 dataIndex = dataIndex + (dataLength > 65535 ? 8 : (dataLength > 125 ? 2 : 0)); frame = new Buffer.alloc(dataIndex + dataLength); //第一個(gè)字節(jié),fin = 1,opcode = 1 frame[0] = parseInt(10000001, 2); //長(zhǎng)度超過(guò)65535的則由8個(gè)字節(jié)表示,因?yàn)?個(gè)字節(jié)能表達(dá)的長(zhǎng)度為4294967295,已經(jīng)完全夠用,因此直接將前面4個(gè)字節(jié)置0 if(dataLength > 65535){ frame[1] = 127; //第二個(gè)字節(jié) frame.writeUInt32BE(0, 2); frame.writeUInt32BE(dataLength, 6); }else if(dataLength > 125){ frame[1] = 126; frame.writeUInt16BE(dataLength, 2); }else{ frame[1] = dataLength; } // 服務(wù)端發(fā)送到客戶端的數(shù)據(jù) frame.write(dataBuf.toString(), dataIndex); return frame; }
心跳檢測(cè)
// 心跳檢查 sendCheckPing(){ let _this = this; let timer = setTimeout(() => { clearTimeout(timer); if (_this.pingTimes >= 3) { _this.closeSocket(); return; } //記錄心跳次數(shù) _this.pingTimes++; if(_this.pingTimes == 100000) _this.pingTimes = 0; _this.sendCheckPing(); }, 5000); } // 發(fā)送心跳ping sendPing() { let ping = Buffer.alloc(2); ping[0] = parseInt(10001001, 2); ping[1] = 0; this.writeData(ping); }關(guān)閉連接
客戶端直接調(diào)用close方法,服務(wù)器端可以使用socket.end方法。
最后WebSocket在一定程度上讓前端更加的有所作為,這個(gè)無(wú)疑是令人欣喜的,但是其規(guī)范中的很多不確定也是令人很惋惜的。
因?yàn)闉g覽器對(duì)WebSocket規(guī)范的不完全實(shí)現(xiàn),還有很多需要做的優(yōu)化,這篇文章只是實(shí)現(xiàn)以一下WebSocket,關(guān)于期間很多的安全、穩(wěn)定等方面的需要在應(yīng)用中進(jìn)行充實(shí)。當(dāng)然是用socket.io這種相對(duì)成熟的插件也是不錯(cuò)的選擇。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/90246.html
摘要:為了更加高效的網(wǎng)絡(luò)層,它需要不僅僅只是扮演套接字管理員的角色。用套接字池來(lái)組織套接字,以源來(lái)分組套接字,每個(gè)套接字池強(qiáng)制限制其連接數(shù)和安全約束。協(xié)商是一個(gè)為計(jì)算機(jī)網(wǎng)絡(luò)提供通信安全的加密協(xié)議。 原文請(qǐng)查閱這里,略有改動(dòng),本文采用知識(shí)共享署名 4.0 國(guó)際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原理的第十二章...
摘要:為了更加高效的網(wǎng)絡(luò)層,它需要不僅僅只是扮演套接字管理員的角色。用套接字池來(lái)組織套接字,以源來(lái)分組套接字,每個(gè)套接字池強(qiáng)制限制其連接數(shù)和安全約束。協(xié)商是一個(gè)為計(jì)算機(jī)網(wǎng)絡(luò)提供通信安全的加密協(xié)議。 原文請(qǐng)查閱這里,略有改動(dòng),本文采用知識(shí)共享署名 4.0 國(guó)際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原理的第十二章...
摘要:為了更加高效的網(wǎng)絡(luò)層,它需要不僅僅只是扮演套接字管理員的角色。用套接字池來(lái)組織套接字,以源來(lái)分組套接字,每個(gè)套接字池強(qiáng)制限制其連接數(shù)和安全約束。協(xié)商是一個(gè)為計(jì)算機(jī)網(wǎng)絡(luò)提供通信安全的加密協(xié)議。 原文請(qǐng)查閱這里,略有改動(dòng),本文采用知識(shí)共享署名 4.0 國(guó)際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原理的第十二章...
摘要:預(yù)備工作序最近正在研究相關(guān)的知識(shí),想著如何能自己實(shí)現(xiàn)協(xié)議。監(jiān)聽(tīng)事件就是協(xié)議的抽象,直接在上面監(jiān)聽(tīng)已有的事件和事件這兩個(gè)事件。表示當(dāng)前數(shù)據(jù)幀為消息的最后一個(gè)數(shù)據(jù)幀,此時(shí)接收方已經(jīng)收到完整的消息,可以對(duì)消息進(jìn)行處理。 A、預(yù)備工作 1、序 最近正在研究 Websocket 相關(guān)的知識(shí),想著如何能自己實(shí)現(xiàn) Websocket 協(xié)議。到網(wǎng)上搜羅了一番資料后用 Node.js 實(shí)現(xiàn)該協(xié)議,倒也沒(méi)...
閱讀 3494·2021-11-12 10:36
閱讀 2874·2021-09-22 15:35
閱讀 2824·2021-09-04 16:41
閱讀 1173·2019-08-30 15:55
閱讀 3584·2019-08-29 18:43
閱讀 2079·2019-08-23 18:24
閱讀 1424·2019-08-23 18:10
閱讀 1927·2019-08-23 11:31