摘要:一協(xié)議概述協(xié)議允許不受信用的客戶端代碼在可控的網(wǎng)絡(luò)環(huán)境中控制遠程主機。該協(xié)議包含一個握手和一個基本消息分幀分層通過。該協(xié)議包括兩個方面,握手鏈接和數(shù)據(jù)傳輸。二注意事項很多人可能只是到,但事實上協(xié)議地址是可以加和的。
本文同步自我的博客園:http://hustskyking.cnblogs.com
P.S:文章代碼格式錯亂,也不知道是什么原因,還望@segmentFault的兄弟看下~
在上一篇提高到了 web 通信的各種方式,包括 輪詢、長連接 以及各種 HTML5 中提到的手段。本文將詳細描述 WebSocket協(xié)議 在 web通訊 中的實現(xiàn)。
一、WebSocket 協(xié)議 1. 概述websocket協(xié)議允許不受信用的客戶端代碼在可控的網(wǎng)絡(luò)環(huán)境中控制遠程主機。該協(xié)議包含一個握手和一個基本消息分幀、分層通過TCP。簡單點說,通過握手應(yīng)答之后,建立安全的信息管道,這種方式明顯優(yōu)于前文所說的基于 XMLHttpRequest 的 iframe 數(shù)據(jù)流和長輪詢。該協(xié)議包括兩個方面,握手鏈接(handshake)和數(shù)據(jù)傳輸(data transfer)。
2. 握手連接這部分比較簡單,就像路上遇到熟人問好。
Client:嘿,大哥,有火沒?(煙遞了過去) Server:哈,有啊,來~ (點上) Client:火柴啊,也行!(煙點上,驗證完畢)
握手連接中,client 先主動伸手:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
客戶端發(fā)了一串 Base64 加密的密鑰過去,也就是上面你看到的 Sec-WebSocket-Key。
Server 看到 Client 打招呼之后,悄悄地告訴 Client 他已經(jīng)知道了,順便也打個招呼。
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
Server 返回了 Sec-WebSocket-Accept 這個應(yīng)答,這個應(yīng)答內(nèi)容是通過一定的方式生成的。生成算法是:
mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 這是算法中要用到的固定字符串 accept = base64( sha1( key + mask ) );
key 和 mask 串接之后經(jīng)過 SHA-1 處理,處理后的數(shù)據(jù)再經(jīng)過一次 Base64 加密。分解動作:
1. t = "GhlIHNhbXBsZSBub25jZQ==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" -> "GhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 2. s = sha1(t) -> 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea 3. base64(s) -> "s3pPLMBiTxaQ9kYGzzhZRbK"
上面 Server 端返回的 HTTP 狀態(tài)碼是 101,如果不是 101 ,那就說明握手一開始就失敗了~
下面就來個 demo,跟服務(wù)器握個手:
var crypto = require("crypto"); require("net").createServer(function(o){ var key; o.on("data",function(e){ if(!key){ // 握手 // 應(yīng)答部分,代碼先省略 console.log(e.toString()); }else{ }; }); }).listen(8000);
客戶端代碼:
var ws=new WebSocket("ws://127.0.0.1:8000"); ws.onerror=function(e){ console.log(e); };
上面當然是一串不完整的代碼,目的是演示握手過程中,客戶端給服務(wù)端打招呼。在控制臺我們可以看到:
看起來很熟悉吧,其實就是發(fā)送了一個 HTTP 請求,這個我們在瀏覽器的 Network 中也可以看到:
但是 WebSocket協(xié)議 并不是 HTTP 協(xié)議,剛開始驗證的時候借用了 HTTP 的頭,連接成功之后的通信就不是 HTTP 了,不信你用 fiddler2 抓包試試,肯定是拿不到的,后面的通信部分是基于 TCP 的連接。
服務(wù)器要成功的進行通信,必須有應(yīng)答,往下看:
//服務(wù)器程序 var crypto = require("crypto"); var WS = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; require("net").createServer(function(o){ var key; o.on("data",function(e){ if(!key){ //握手 key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1]; key = crypto.createHash("sha1").update(key + WS).digest("base64"); o.write("HTTP/1.1 101 Switching Protocols "); o.write("Upgrade: websocket "); o.write("Connection: Upgrade "); o.write("Sec-WebSocket-Accept: " + key + " "); o.write(" "); }else{ console.log(e); }; }); }).listen(8000);
關(guān)于crypto模塊,可以看看官方文檔,上面的代碼應(yīng)該是很好理解的,服務(wù)器應(yīng)答之后,Client 拿到 Sec-WebSocket-Accept ,然后本地做一次驗證,如果驗證通過了,就會觸發(fā) onopen 函數(shù)。
//客戶端程序 var ws=new WebSocket("ws://127.0.0.1:8000/"); ws.onopen=function(e){ console.log("握手成功"); };
可以看到
官方文檔提供了一個結(jié)構(gòu)圖
0 1 2 3 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 ... | +---------------------------------------------------------------+
第一眼瞟到這張圖恐怕是要吐血,如果大學修改計算機網(wǎng)絡(luò)這門課應(yīng)該不會對這東西陌生,數(shù)據(jù)傳輸協(xié)議嘛,是需要定義字節(jié)長度及相關(guān)含義的。
FIN 1bit 表示信息的最后一幀,flag,也就是標記符 RSV 1-3 1bit each 以后備用的 默認都為 0 Opcode 4bit 幀類型,稍后細說 Mask 1bit 掩碼,是否加密數(shù)據(jù),默認必須置為1 (這里很蛋疼) Payload 7bit 數(shù)據(jù)的長度 Masking-key 1 or 4 bit 掩碼 Payload data (x + y) bytes 數(shù)據(jù) Extension data x bytes 擴展數(shù)據(jù) Application data y bytes 程序數(shù)據(jù)
每一幀的傳輸都是遵從這個協(xié)議規(guī)則的,知道了這個協(xié)議,那么解析就不會太難了,這里我就直接拿了次碳酸鈷同學的代碼。
4. 數(shù)據(jù)幀的解析和編碼數(shù)據(jù)幀的解析代碼:
function decodeDataFrame(e){ var i=0,j,s,frame={ //解析前兩個字節(jié)的基本數(shù)據(jù) FIN:e[i]>>7,Opcode:e[i++]&15,Mask:e[i]>>7, PayloadLength:e[i++]&0x7F }; //處理特殊長度126和127 if(frame.PayloadLength==126) frame.length=(e[i++]<<8)+e[i++]; if(frame.PayloadLength==127) i+=4, //長度一般用四字節(jié)的整型,前四個字節(jié)通常為長整形留空的 frame.length=(e[i++]<<24)+(e[i++]<<16)+(e[i++]<<8)+e[i++]; //判斷是否使用掩碼 if(frame.Mask){ //獲取掩碼實體 frame.MaskingKey=[e[i++],e[i++],e[i++],e[i++]]; //對數(shù)據(jù)和掩碼做異或運算 for(j=0,s=[];j數(shù)據(jù)幀的編碼:
//NodeJS function encodeDataFrame(e){ var s=[],o=new Buffer(e.PayloadData),l=o.length; //輸入第一個字節(jié) s.push((e.FIN<<7)+e.Opcode); //輸入第二個字節(jié),判斷它的長度并放入相應(yīng)的后續(xù)長度消息 //永遠不使用掩碼 if(l<126) s.push(l); else if(l<0x10000) s.push(126,(l&0xFF00)>>2,l&0xFF); else s.push( 127, 0,0,0,0, //8字節(jié)數(shù)據(jù),前4字節(jié)一般沒用留空 (l&0xFF000000)>>6,(l&0xFF0000)>>4,(l&0xFF00)>>2,l&0xFF ); //返回頭部分和數(shù)據(jù)部分的合并緩沖區(qū) return Buffer.concat([new Buffer(s),o]); }有些童鞋可能沒有明白,應(yīng)該解析哪些數(shù)據(jù)。這的解析任務(wù)主要是服務(wù)端處理,客戶端送過去的數(shù)據(jù)是二進制流形式的,比如:
var ws = new WebSocket("ws://127.0.0.1:8000/"); ws.onopen = function(){ ws.send("握手成功"); };Server 收到的信息是這樣的:
一個放在Buffer格式的二進制流。而當我們輸出的時候解析這個二進制流://服務(wù)器程序 var crypto = require("crypto"); var WS = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; require("net").createServer(function(o){ var key; o.on("data",function(e){ if(!key){ //握手 key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1]; key = crypto.createHash("sha1").update(key + WS).digest("base64"); o.write("HTTP/1.1 101 Switching Protocols "); o.write("Upgrade: websocket "); o.write("Connection: Upgrade "); o.write("Sec-WebSocket-Accept: " + key + " "); o.write(" "); }else{ // 輸出之前解析幀 console.log(decodeDataFrame(e)); }; }); }).listen(8000);那輸出的就是一個幀信息十分清晰的對象了:
5. 連接的控制上面我買了個關(guān)子,提到的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 | -+--------+-------------------------------------+-----------|次碳酸鈷給出的解析函數(shù),得到的數(shù)據(jù)格式是:
{ FIN: 1, Opcode: 1, Mask: 1, PayloadLength: 4, MaskingKey: [ 159, 18, 207, 93 ], PayLoadData: "test" }那么可以對應(yīng)上面查看,此幀的作用就是發(fā)送文本,為文本幀。因為連接是基于 TCP 的,直接關(guān)閉 TCP 連接,這個通道就關(guān)閉了,不過 WebSocket 設(shè)計的還比較人性化,關(guān)閉之前還跟你打一聲招呼,在服務(wù)器端,可以判斷frame的Opcode:
var frame=decodeDataFrame(e); console.log(frame); if(frame.Opcode==8){ o.end(); //斷開連接 }客戶端和服務(wù)端交互的數(shù)據(jù)(幀)格式都是一樣的,只要客戶端發(fā)送 ws.close(), 服務(wù)器就會執(zhí)行上面的操作。相反,如果服務(wù)器給客戶端也發(fā)送同樣的關(guān)閉幀(close frame):
o.write(encodeDataFrame({ FIN:1, Opcode:8, PayloadData:buf }));客戶端就會相應(yīng) onclose 函數(shù),這樣的交互還算是有規(guī)有矩,不容易出錯。
二、注意事項 1. WebSocket URIs很多人可能只是到 ws://text.com:8888,但事實上 websocket 協(xié)議地址是可以加 path 和 query 的。
ws-URI = "ws:" "http://" host [ ":" port ] path [ "?" query ] wss-URI = "wss:" "http://" host [ ":" port ] path [ "?" query ]如果使用的是 wss 協(xié)議,那么 URI 將會以安全方式連接。 這里的 wss 大小寫不敏感。
2. 協(xié)議中多余的部分(吐槽)握手請求中包含Sec-WebSocket-Key字段,明眼人一下就能看出來是websocket連接,而且這個字段的加密方式在服務(wù)器也是固定的,如果別人想黑你,不會太難。
再就是那個mask掩碼,既然強制加密了,還有必要讓開發(fā)者處理這個東西么?直接封裝到內(nèi)部不就行了?
3. 與 TCP 和 HTTP 之間的關(guān)系WebSocket協(xié)議是一個基于TCP的協(xié)議,就是握手鏈接的時候跟HTTP相關(guān)(發(fā)了一個HTTP請求),這個請求被Server切換到(Upgrade)websocket協(xié)議了。websocket把 80 端口作為默認websocket連接端口,而websocket的運行使用的是443端口。
三、參考資料http://tools.ietf.org/html/rfc6455 web standard - The WebSocket Protocol
http://www.w3.org/TR/websockets/ W3.ORG - WebSockets
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/87483.html
摘要:發(fā)送加密返回本地校驗看了我寫的上一篇文章的同學應(yīng)該是對上圖有了比較全面的理解。前文中也提到了請求的格式如上,首先建立一個連接,監(jiān)聽端口的信息。數(shù)據(jù)幀解析代碼本文沒有給出這樣數(shù)據(jù)幀解析代碼,前文中給出了數(shù)據(jù)幀的格式,解析純屬體力活。 本文同步自我的博客園:http://hustskyking.cnblogs.com 下面我畫了一個圖演示 client 和 server 之間建立 web...
摘要:發(fā)送加密返回本地校驗看了我寫的上一篇文章的同學應(yīng)該是對上圖有了比較全面的理解。前文中也提到了請求的格式如上,首先建立一個連接,監(jiān)聽端口的信息。數(shù)據(jù)幀解析代碼本文沒有給出這樣數(shù)據(jù)幀解析代碼,前文中給出了數(shù)據(jù)幀的格式,解析純屬體力活。 本文同步自我的博客園:http://hustskyking.cnblogs.com 下面我畫了一個圖演示 client 和 server 之間建立 web...
摘要:覺得很容易用到從開始支持現(xiàn)在已經(jīng)是了相對看過例子發(fā)現(xiàn)配置其實比較簡單先用模塊寫一個簡單的服務(wù)器然后修改添加比如指向然后是配置然后從瀏覽器控制臺嘗試鏈接或者通過的寫法先是通過建立連接然后通過狀態(tài)碼表示切換協(xié)議在配置里是不清楚具體里邊發(fā)生了什 覺得很容易用到.. Nginx 從 1.3 開始支持 WebSocket, 現(xiàn)在已經(jīng)是 1.4.4 了 相對 HTTP, 看過例子發(fā)現(xiàn)配置其實比較簡...
摘要:一異步編程原理顯然,上面這種方式和銀行取號等待有些類似,只不過銀行取號我們并不知道上一個人需要多久才會完成。下面來探討下中的異步編程原理。 眾所周知,JavaScript 的執(zhí)行環(huán)境是單線程的,所謂的單線程就是一次只能完成一個任務(wù),其任務(wù)的調(diào)度方式就是排隊,這就和火車站洗手間門口的等待一樣,前面的那個人沒有搞定,你就只能站在后面排隊等著。在事件隊列中加一個延時,這樣的問題便可以得到緩解...
摘要:如果你有一個高流量的站點,提高性能的第一步是在你的前面放一個反向代理服務(wù)器。使用在一個已經(jīng)存在的服務(wù)器前做反向代理,作為的一個核心應(yīng)用,已經(jīng)被用于全世界成千上萬的站點中。 如果你的 node 服務(wù)器前面沒有 nginx, 那么你可能做錯了。— Bryan Hughes Node.js 是使用 最流行的語言— JavaScript 構(gòu)建服務(wù)器端應(yīng)用的領(lǐng)先工具 。由于可以同時提供 web ...
閱讀 2831·2023-04-25 20:06
閱讀 1450·2021-08-26 14:15
閱讀 2240·2021-08-12 13:27
閱讀 1775·2019-08-30 15:55
閱讀 3477·2019-08-30 13:20
閱讀 2832·2019-08-29 15:12
閱讀 3336·2019-08-29 15:06
閱讀 2863·2019-08-29 14:13