本文章同時發在 cpper.info。html
Websocket是html5提出的一個協議規範,參考rfc6455。html5
websocket約定了一個通訊的規範,經過一個握手的機制,客戶端(瀏覽器)和服務器(webserver)之間能創建一個相似tcp的鏈接,從而方便c-s之間的通訊。在websocket出現以前,web交互通常是基於http協議的短鏈接或者長鏈接。git
WebSocket是爲解決客戶端與服務端實時通訊而產生的技術。websocket協議本質上是一個基於tcp的協議,是先經過HTTP/HTTPS協議發起一條特殊的http請求進行握手後建立一個用於交換數據的TCP鏈接,此後服務端與客戶端經過此TCP鏈接進行實時通訊。github
注意:此時再也不須要原HTTP協議的參與了。web
之前web server實現推送技術或者即時通信,用的都是輪詢(polling),在特色的時間間隔(好比1秒鐘)由瀏覽器自動發出請求,將服務器的消息主動的拉回來,在這種狀況下,咱們須要不斷的向服務器發送請求,然而HTTP request 的header是很是長的,裏面包含的數據可能只是一個很小的值,這樣會佔用不少的帶寬和服務器資源。ajax
而最比較新的技術去作輪詢的效果是Comet – 用了AJAX。但這種技術雖然可達到全雙工通訊,但依然須要發出請求(reuqest)。算法
WebSocket API最偉大之處在於服務器和客戶端能夠在給定的時間範圍內的任意時刻,相互推送信息。 瀏覽器和服務器只須要要作一個握手的動做,在創建鏈接以後,服務器能夠主動傳送數據給客戶端,客戶端也能夠隨時向服務器發送數據。 此外,服務器與客戶端之間交換的標頭信息很小。api
WebSocket並不限於以Ajax(或XHR)方式通訊,由於Ajax技術須要客戶端發起請求,而WebSocket服務器和客戶端能夠彼此相互推送信息;
所以從服務器角度來講,websocket有如下好處:數組
1996年IETF HTTP工做組發佈了HTTP協議的1.0版本,到如今廣泛使用的版本1.1,HTTP協議經歷了17年的發展。 這種分佈式、無狀態、基於TCP的請求/響應式、在互聯網盛行的今天獲得普遍應用的協議。互聯網從興起到如今,經歷了門戶網站盛行的web1.0時代,然後隨着ajax技術的出現,發展爲web應用盛行的web2.0時代,現在又朝着web3.0的方向邁進。反觀http協議,從版本1.0發展到1.1,除了默認長鏈接以外就是緩存處理、帶寬優化和安全性等方面的不痛不癢的改進。它一直保留着無狀態、請求/響應模式,彷佛曆來沒意識到這應該有所改變。瀏覽器
傳統的web應用要想與服務器交互,必須提交一個表單(form),服務器接收並處理傳來的表單,而後返回全新的頁面,由於先後兩個頁面的數據大部分都是相同的,這個過程傳輸了不少冗餘的數據、浪費了帶寬。因而Ajax技術便應運而生。
Ajax是Asynchronous JavaScript and 的簡稱,由Jesse James Garrett 首先提出。這種技術開創性地容許瀏覽器腳本(JS)發送http請求。Outlook Web Access小組於98年使用,並很快成爲IE4.0的一部分,可是這個技術一直很小衆,直到2005年初,google在他的goole groups、gmail等交互式應用中普遍使用此種技術,才使得Ajax迅速被你們所接受。
Ajax的出現使客戶端與服務器端傳輸數據少了不少,也快了不少,也知足了以豐富用戶體驗爲特色的web2.0時代 初期發展的須要,可是慢慢地也暴露了他的弊端。好比沒法知足即時通訊等富交互式應用的實時更新數據的要求。這種瀏覽器端的小技術畢竟仍是基於http協議,http協議要求的請求/響應的模式也是沒法改變的,除非http協議自己有所改變。
以即時通訊爲表明的web應用程序對數據的Low Latency要求,傳統的基於輪詢的方式已經沒法知足,並且也會帶來很差的用戶體驗。因而一種基於http長鏈接的「服務器推」技術便被hack出來。這種技術被命名爲Comet,這個術語由Dojo Toolkit 的項目主管Alex Russell在博文Comet: Low Latency Data for the Browser首次提出,並沿用下來。
其實,服務器推很早就存在了,在經典的client/server模型中有普遍使用,只是瀏覽器太懶了,並無對這種技術提供很好的支持。可是Ajax的出現使這種技術在瀏覽器上實現成爲可能, google的gmail和gtalk的整合首先使用了這種技術。隨着一些關鍵問題的解決(好比IE的加載顯示問題),很快這種技術獲得了承認,目前已經有不少成熟的開源Comet框架。
如下是典型的Ajax和Comet數據傳輸方式的對比,區別簡單明瞭。典型的Ajax通訊方式也是http協議的經典使用方式,要想取得數據,必須首先發送請求。在Low Latency要求比較高的web應用中,只能增長服務器請求的頻率。Comet則不一樣,客戶端與服務器端保持一個長鏈接,只有客戶端須要的數據更新時,服務器才主動將數據推送給客戶端。
Comet的實現主要有兩種方式:
Iframe是html標記,這個標記的src屬性會保持對指定服務器的長鏈接請求,服務器端則能夠不停地返回數據,相對於第一種方式,這種方式跟傳統的服務器推則更接近。
在第一種方式中,瀏覽器在收到數據後會直接調用JS回調函數,可是這種方式該如何響應數據呢?能夠經過在返回數據中嵌入JS腳本的方式,如「」,服務器端將返回的數據做爲回調函數的參數,瀏覽器在收到數據後就會執行這段JS腳本。
若是說Ajax的出現是互聯網發展的必然,那麼Comet技術的出現則更多透露出一種無奈,僅僅做爲一種hack技術,由於沒有更好的解決方案。Comet解決的問題應該由誰來解決纔是合理的呢?瀏覽器,html標準,仍是http標準?主角應該是誰呢?本質上講,這涉及到數據傳輸方式,http協議應首當其衝,是時候改變一下這個懶惰的協議的請求/響應模式了。
W3C給出了答案,在新一代html標準html5中提供了一種瀏覽器和服務器間進行全雙工通信的網絡技術Websocket。從Websocket草案得知,Websocket是一個全新的、獨立的協議,基於TCP協議,與http協議兼容、卻不會融入http協議,僅僅做爲html5的一部分。因而乎腳本又被賦予了另外一種能力:發起websocket請求。這種方式咱們應該很熟悉,由於Ajax就是這麼作的,所不一樣的是,Ajax發起的是http請求而已。
與http協議不一樣的請求/響應模式不一樣,Websocket在創建鏈接以前有一個Handshake(Opening Handshake)過程,在關閉鏈接前也有一個Handshake(Closing Handshake)過程,創建鏈接以後,雙方便可雙向通訊。
在websocket協議發展過程當中前先後後就出現了多個版本的握手協議,這裏分狀況說明一下:
客戶端請求:
GET /ls HTTP/1.1 Upgrade: WebSocket Connection: Upgrade Host: www.qixing318.com Origin: http://www.qixing318.com
服務器返回:
HTTP/1.1 101 Web Socket Protocol Handshake Upgrade: WebSocket Connection: Upgrade WebSocket-Origin: http://www.qixing318.com WebSocket-Location: ws://www.qixing318.com/ls
基於md5加密方式的握手協議
客戶端請求:
GET /demo HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Key2:
Upgrade: WebSocket
Sec-WebSocket-Key1:
Origin: http://www.qixing318.com
[8-byte security key]
服務端返回:
HTTP/1.1 101 WebSocket Protocol Handshake Upgrade: WebSocket Connection: Upgrade WebSocket-Origin: http://www.qixing318.com WebSocket-Location: ws://example.com/demo [16-byte hash response]
其中 Sec-WebSocket-Key1,Sec-WebSocket-Key2 和 [8-byte security key] 這幾個頭信息是web server用來生成應答信息的來源,依據 draft-hixie-thewebsocketprotocol-76 草案的定義。
web server基於如下的算法來產生正確的應答信息:
1. 逐個字符讀取 Sec-WebSocket-Key1 頭信息中的值,將數值型字符鏈接到一塊兒放到一個臨時字符串裏,同時統計全部空格的數量; 2. 將在第(1)步裏生成的數字字符串轉換成一個整型數字,而後除以第(1)步裏統計出來的空格數量,將獲得的浮點數轉換成整數型; 3. 將第(2)步裏生成的整型值轉換爲符合網絡傳輸的網絡字節數組; 4. 對 Sec-WebSocket-Key2 頭信息一樣進行第(1)到第(3)步的操做,獲得另一個網絡字節數組; 5. 將 [8-byte security key] 和在第(3)、(4)步裏生成的網絡字節數組合併成一個16字節的數組; 6. 對第(5)步生成的字節數組使用MD5算法生成一個哈希值,這個哈希值就做爲安全密鑰返回給客戶端,以代表服務器端獲取了客戶端的請求,贊成建立websocket鏈接
基於sha加密方式的握手協議
也是目前見的最多的一種方式,這裏的版本號目前是須要13以上的版本。
客戶端請求:
GET /ls HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: www.qixing318.com
Sec-WebSocket-Origin: http://www.qixing318.com
Sec-WebSocket-Key: 2SCVXUeP9cTjV+0mWB8J6A==
Sec-WebSocket-Version: 13
服務器返回:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: mLDKNeBNWz6T9SxU+o0Fy/HgeSw=
其中 server就是把客戶端上報的key拼上一段GUID( 「258EAFA5-E914-47DA-95CA-C5AB0DC85B11″),拿這個字符串作SHA-1 hash計算,而後再把獲得的結果經過base64加密,最後再返回給客戶端。
客戶端發起鏈接Handshake請求
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
服務器端響應:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
Sec-WebSocket-Accept
服務器端在接收到的Sec-WebSocket-Key密鑰後追加一段神奇字符串「258EAFA5-E914-47DA-95CA-C5AB0DC85B11」,並將結果進行sha-1哈希,而後再進行base64加密返回給客戶端(就是Sec-WebSocket-Key)。 好比:
function encry($req) { $key = $this->getKey($req); $mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; # 將 SHA-1 加密後的字符串再進行一次 base64 加密 return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); }若是加密算法錯誤,客戶端在進行校檢的時候會直接報錯。若是握手成功,則客戶端側會出發onopen事件。
HTTP/1.1 101 Switching Protocols
101爲服務器返回的狀態碼,全部非101的狀態碼都表示handshake並未完成。
Websocket協議經過序列化的數據幀傳輸數據。數據封包協議中定義了opcode、payload length、Payload data等字段。其中要求:
針對上狀況,發現錯誤的一方可向對方發送close幀(狀態碼是1002,表示協議錯誤),以關閉鏈接。
具體數據幀格式以下圖所示:
這裏的長度表示遵循一個原則,用最少的字節表示長度(儘可能減小沒必要要的傳輸)。舉例說,payload真實長度是124,在0-125之間,必須用前7位表示;不容許長度1是126或127,而後長度2是124,這樣違反原則。
Payload data
應用層數據
接收到客戶端數據後的解析規則以下:
示例代碼:
/// 解析客戶端數據包 /// <param name="recBytes">服務器接收的數據包</param> /// <param name="recByteLength">有效數據長度</param> private static string AnalyticData(byte[] recBytes, int recByteLength) { if(recByteLength < 2) { return string.Empty; } bool fin = (recBytes[0] & 0x80) == 0x80; // 1bit,1表示最後一幀 if(!fin) { return string.Empty;// 超過一幀暫不處理 } bool mask_flag = (recBytes[1] & 0x80) == 0x80; // 是否包含掩碼 if(!mask_flag) { return string.Empty;// 不包含掩碼的暫不處理 } int payload_len = recBytes[1] & 0x7F; // 數據長度 byte[] masks = new byte[4]; byte[] payload_data; if(payload_len == 126) { Array.Copy(recBytes, 4, masks, 0, 4); payload_len = (UInt16)(recBytes[2] << 8 | recBytes[3]); payload_data = new byte[payload_len]; Array.Copy(recBytes, 8, payload_data, 0, payload_len); } else if(payload_len == 127) { Array.Copy(recBytes, 10, masks, 0, 4); byte[] uInt64Bytes = new byte[8]; for(int i = 0; i < 8; i++) { uInt64Bytes[i] = recBytes[9 - i]; } UInt64 len = BitConverter.ToUInt64(uInt64Bytes, 0); payload_data = new byte[len]; for(UInt64 i = 0; i < len; i++) { payload_data[i] = recBytes[i + 14]; } } else { Array.Copy(recBytes, 2, masks, 0, 4); payload_data = new byte[payload_len]; Array.Copy(recBytes, 6, payload_data, 0, payload_len); } for(var i = 0; i < payload_len; i++) { payload_data[i] = (byte)(payload_data[i] ^ masks[i % 4]); } return Encoding.UTF8.GetString(payload_data); }
服務器發送的數據以0x81開頭,緊接發送內容的長度(若長度在0-125,則1個byte表示長度;若長度不超過0xFFFF,則後2個byte 做爲無符號16位整數表示長度;若超過0xFFFF,則後8個byte做爲無符號64位整數表示長度),最後是內容的byte數組。
示例代碼:
/// 打包服務器數據 /// <param name="message">數據</param> /// <returns>數據包</returns> private static byte[] PackData(string message) { byte[] contentBytes = null; byte[] temp = Encoding.UTF8.GetBytes(message); if(temp.Length < 126) { contentBytes = new byte[temp.Length + 2]; contentBytes[0] = 0x81; contentBytes[1] = (byte)temp.Length; Array.Copy(temp, 0, contentBytes, 2, temp.Length); } else if(temp.Length < 0xFFFF) { contentBytes = new byte[temp.Length + 4]; contentBytes[0] = 0x81; contentBytes[1] = 126; contentBytes[2] = (byte)(temp.Length & 0xFF); contentBytes[3] = (byte)(temp.Length >> 8 & 0xFF); Array.Copy(temp, 0, contentBytes, 4, temp.Length); } else { // 暫不處理超長內容 } return contentBytes; }
相對於Opening Handshake,Closing Handshake則簡單得多,主動關閉的一方向另外一方發送一個關閉類型的數據包,對方收到此數據包以後,再回復一個相同類型的數據包,關閉完成。
關閉類型數據包遵照封包協議,Opcode爲0x8,Payload data能夠用於攜帶關閉緣由或消息。
以上的Opening Handshake、Data Framing、Closing Handshake三個步驟其實分別對應了websocket的三個事件:
任何程序語言的websocket api都至少要提供上面三個事件的api接口, 有的可能還提供的有onerror事件的處理機制。
websocket 在任什麼時候候都會處於下面4種狀態中的其中一種:
客戶端
在支持WebSocket的瀏覽器中,在建立socket以後。能夠經過onopen,onmessage,onclose即onerror四個事件實現對socket進行響應
一個簡單是示例:
var ws = new WebSocket(「ws://localhost:8080」); ws.onopen = function() { console.log(「open」); ws.send(「hello」); }; ws.onmessage = function(evt) { console.log(evt.data); }; ws.onclose = function(evt) { console.log(「WebSocketClosed!」); }; ws.onerror = function(evt) { console.log(「WebSocketError!」); };
首先申請一個WebSocket對象,參數是須要鏈接的服務器端的地址,同http協議使用http://開頭同樣,WebSocket協議的URL使用ws://開頭,另外安全的WebSocket協議使用wss://開頭。
client先發起握手請求:
GET /echobot HTTP/1.1 Host: 192.168.14.215:9000 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://192.168.14.215 Sec-WebSocket-Version: 13 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36 Accept-Encoding: gzip, deflate, sdch Accept-Language: zh-CN,zh;q=0.8 Sec-WebSocket-Key: mh3xLXeRuIWNPwq7ATG9jA== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
服務端響應:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: SIEylb7zRYJAEgiqJXaOW3V+ZWQ=
交互數據:
ws.send(「hello」); # 用於將消息發送到服務端 ws.recv($buffer); # 用於接收服務端的消息
我分別用C++、PHP、Python語言實現了websocket server和client, 只支持基本功能,也是爲了加深理解websocket協議內容。
全部源代碼放在github上,點此查看:websocket server & client 分別用C++/PHP/Python實現, 如何使用、測試及集成本身的邏輯也在文檔中進行了說明,這裏再也不列出了。