本文是Writing WebSocket servers的中文文檔,翻譯自MDNWriting WebSocket servers。篇幅略長,我的能力有限不免有所錯誤,拋磚引玉共同進步。node
WebSocket 服務器簡單來講就是一個遵循特殊協議監聽服務器任意端口的tcp應用。搭建一個定製服務器的任務一般會讓讓人們感到懼怕。然而基於實現一個簡單的Websocket服務器沒有那麼麻煩。 git
一個WebSocket server可使用任意的服務端編程語言來實現,只要該語言能實現基本的Berkeley sockets(伯克利套接字)。例如c(++)、Python、PHP、服務端JavaScript(node.js)。下面不是關於特定語言的教程,而是一個促進咱們搭建本身服務器的指南。 github
咱們須要明白http如何工做而且有中等編程經驗。基於特定語言的支持,瞭解TCP sockets 一樣也是必要的。該篇教程的範圍是介紹開發一個WebSocket server須要的最少知識。 web
該文章將會從很底層的觀點來解釋一個 WebSocket server。WebSocket servers 一般是獨立的專門的servers(由於負載均衡和其餘一些緣由),所以一般使用一個反向代理(例如一個標準的HTTP server)來發現 WebSocket握手協議,預處理他們而後將客戶端信息發送給真正的WebSocket server。這意味着WebSocket server沒必要充斥這cookie和簽名的處理方法。徹底能夠放在代理中處理。 編程
首先,服務器必須使用標準的TCPsocket來監聽即將到來的socket鏈接。基於咱們的平臺,這些極可能被咱們處理了(成熟的服務端語言提供了這些接口,使咱們沒必要從頭作起)。例如,假設咱們的服務器監聽example.com的8000端口,socket server響應/chat的GET請求。 json
警告:服務器能夠選擇監放任意端口,可是若是在80或443以外,可能會遇到防火牆或者代理的問題。443端口大多數狀況下是能夠的,固然須要一個安全鏈接(TLS/SSL)。此外,注意這一點,大多數瀏覽器不容許從安全的頁面鏈接到不安全的Websocket服務器。
在WebSockets中握手是web,是HTTP想WS轉化的橋樑。經過握手,鏈接的詳情會被判斷,而且在完成以前每個部分均可以終端若是條件不知足。服務器必須謹慎解析客戶端請求的全部信息,不然安全問題將會發生。 瀏覽器
儘管咱們在開發一個服務器,客戶端仍然須要發起一個Websocket握手過程。所以咱們必須知道如何解析客戶端的請求。客戶端將會發送一個標準的HTTP請求,大概像下面的例子(HTTP版本必須1.1及以上,請求方式爲GET)。 安全
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13複製代碼
此處客戶端能夠發起擴展或者子協議,在Miscellaneous查看更多細節。一樣,公共的headers像User-Agent, Referer, Cookie, or authentication等一樣能夠包括,一句話作你想作的。這些並不直接和WebSocket相關,忽略掉他們也是安全的,在不少公共的設置中,會有一個代理服務器來處理這些信息。 bash
若是有的header不被識別或者有非法值,服務器應該發送'400 Bad Request'並馬上關閉socket,一般也會在HTTP返回體中給出握手失敗的緣由,不過這些信息可能不會被展現(由於瀏覽器不會展現他們)。若是服務器不識別WebSockets的版本,應該返回一個Sec-WebSocket-Version 消息頭,指明能夠接受的版本(最好是V13,及最新)。下面一塊兒看一下最神祕的消息頭Sec-WebSocket-Key。 服務器
當服務器接受到請求時,應該發送一個至關奇怪的響應,看起來大概這個樣子,不過仍然遵循HTTP規範。 請注意每個header以\r\n結尾而且在最後一個後面加入額外的\r\n。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=複製代碼
此外,服務器能夠在這裏決定擴展或者子協議請求。更多詳情請查看Miscellaneous。Sec-WebSocket-Accept 部分頗有趣,服務器必須基於客戶端請求的Sec-WebSocket-Key 中獲得它,具體作法以下:將Sec-WebSocket-Key 和"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"連接,經過SHA-1 hash得到結果,而後返回該結果的base64編碼。
###提示
由於這個看似複雜的過程存在,因此客戶端不用關心服務器是否支持websocket。另外,該過程的重要性仍是在於安全性,若是一個服務器將一個Websocket鏈接做爲http請求解析的話,將會有不小的問題。
所以,若是key是"dGhlIHNhbXBsZSBub25jZQ==",Accept將會是"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",一旦服務器發送這些消息頭,握手協議就完成了。
服務器在回覆握手以前,能夠發送其餘的header像Set-Cookie、要求籤名、重定向等。
雖然並不直接與Websocket協議相關,但值得咱們注意。服務器將會跟蹤客戶端的sockets,所以咱們沒必要和已經完成握手協議的客戶端再次進行握手。相同客戶端的IP地址能夠嘗試屢次鏈接(可是服務器能夠選擇拒絕,若是他們嘗試屢次鏈接以達到保存本身Denial-of-Service 蹤影的目的)
客戶端和服務器均可以在任意時間發送消息、這正是websocket的魔力所在。然而從數據幀中提取信息的過程就不那麼充滿魔力了。儘管全部的幀遵循相同的特定格式,從客戶端發到服務器的數據經過X異或加密 (使用32位的密鑰)進行處理,該規範的第五章詳細描述了相關內容。
每一個從客戶端發送到服務器的數據幀遵循下面的格式:
幀格式:
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 ... |
+---------------------------------------------------------------+複製代碼
MASK (掩碼:一串二進制代碼對目標字段進行位與運算,屏蔽當前的輸入位。)位只代表信息是否已進行掩碼處理。來自客戶端的消息必須通過處理,所以咱們應該將其置爲1(事實上5.1節代表,若是客戶端發送未掩碼處理的消息,服務器必須斷開鏈接)當發送一個幀至客戶端時,不要處理數據而且不設置mask位。下面將會闡述緣由。注意:咱們必須處理消息即便用一個安全的socket。RSV1-3能夠被忽略,這是待擴展位。
opcode字段定義如何解析有效的數據:
FIN 代表是不是數據集合的最後一段消息,若是爲0,服務器繼續監聽消息,以待消息剩餘的部分。不然服務器認爲消息已經徹底發送。
爲了解析有效編碼數據,咱們必須知道什麼時候結束。這是知道有效數據長度的重要所在。不幸的是,有一些複雜。讓咱們分步驟來看。
若是MASK位被設置(固然它應該被設置,對於一個從客戶端到服務器的消息),讀取後4字節(即32位),即加密的key。一旦數據長度和加密key被解碼,咱們能夠直接從socket中讀取成批的字節。獲取編碼的數據和掩碼key,將其解碼,循環遍歷加密的字節(octets,text數據的單位)而且將其與第(i%4)位掩碼字節(即i除以4取餘)進行異或運算,若是用js就以下所示(該規則就是加密解密的規則而已,不必深究,你們知道如何使用就好)。
var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}複製代碼
如今咱們能夠知道咱們應用上解碼以後的數據具體含義了。
FIN和opcode字段共同工做來說一個消息分解爲單獨的幀,該過程叫作消息分割,只有在opcodes爲0x0-0x2時纔可用(前面也提到,當前版本其餘數值無心義)。
回想一下,opcode指明瞭一個幀的將要作什麼,若是是0x1,數據是text。若是是0x2,詩句是二進制數據。然而當其爲0x0時,該幀是一個繼續幀,表示服務器應該將該幀的有效數據和服務器收到的最後一幀連接起來。這是一個草圖,指明瞭當客戶端發送text消息時,第一個消息在一個單獨的幀裏發送,然而第二個消息卻包括三個幀,服務器如何反應。FIN和opcode細節僅僅對客戶端展現。看一下下面的例子應該會更容易理解。
Client: FIN=1, opcode=0x1, msg="hello"
Server: (消息傳輸過程完成) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (監聽,新的消息包含開始的文本)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (監聽,有效數據與上面的消息拼接)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (消息傳輸完成) Happy new year to you too!複製代碼
注意:第一幀包括一個徹底的消息(FIN=1而且opcode!=0x0),所以當服務器發現結束時能夠返回。第二幀有效數據爲text(opcode=0x1),可是完整的消息沒有到達(FIN=0)。該消息全部剩下的部分經過繼續幀發送(opcode=0x0),而且最後以幀經過FIN=1代表身份。
在握手接受以後的任意點,不管是客戶端仍是服務器均可以選擇發送ping給另外一部分。當ping被接收時,接收方必須儘量的返回一個pong。咱們能夠用該方式來確保鏈接依然有效。
一個ping或者pong只是一個規則的幀,可是是控制幀,Pings的opcode爲0x9,pong是0xA。當咱們獲得ping時,返回具備徹底相同有效數據的pong。(對ping和pong而言,最大有效數據長度是125)咱們可能在沒有發送ping的狀況下,獲得一個pong。這種狀況請忽略。
在發送pong以前,若是咱們接收到不止一個ping,只需迴應一個pong便可。
要關閉客戶端和服務器之間的鏈接,咱們能夠發送一個包含特定控制隊列的數據的控制幀來開始關閉的握手協議。當接收到該幀時,另外一方發送一個關閉幀做爲迴應。而後前者會關閉鏈接。關閉鏈接以後接收到的數據都會被丟棄。
WebSocket 擴展和子協議在握手過程當中經過headers進行約定。有時擴展和子協議太近似了以至於難以分別。最基本的區別是,擴展控制websocket 幀而且修改有效數據。然而子協議構成websocket有效數據而且從不修改任何事物。擴展是可選的廣義的,子協議是必須的侷限性的。
將擴展看做壓縮一個文件在發送以前,不管你如何作,你將發送相同的數據只不過幀不一樣而已。收件人最終將會受到與你本地拷貝相同的數據,不過以不一樣方式發送。這就是擴展作的事情。websockets定義了一個協議和基本的方式去發送數據,然而擴展例如壓縮能夠以更短的幀來阿鬆相同的數據。
將子協議看做定作的xml表或者文檔類型說明。你在使用XML和它的語法,可是你被限制於你贊成的結構。WebSocket子協議就是如此。他們不介紹其餘一些華麗的東西,僅僅創建結構,像一個文檔類型和表同樣,兩個部分(client & server)都贊成該協議,和文檔類型和表不一樣,子協議由服務器實現而且客戶端不能對外引用。
一個客戶端必須請求特定的子協議,爲了達到目的,將會發送一些像下面的內容做爲原始握手的一部分。
GET /chat HTTP/1.1
...
Sec-WebSocket-Protocol: soap, wamp複製代碼
或者等價的寫法
...
Sec-WebSocket-Protocol: soap
Sec-WebSocket-Protocol: wamp複製代碼
如今,服務器必須選擇客戶端建議而且支持的一種協議。若是多餘一個,發送客戶端發送過來的第一個。想象咱們的服務器可使用soap和wamp中的一個,而後,返回的握手中將會發送以下形式。
Sec-WebSocket-Protocol: soap複製代碼
服務器不能發送超過一個的Sec-Websocket-Protocol消息頭,若是服務器不想使用任一個子協議,應該不發送Sec-WebSocket-Protocol 消息頭。發送一個空白的消息頭是錯誤的。客戶端可能會關閉鏈接若是不能得到指望的子協議。
若是咱們但願咱們的服務器遵照必定的子協議,天然地在咱們的服務器須要額外的代碼。想象咱們使用一個子協議json,基於該子協議,全部的數據將會做爲JSON傳遞,若是一個客戶端徵求子協議而且服務器想使用它,服務你須要有一個JSON解析。實話實說,將會有一個工具庫,可是服務器也要須要傳遞數據。
爲了不名稱衝突,推薦選用domain的一部分做爲子協議的名稱。若是咱們開發一個使用特定格式的聊天app,咱們可能使用這樣的名字:Sec-WebSocket-Protocol: chat.example.com 注意,這不是必須的。僅僅是一個可選的慣例,咱們可使用咱們想用的任意字符。
翻譯這篇文檔的初衷是看到關於websocket的中文大部分都是客戶端相關的內容,本身又對服務器端的實現感興趣,沒有找到合適的資料,就只好本身閱讀下英文,本着提升本身的目的將其翻譯下來,但願對其餘同窗有所幫助,原文查看 。後面請期待node實現websocket服務器的實踐篇。