WebSocket協議詳解及應用(七)-WebSocket協議關閉幀
本篇介紹WebSocket協議的關閉幀,包括客戶端及服務器如何發送並處理關閉幀、關閉幀錯誤碼及錯誤處理方法。本篇內容主要翻譯自RFC 6455 第7節,大部分介紹的是原理,如果僅需要了解應用方法可略過此篇。
一、關閉WebSocket連接
要斷開WebSocket連接,需要一個端點斷開底層的TCP連接。端點需要通過某種方式來完全關閉TCP連接,例如TLS會話,並適當的丟棄未接收完畢的數據。端點也在必要時可以通過一些有效的方式斷開連接,如在受到攻擊時。
在一般情況下,底層TCP連接應先被服務端斷開,以便保持TIME_WAIT狀態。這是為了防止其在2個最大分節生命期(1~4分鍾,Windows操作系統為4分鍾)之內重新打開,否則可能會由於接到一個高序列的SYN包而重新打開連接。在一些異常的情況下(如在一段時間內未收到服務器端TCP關閉幀),客戶端可以關閉TCP連接。如果服務器發出關閉指令,則它需要立即關閉連接。而客戶端發出關閉指令需要等待服務器發送的TCP關閉幀。
二、關閉握手階段
關閉握手階段需要一個狀態碼和一個可選的關閉原因,端點必須發送一個關閉控制幀,並設置狀態碼和關閉原因。一旦端點發送並接收了關閉幀,就需要按上節中的方法關閉WebSocket連接。
三、關閉握手階段開始
當接收或發送關閉幀后,代表關閉握手階段開始,此時WebSocket連接進入到CLOSING狀態。
四、關閉WebSocket連接結束
當底層TCP連接關閉時,代表WebSocket連接已關閉,此時WebSocket連接狀態改為CLOSED。如果TCP連接在WebSocket關閉握手結束后斷開,則此次WebSocket為一次完整的(cleanly)關閉。
如果WebSocket連接未能建立,它仍叫做連接關閉,但不是完整的。
五、關閉碼
關閉幀可以包含一個關閉碼和一個關閉原因。關閉幀可以由任何一方發起,也可以雙方同時發起。若關閉幀沒有指明關閉碼,則認為關閉碼為1005,如果WebSocket連接斷開,而沒有任何關閉幀(如底層傳輸時丟幀),則認為關閉碼為1006。
注意:雙方發送的關閉碼可能不一致。舉個栗子,對方發送了一個關閉幀,但本地程序還沒有將數據及關閉幀從socket接收緩存中讀取出來,然后本地程序決定發送一個關閉幀,雙方都會發送並接收到一個關閉幀並不會再次發送關閉幀(即只進行一次收發,即使不是發過關閉幀后收到的幀)。
六、關閉原因
關閉原因是可選的,跟在關閉碼后面,為UTF-8編碼的數據,並未對其內容做詳細的定義。如果沒有設置關閉原因,則關閉原因是一個空的字符串。
注意:同關閉碼一樣,雙方發送的關閉原因可能不一致。
七、強制關閉連接
一些情況會引起強制關閉連接,當情況發生時,客戶端需要關閉連接並將錯誤返回給用戶(如控制台中報錯等),同樣,服務器需要關閉連接並將問題記錄在日志中。
如果WebSocket連接建立在端點需要強制關閉連接之前,端點需要在處理關閉幀之前發送關閉幀並發送正確的關閉碼。當強制關閉連接后,端點不能再次嘗試向對方發送任何數據(包括關閉幀)。
除了上述情況或指定的應用層協議(如WebSocket API)外,客戶端不應該斷開連接。
八、關閉碼
1000 正常關閉
1001 端點丟失,如服務器宕機或瀏覽器切換其他頁面
1002 協議錯誤
1003 數據類型錯誤(例如端點只能處理文本,但傳來了二進制消息)
1004 保留
1005 保留,禁止由端點發送此類型關閉幀,它是用來當端點沒有表明關閉碼時的默認關閉碼。
1006 保留,禁止由端點發送此類型關閉幀,它是用來當端點未發送關閉幀,連接異常斷開時使用。
1007 數據內容錯誤(如在text幀中非utf-8編碼的數據)
1008 端點已接收消息,但違反其策略。當沒有更好的關閉碼(1003或1009)的時候用此關閉碼或者不希望顯示錯誤細節。
1009 內容過長
1010 客戶端期望服務器協商一個或多個擴展,但這些擴展並未在WebSocket握手響應中返回。
1011 遇到未知情況無法執行請求
1015 保留,禁止由端點發送此類型關閉幀,它會在TLS握手失敗(如證書驗證失敗)時返回。
保留關閉碼
0-999 尚未使用
1000-2999 協議保留,用於未來版本、擴展等
3000-3999 為庫、框架、應用程序保留,這些狀態碼可在IANA中注冊,這些狀態碼並未在此協議中實現。
4000-4999 私有保留,不可被注冊。用於開發者自定義關閉碼。
WebSocket協議詳解及應用(六)-WebSocket協議控制幀結構詳解
目前,WebSocket控制幀有3種:Close(關閉幀)、Ping以及Pong。本篇文章主要對RFC6455的5.5節進行翻譯及介紹。
一、控制幀
控制幀是由操作碼上的位值置為1來定義的。目前,控制幀的操作碼定義了0x08(關閉幀)、0x09(Ping幀)、0x0A(Pong幀)。0x0B-0x0F是為那些將來可能定義而目前尚未定義的控制幀預留的。
控制幀用於WebSocket協議交換狀態信息,控制幀可以插在消息片段之間。
注意:所有的控制幀的負載長度務必不大於125字節,並且禁止對控制幀進行分片處理。
二、關閉幀
關閉幀的操作碼是0x08。
關閉幀可能包含數據部分(應用數據幀),該部分表明了關閉的原因,例如端點關閉、端點接收幀過大或端點收到的幀不符合預期。如果有數據部分,則數據的前兩個字節必須是一個無符號整數(網絡字節序),該無符號整數表示了一個狀態碼,具體定義哪些關閉碼將在后面的文章中介紹。在無符號整數后面,可能還有一個UTF-8編碼的數據,表示關閉原因,關閉原因由開發者自行定義(可選),並無規范。關閉原因並不一定是對人可讀的,但會對調試或傳遞相關信息起到一定的作用。由於數據不能保證人類可讀,所以客戶端一定不能將其顯示給用戶(會在關閉事件onclose中)。
客戶端發送給服務器的關閉幀必須掩碼處理。
應用程序在發送了一個關閉幀后,禁止再發送任何數據(此時處於CLOSING狀態)。
如果端點(客戶端或服務器)收到了一個關閉幀,並且之前沒有發送過關閉幀,則端點必須發送一個關閉幀作為響應。(當端點發送一個關閉幀回應時,通常會顯示它收到的狀態碼。)當端點可以發送關閉響應時應盡快發送關閉響應。一個端點可以延遲發送響應直到它的當前消息發送完畢(例如,已經發送了大多數的消息片段,則端點可能會在發送關閉響應幀前先將剩下的消息幀發送出去)。但不能保證對方在已經發送了關閉幀后還能夠繼續處理這些數據。
在雙方都以發送並接收了關閉幀后,端點需要斷掉WebSocket連接並且必須關閉底層的TCP連接。服務器必須立即切斷底層TCP連接,客戶端最好等待服務器斷開連接,但也可以在發送並接收了關閉幀后任何時候斷開連接,例如在一段時間內服務器仍沒有斷開TCP連接。
如果服務器和客戶端同時發送了關閉幀,兩端都會接收關閉幀,並且都需要斷開TCP連接。
三、PING幀
Ping幀的操作碼為0x09。
Ping幀可以包含應用數據。
一旦接到了一個Ping幀,端點必須返回一個Pong幀作為響應,除非它收到了一個關閉幀。它應在可以發送時盡快發送Pong幀響應。
端點可以在連接建立后一直到連接關閉前任何時候發送Ping幀。
提示:Ping幀既可用於保持活動狀態也可用於驗證遠端仍可響應數據。
四、PONG幀
Pong幀的操作碼為0x0A。
Pong幀必須與Ping幀擁有相同的應用數據部分。
如果端點收到了多個Ping幀,但還沒來的及全部回應,可以只回應最后一個Ping幀。
Pong幀可以在未收到Ping幀時就被發送,用作單向心跳包。
對未被請求的Pong幀(對方主動發送的Pong幀)進行回應是不需要的。
下篇將介紹WebSocket的關閉協議,及關閉的狀態碼與關閉異常處理。
2015年1月27日WebSocket 留下評論
WebSocket協議詳解及應用(五)-WebSocket協議幀結構詳解
本篇主要介紹WebSocket協議的幀結構,詳細講解WebSocket幀的構成。
一、幀結構圖及含義
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 ... |
+---------------------------------------------------------------+
0Bit:
FIN 結束標識位,如果FIN為1,代表該幀為結束幀(如果一條消息過長可以將其拆分為多個幀,這時候FIN可以置為0,表示后面還有數據幀,服務器需要將該幀內容緩存起來,待所有幀都接收后再拼接到一起。控制幀不可拆分為多幀)。
1~3Bit:
RSV1~RSV3 保留標識位,以后做協議擴展時才會用到,目前該3位都為0
4~7Bit:
opcode 操作碼,用於標識該幀負載的類型,如果收到了未知的操作碼,則根據協議,需要斷開WebSocket連接。操作碼含義如下:
0x00 連續幀,瀏覽器的WebSocket API一般不會收到該類型的操作碼
0x01 文本幀,最常用到的數據幀類別之一,表示該幀的負載是一段文本(UTF-8字符流)
0x02 二進制幀,較常用到的數據幀類別之一,表示該幀的負載是二進制數據
0x03-0x07 保留幀,留作未來非控制幀擴展使用
0x08 關閉連接控制幀,表示要斷開WebSocket連接,瀏覽器端調用close方法會發送0x08控制幀
0x09 ping幀,用於檢測端點是否可用,暫未發現瀏覽器可以通過何種方法發送該幀
0x0A pong幀,用於回復ping幀,暫未發現瀏覽器可以發送此種類型的控制幀
0x0B-0x0F 保留幀,留作未來控制幀擴展使用
8Bit:
MASK 掩碼標識位,用來表明負載是否經過掩碼處理,瀏覽器發送的數據都是經過掩碼處理(瀏覽器自動處理,無需開發者編碼),服務器發送的幀必須不經過掩碼處理。所以此處瀏覽器發送的幀必為1,服務器發送的幀必為0,否則應斷開WebSocket連接
9~15Bit:
payload length 負載長度,單位字節如果負載長度0~125字節,則此處就是負載長度的字節數,如果負載長度在126~65535之間,則此處的值為126,16~32Bit表示負載的真實長度。如果負載長度在65536~2的64次方-1時,16~80Bit表示負載的真實長度。其中負載長度包括應用數據長度和擴展數據的長度
payload length 后面4個字節可能是掩碼的key(如果掩碼位是1則有這4個字節的key,否則沒有),掩碼計算方法將在后面給出。
接下來就是負載的數據了,他們可能需要根據掩碼的key進行編碼(僅瀏覽器需要掩碼),如果存在擴展數據,需要放在應用數據之前
二、掩碼計算
如果你只需要做瀏覽器端的編程,可以忽略以下內容,瀏覽器會自動計算掩碼。如果你需要做服務端編程,則需要詳細閱讀下面內容,你必須根據掩碼計算的方法將瀏覽器發送的數據幀進行解碼操作。
掩碼的key值是一個由客戶端隨機選擇的32比特值。首先,瀏覽器必須從掩碼可用的key值中選擇一個值,用於封裝數據幀。掩碼的key必須是一個不可預測的值(例如從一個健壯的熵值中獲取),並且必須使服務器或代理不能簡單的預測出接下來的key值,以防止有人使用惡意的應用通過監聽網絡選擇key值。
掩碼操作不會影響負載的長度,通過下面的算法可以進行編碼和解碼,編碼和解碼的步驟完全一樣:
- 將負載和掩碼分成8位位組(分割成字節)
- 將負載每一個字節與掩碼的每個字節做循環位異或操作
- 將結果得到的每一個字節拼接到一起即為掩碼計算后的數據
使用PHP計算代碼如下:
function parseMask($source, $mask){
$len = strlen($source);
$dest = '';
for($i=0; $i<$len; $i++){ $dest .= chr(ord($source[$i]) ^ ord($mask[$i%4])); } return $dest; }
下一篇將介紹WebSocket幾種控制幀的格式及用法
2015年1月25日WebSocket 留下評論
WebSocket協議詳解及應用(四)-WebSocket握手協議之通信建立與錯誤處理
上一篇介紹了服務器端如何處理握手協議並正確返回響應頭,本篇主要介紹瀏覽器在服務器返回的握手協議有問題時如何進行處理。注:本文介紹的內容已在瀏覽器內部實現,不需要做任何編碼工作,以下內容只介紹原理和協議本身。
一、連接異常的情況
以下情況,客戶端必須停止握手並斷開連接:
- 服務器域名不能解析
- 數據包不能成功的傳遞到服務器
- 服務器指定端口禁止連接
- 服務器TLS傳輸失敗,例如服務器證書無法驗證
- 服務器無法完成握手協議,例如目標服務器不是WebSocket服務器(返回了200或其他非101狀態碼)
- 服務器握手成功,但一些選項引起客戶端放棄連接(如服務器提供了客戶端無法識別子協議)
- 服務器在完成握手協議后意外關閉了連接
以上所有的情況都會使WebSocket以1006(連接意外斷開)的退出碼斷開連接。
二、握手成功后瀏覽器的工作
當整個握手階段無任何異常時,服務器與瀏覽器已經建立連接,此時瀏覽器會完成一些初始化工作。
首先將readyState屬性設置為OPEN(WebSocket的常量,值為1),readyState是WebSocket的一個屬性,代表連接的狀態,包含CONNECTING、OPEN、CLOSING、CLOSED等4個狀態。其中OPEN即連接已建立,可以進行數據通信了。
如果extensions非空,則設置此屬性的值。
如果protocol的值非空,則設置此屬性的值。
如果返回了cookie設置的響應頭,則需要將指定的cookie設置到WebSocket構造函數的第二個參數URL下。
最后觸發WebSocket的onopen事件處理函數。
三、WebSocket對象的屬性及方法介紹
調用WebSocket構造方法后,會返回一個WebSocket對象,類似如下:
URL: "ws://localhost/"
binaryType: "blob"
bufferedAmount: 0
extensions: ""
onclose: null
onerror: null
onmessage: null
onopen: null
protocol: ""
readyState: 3
url: "ws://localhost/"
CLOSED: 3
CLOSING: 2
CONNECTING: 0
OPEN: 1
close: function close() { [native code] }
send: function send() { [native code] }
binaryType為二進制使用哪種類型的數據結構,取值可以是blob或arraybuffer.
bufferedAmount為緩存中剩余的字節數,有時數據並不是准備好才傳輸到網絡上,而是一邊生成一邊傳遞,這時需要將數據緩存到一定大小再發送,並且需要定時將緩存中剩余的內容發送出去。bufferedAmount就記錄了send函數中的應用數據(UTF-8的字符串或二進制的數據)隊列中還未被發送到網絡的字節數。如果連接關閉,該屬性值會在send方法被調用時增長(一旦連接關閉,該屬性值就不會重置為0)
onclose在連接關閉時觸發
onerror在發生錯誤時觸發
onmessage在瀏覽器接到服務器發來的數據時觸發
onopen在連接建立時觸發
readyState連接狀態,上文中已介紹
CLOSED連接已經關閉
CLOSING連接關閉中,此時處於半關閉狀態,瀏覽器已經發送了關閉指令,但此時服務器還未響應關閉指令。瀏覽器不能再收發數據了。
CONNECTING正在連接中,瀏覽器已經發送了握手請求,等待服務器返回握手響應。
OPEN已建立連接,可以傳輸數據。
close方法用於瀏覽器主動關閉連接,它會發送操作碼8到服務器,它接受2個可選參數,關閉碼和關閉原因。有關close的詳細用法將在以后的文章中詳細介紹。
send方法用於瀏覽器向服務器發送數據,它支持多種數據類型的傳輸,send的用法也會在后面的文章中詳細介紹。
下篇會詳細介紹WebSocket數據幀的格式,主要針對RFC6455的5.2節(基礎數據幀協議)進行翻譯和介紹。
2015年1月22日WebSocket 留下評論
WebSocket協議詳解及應用(三)-WebSocket握手協議之服務器響應
本文主要介紹服務端應用在接收到WebSocket握手后,如何構造正確的響應包。本文部分翻譯自rfc6455。
一、響應包Sec-WebSocket-Accept字段值的計算
上篇說道瀏覽器發送的最重要的一個請求頭是Sec-WebSocket-Key,服務端程序需要根據RFC6455中的算法計算Sec-WebSocket-Accept的值。我們以瀏覽器發送了Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==為例,介紹如何計算響應值。
首先服務端程序要將Sec-WebSocket-Key的值與一個魔法字符串”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″拼接到一起,得到”dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11″,關於魔法字符串的來歷,有興趣的同學可以看一下RFC4122(實際是一個GUID)。
第二步,將上一步中合並的字符串使用sha1計算sha1值,這里如果使用PHP的sha1函數進行計算,要注意sha1的第二個參數必須顯式的給出true值,否則sha1的結果是一個16進制的字符串,而不是二進制數值,其他語言如果有類似的情況也要注意,求出的結果是二進制,而不是轉換后的16進制值。
第三步,將第二步中的二進制值使用base64進行編碼,使用PHP計算方法如下:base64_encode(sha1($secWebSocketKey."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
最后得出的結果為”s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”,將這個結果作為Sec-WebSocket-Accept的值,放入響應頭中。
二、響應頭的構成
1.HTTP狀態碼
握手響應的HTTP狀態碼為101 Switching Protocols,代表協議轉換,如HTTP/1.1 101 Switching Protocols
2.Upgrade字段
Upgrade: websocket固定,代表轉換為WebSocket協議,同瀏覽器請求頭
3.Connection字段
Connection: Upgrade固定,同瀏覽器請求頭
4.Sec-WebSocket-Accept
通過上面的方法計算出的值
5.Sec-WebSocket-Protocol
可選返回頭,根據瀏覽器發送的子協議返回。如果瀏覽器發送了多個子協議,這里可以選擇一個或多個進行返回,此處是子協議協商的過程
三、服務端響應步驟
- 判斷Origin是否可信,上一篇中說到,WebSocket不存在跨域問題,在任何域中都可以與其他域建立WebSocket連接。但出於某些原因,我們不希望一些其他的網站連接服務器,這時可以驗證Origin是否在訪問源白名單中。
- 通過Cookie驗證身份,由於WebSocket的握手協議仍是通過HTTP協議進行的,它會向服務端發送目標域下(當前運行的WebSocket服務器)的cookie。服務端程序可以通過cookie將該socket與用戶身份綁定到一起,就不需要在以后傳輸身份信息
- 讀取Sec-WebSocket-Version的值判斷協議版本,如果不是13,則需要用其他方法解析(本系列只介紹13版本,其他版本的握手及通信不在介紹范圍內)
- 通過Sec-WebSocket-Key計算Sec-WebSocket-Accept的值
- 讀取Sec-WebSocket-Protocol,根據服務器實現情況返回支持的一個或多個子協議名,使用半角逗號分隔
- 將第二節中的響應頭發送給瀏覽器
四、關於Sec-WebSocket-Extensions
在WebSocket的請求和響應頭中,還有一個可選的字段Sec-WebSocket-Extensions,目前很少使用,現將RFC6455內容翻譯如下:
“Sec-WebSocket-Extensions”字段僅用在WebSocket連接握手階段。它先由客戶端(瀏覽器)發送到服務器,然后再由服務器傳回客戶端,以便協商連接過程中的協議層擴展集。
“Sec-WebSocket-Extensions”字段可能在HTTP請求中出現多次(等價於出現一次,但有多個值),但是最多只能在HTTP響應中出現一次。
五、握手階段的錯誤處理
WebSocket協議中並未明確規定當服務器遇到錯誤(偽造)的握手請求該如何處理,你可以直接斷開客戶端的連接,但這樣並不是比較好的方法,推薦使用下面的做法(瀏覽器會忽略出101之外的狀態碼,返回非101狀態碼瀏覽器會自動切斷連接):
1.當客戶端請求中缺少host、Upgrade、Connection、Sec-WebSocket-Key、Sec-WebSocket-Version時返回狀態碼400(語法格式錯誤)
2.當Origin不在可信源中時,返回403(權限)
3.當身份驗證(cookie)出錯時,返回401或403
4.當version或protocol不支持時,返回501(尚未實現)
下一篇將介紹瀏覽器在收到服務器返回的響應頭后會做些什么,以及響應錯誤的情況及處理
2015年1月21日WebSocket 留下評論
WebSocket協議詳解及應用(二)-WebSocket握手協議之瀏覽器請求
目前WebSocket協議版本已更新到13,本系列文章均以WebSocket 13版本為例。
一、通過WebSocket API與服務器進行握手
WebSocket的構造方法中有兩個參數,其原型如下:WebSocket(url, [protocols])
,url為需要連接的地址。WebSocket的協議頭的寫法有2個,一個是ws://
,另一個是wss://
,它們的區別就是后者相當於https,是加密的。如果url沒有加端口號,當協議頭為ws時默認為80端口,當協議頭為wss時默認為443端口。如果需要連接其他端口,則需要向http協議一樣,加上端口號,如ws://localhost:12345
。url其他部分與標准URL一致,以下URL是合法的:ws://localhost:12345/path/?queryString=queryString
。具體URL標准將在下面協議詳解中詳細說明。
第二個參數protocols為可選參數,代表WebSocket協議的子協議(可以是用戶自定義的)。你可能已經注意到了第二個參數起名為protocols最后有個s,它可以是一個字符串亦可以是一個數組,如果protocols是一個字符串,則它等價於一個單一值的數組。例如,WebSocket('ws://localhost','subprotocol')
等價於WebSocket('ws://localhost',['subprotocol'])
二、WebSocket URL解析
以下內容部分翻譯自W3C規范
WebSocket URL解析組件解析URL步驟如下,這些步驟會返回一個主機名、端口號、資源名和一個安全標識符否則返回失敗:
- 如果URL不是一個絕對URL,算法失敗
- 將URL字符串轉化為UTF-8格式
- 如果URL的模式不是ws或wss,則算法失敗
- 如果解析的URL結果中存在非空的片段,則算法失敗
- 如果是ws模式的URL,則將安全標識符設為假,如果是wss模式的URL則將安全標示符置為真
- 解析host名
- 如果為顯示的聲明端口號,則認為端口號是隱式聲明的
- 如果端口號是隱式聲明的,當安全標示符為真時,端口號是443,否則為80
- 如果資源名是非空的,則將其設置到結果中,否則將/作為資源名
- 如果URL請求字符串非空,則將請求字符串加入結果中,並使用?拼接(同HTTP中URL的請求字符串寫法)
- 將主機名、端口號、資源名和安全標示符返回
三、瀏覽器發送握手包
通過本文第一部分我們可知,瀏覽器端發送握手請求的API非常簡單,只需要new出一個WebSocket的對象,最少情況只需要帶一個參數,如var ws = new WebSocket('ws://localhost');
我們在http://dev.w3.org中打開控制台使用var ws = new WebSocket('ws://localhost','test');
瀏覽器會發出類似如下請求(注意:這里不會存在跨域問題,不需要在同域下即可,因為WebSocket是在TCP層面上傳輸數據,而”域”的概念在應用層):
GET ws://localhost/ HTTP/1.1
Host: localhost
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://dev.w3.org
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol:test
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4
Sec-WebSocket-Key: 32pdAhmqFrFZik/MP7fU8A==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
其實這個就是一個標准的HTTP協議,WebSocket協議中的握手過程是HTTP協議,而一旦握手成功后,就不在是HTTP協議,而是直接通過TCP傳輸數據。
請求頭中大部分內容與HTTP協議一致,這里不再解釋,只解釋那些不曾出現在普通請求的請求頭。
Connection: Upgrade是固定的,表示需要轉換為其他協議
Upgrade: websocket是固定的,說明要轉換成的協議時WebSocket
Sec-WebSocket-Version: 13代表WebSocket的版本,目前版本是13
Sec-WebSocket-Key: 32pdAhmqFrFZik/MP7fU8A==這個是握手的認證串,服務端需要將此key進行一定處理后返回,再由瀏覽器驗證有效性,必須符合算法結果才可正常建立連接(類似交換證書,但實際上只是簡單的哈希計算)
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits代表客戶端支持的擴展類型
Sec-WebSocket-Protocol:test為子協議,是否存在取決於構造方法的第二個參數,如果第二個參數是個數組,則此處的值為數組按一個逗號和一個空格分隔,相當於[].join(', ');
其中Sec-WebSocket-Key是非常重要的請求頭,我們會在服務端處理這個請求頭,只有處理正確,瀏覽器才會正確的與服務器建立連接。
下一篇將會介紹服務器端如何處理瀏覽器的請求頭,並返回正確響應頭以及如何處理錯誤的請求頭
2015年1月20日WebSocket 留下評論
WebSocket協議詳解及應用(一)-初識WebSocket
一、什么是WebSocket
WebSocket是一個允許Web應用程序(通常指瀏覽器)與服務器進行雙向通信的協議。HTML5的WebSocket API主要是為瀏覽器端提供了一個基於TCP協議實現全雙工通信的方法。
二、優點
1.全雙工通信
在傳統的Web應用中,瀏覽器與服務器交互都是半雙工通信(但並不完全是半雙工通信,服務器無法主動向瀏覽器推送)。即同一時間內數據流向是單一的,瀏覽器向服務器發送請求后需要等待服務器返回數據。
而在WebSocket中,瀏覽器和服務器之間可隨時進行通信,不必等待對方傳送完畢,瀏覽器接收到服務器的數據后會自動觸發onmessage事件。
2.實時性
傳統的Web應用很難做到實時通信,通常是用長連接或輪詢的方式進行。對於服務器來說,輪詢法是被動傳輸數據,即使數據有更新,但瀏覽器還未發送請求,則消息無法進行實時推送。
3.更少的數據傳輸及更少的請求數
傳統Web應用中瀏覽器與服務器進行數據交互通常需要經過以下幾個步驟:
- DNS查詢
- TCP三次握手
- 傳送HTTP請求頭
- 傳送HTTP請求體(如果有)
- 服務器處理后傳送響應頭
- 服務器傳送響應體
- 斷開TCP連接
在WebSocket中進行交互通常為以下幾個步驟:
- DNS查詢
- TCP三次握手
- WebSocket握手
- 瀏覽器發送請求
- 服務器發送響應
- 斷開TCP連接
從上面可以看到如果僅是一次通信,二者差異並不是很大,甚至WebSocket比普通方式還要多一次握手。但在需要頻繁交互數據時,WebSocket的優勢就顯露出來了。
例如,當有10次數據交互時,前者要建立10個TCP連接(HTTP 1.0需要建立10次,HTTP 1.1可以通過長連接keep-alive復用TCP連接),然后要發送10次請求頭(包含Cookie等信息,可能會達到K級別),接收的響應信息可能才幾個字節(如某些心跳包),這樣會極大的浪費帶寬等資源。試想,如果你在做一個聊天應用,想要獲取當前在線人數,你需要向服務器發送你的全部cookie(至少要幾百個字節),除此之外HTTP頭中還要包含其他信息,如URL、host等,這些都是必不可少的。最后服務器返回了幾百個字節,但其中真正需要用到的只有不到10字節(只需要知道在線人數,其他信息都是無用的)。通過WebSocket,瀏覽器可以向服務器發送1~2字節的請求(不需要帶上cookie驗證身份,可以在握手時進行認證,一旦TCP連接建立,則在連接上的通信都是認證過身份的數據,這也是它的好處之一:便於服務端識別客戶端的狀態),這個請求僅包含一個特定的控制碼(由開發者實現的應用層協議指定),服務器只需返回特定的返回碼及數據即可,一切無用的字節都被省去。
三、應用場景
1.數據傳輸實時性要求較高
Web聊天室、貼吧直播貼、微博話題牆、專題討論、實時網絡攻擊展示(已應用)等
2.推送類應用
網站消息通知、郵箱新郵件提醒
3.監控在線狀態、精確統計在線時長
統計用戶行為
4.遠程調試代碼、雲指令系統
部分移動端調試工具是基於WebSocket開發(此處為WebSocket協議而非WebSocket API)
5.其他用途
網絡(包括內網)嗅探(端口掃描)、僵屍網絡及后門(雲指令系統)
WebSocket協議詳解及應用(七)-WebSocket協議關閉幀
本篇介紹WebSocket協議的關閉幀,包括客戶端及服務器如何發送並處理關閉幀、關閉幀錯誤碼及錯誤處理方法。本篇內容主要翻譯自RFC 6455 第7節,大部分介紹的是原理,如果僅需要了解應用方法可略過此篇。
一、關閉WebSocket連接
要斷開WebSocket連接,需要一個端點斷開底層的TCP連接。端點需要通過某種方式來完全關閉TCP連接,例如TLS會話,並適當的丟棄未接收完畢的數據。端點也在必要時可以通過一些有效的方式斷開連接,如在受到攻擊時。
在一般情況下,底層TCP連接應先被服務端斷開,以便保持TIME_WAIT狀態。這是為了防止其在2個最大分節生命期(1~4分鍾,Windows操作系統為4分鍾)之內重新打開,否則可能會由於接到一個高序列的SYN包而重新打開連接。在一些異常的情況下(如在一段時間內未收到服務器端TCP關閉幀),客戶端可以關閉TCP連接。如果服務器發出關閉指令,則它需要立即關閉連接。而客戶端發出關閉指令需要等待服務器發送的TCP關閉幀。
二、關閉握手階段
關閉握手階段需要一個狀態碼和一個可選的關閉原因,端點必須發送一個關閉控制幀,並設置狀態碼和關閉原因。一旦端點發送並接收了關閉幀,就需要按上節中的方法關閉WebSocket連接。
三、關閉握手階段開始
當接收或發送關閉幀后,代表關閉握手階段開始,此時WebSocket連接進入到CLOSING狀態。
四、關閉WebSocket連接結束
當底層TCP連接關閉時,代表WebSocket連接已關閉,此時WebSocket連接狀態改為CLOSED。如果TCP連接在WebSocket關閉握手結束后斷開,則此次WebSocket為一次完整的(cleanly)關閉。
如果WebSocket連接未能建立,它仍叫做連接關閉,但不是完整的。
五、關閉碼
關閉幀可以包含一個關閉碼和一個關閉原因。關閉幀可以由任何一方發起,也可以雙方同時發起。若關閉幀沒有指明關閉碼,則認為關閉碼為1005,如果WebSocket連接斷開,而沒有任何關閉幀(如底層傳輸時丟幀),則認為關閉碼為1006。
注意:雙方發送的關閉碼可能不一致。舉個栗子,對方發送了一個關閉幀,但本地程序還沒有將數據及關閉幀從socket接收緩存中讀取出來,然后本地程序決定發送一個關閉幀,雙方都會發送並接收到一個關閉幀並不會再次發送關閉幀(即只進行一次收發,即使不是發過關閉幀后收到的幀)。
六、關閉原因
關閉原因是可選的,跟在關閉碼后面,為UTF-8編碼的數據,並未對其內容做詳細的定義。如果沒有設置關閉原因,則關閉原因是一個空的字符串。
注意:同關閉碼一樣,雙方發送的關閉原因可能不一致。
七、強制關閉連接
一些情況會引起強制關閉連接,當情況發生時,客戶端需要關閉連接並將錯誤返回給用戶(如控制台中報錯等),同樣,服務器需要關閉連接並將問題記錄在日志中。
如果WebSocket連接建立在端點需要強制關閉連接之前,端點需要在處理關閉幀之前發送關閉幀並發送正確的關閉碼。當強制關閉連接后,端點不能再次嘗試向對方發送任何數據(包括關閉幀)。
除了上述情況或指定的應用層協議(如WebSocket API)外,客戶端不應該斷開連接。
八、關閉碼
1000 正常關閉
1001 端點丟失,如服務器宕機或瀏覽器切換其他頁面
1002 協議錯誤
1003 數據類型錯誤(例如端點只能處理文本,但傳來了二進制消息)
1004 保留
1005 保留,禁止由端點發送此類型關閉幀,它是用來當端點沒有表明關閉碼時的默認關閉碼。
1006 保留,禁止由端點發送此類型關閉幀,它是用來當端點未發送關閉幀,連接異常斷開時使用。
1007 數據內容錯誤(如在text幀中非utf-8編碼的數據)
1008 端點已接收消息,但違反其策略。當沒有更好的關閉碼(1003或1009)的時候用此關閉碼或者不希望顯示錯誤細節。
1009 內容過長
1010 客戶端期望服務器協商一個或多個擴展,但這些擴展並未在WebSocket握手響應中返回。
1011 遇到未知情況無法執行請求
1015 保留,禁止由端點發送此類型關閉幀,它會在TLS握手失敗(如證書驗證失敗)時返回。
保留關閉碼
0-999 尚未使用
1000-2999 協議保留,用於未來版本、擴展等
3000-3999 為庫、框架、應用程序保留,這些狀態碼可在IANA中注冊,這些狀態碼並未在此協議中實現。
4000-4999 私有保留,不可被注冊。用於開發者自定義關閉碼。
WebSocket協議詳解及應用(六)-WebSocket協議控制幀結構詳解
目前,WebSocket控制幀有3種:Close(關閉幀)、Ping以及Pong。本篇文章主要對RFC6455的5.5節進行翻譯及介紹。
一、控制幀
控制幀是由操作碼上的位值置為1來定義的。目前,控制幀的操作碼定義了0x08(關閉幀)、0x09(Ping幀)、0x0A(Pong幀)。0x0B-0x0F是為那些將來可能定義而目前尚未定義的控制幀預留的。
控制幀用於WebSocket協議交換狀態信息,控制幀可以插在消息片段之間。
注意:所有的控制幀的負載長度務必不大於125字節,並且禁止對控制幀進行分片處理。
二、關閉幀
關閉幀的操作碼是0x08。
關閉幀可能包含數據部分(應用數據幀),該部分表明了關閉的原因,例如端點關閉、端點接收幀過大或端點收到的幀不符合預期。如果有數據部分,則數據的前兩個字節必須是一個無符號整數(網絡字節序),該無符號整數表示了一個狀態碼,具體定義哪些關閉碼將在后面的文章中介紹。在無符號整數后面,可能還有一個UTF-8編碼的數據,表示關閉原因,關閉原因由開發者自行定義(可選),並無規范。關閉原因並不一定是對人可讀的,但會對調試或傳遞相關信息起到一定的作用。由於數據不能保證人類可讀,所以客戶端一定不能將其顯示給用戶(會在關閉事件onclose中)。
客戶端發送給服務器的關閉幀必須掩碼處理。
應用程序在發送了一個關閉幀后,禁止再發送任何數據(此時處於CLOSING狀態)。
如果端點(客戶端或服務器)收到了一個關閉幀,並且之前沒有發送過關閉幀,則端點必須發送一個關閉幀作為響應。(當端點發送一個關閉幀回應時,通常會顯示它收到的狀態碼。)當端點可以發送關閉響應時應盡快發送關閉響應。一個端點可以延遲發送響應直到它的當前消息發送完畢(例如,已經發送了大多數的消息片段,則端點可能會在發送關閉響應幀前先將剩下的消息幀發送出去)。但不能保證對方在已經發送了關閉幀后還能夠繼續處理這些數據。
在雙方都以發送並接收了關閉幀后,端點需要斷掉WebSocket連接並且必須關閉底層的TCP連接。服務器必須立即切斷底層TCP連接,客戶端最好等待服務器斷開連接,但也可以在發送並接收了關閉幀后任何時候斷開連接,例如在一段時間內服務器仍沒有斷開TCP連接。
如果服務器和客戶端同時發送了關閉幀,兩端都會接收關閉幀,並且都需要斷開TCP連接。
三、PING幀
Ping幀的操作碼為0x09。
Ping幀可以包含應用數據。
一旦接到了一個Ping幀,端點必須返回一個Pong幀作為響應,除非它收到了一個關閉幀。它應在可以發送時盡快發送Pong幀響應。
端點可以在連接建立后一直到連接關閉前任何時候發送Ping幀。
提示:Ping幀既可用於保持活動狀態也可用於驗證遠端仍可響應數據。
四、PONG幀
Pong幀的操作碼為0x0A。
Pong幀必須與Ping幀擁有相同的應用數據部分。
如果端點收到了多個Ping幀,但還沒來的及全部回應,可以只回應最后一個Ping幀。
Pong幀可以在未收到Ping幀時就被發送,用作單向心跳包。
對未被請求的Pong幀(對方主動發送的Pong幀)進行回應是不需要的。
下篇將介紹WebSocket的關閉協議,及關閉的狀態碼與關閉異常處理。
2015年1月27日WebSocket 留下評論
WebSocket協議詳解及應用(五)-WebSocket協議幀結構詳解
本篇主要介紹WebSocket協議的幀結構,詳細講解WebSocket幀的構成。
一、幀結構圖及含義
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 ... |
+---------------------------------------------------------------+
0Bit:
FIN 結束標識位,如果FIN為1,代表該幀為結束幀(如果一條消息過長可以將其拆分為多個幀,這時候FIN可以置為0,表示后面還有數據幀,服務器需要將該幀內容緩存起來,待所有幀都接收后再拼接到一起。控制幀不可拆分為多幀)。
1~3Bit:
RSV1~RSV3 保留標識位,以后做協議擴展時才會用到,目前該3位都為0
4~7Bit:
opcode 操作碼,用於標識該幀負載的類型,如果收到了未知的操作碼,則根據協議,需要斷開WebSocket連接。操作碼含義如下:
0x00 連續幀,瀏覽器的WebSocket API一般不會收到該類型的操作碼
0x01 文本幀,最常用到的數據幀類別之一,表示該幀的負載是一段文本(UTF-8字符流)
0x02 二進制幀,較常用到的數據幀類別之一,表示該幀的負載是二進制數據
0x03-0x07 保留幀,留作未來非控制幀擴展使用
0x08 關閉連接控制幀,表示要斷開WebSocket連接,瀏覽器端調用close方法會發送0x08控制幀
0x09 ping幀,用於檢測端點是否可用,暫未發現瀏覽器可以通過何種方法發送該幀
0x0A pong幀,用於回復ping幀,暫未發現瀏覽器可以發送此種類型的控制幀
0x0B-0x0F 保留幀,留作未來控制幀擴展使用
8Bit:
MASK 掩碼標識位,用來表明負載是否經過掩碼處理,瀏覽器發送的數據都是經過掩碼處理(瀏覽器自動處理,無需開發者編碼),服務器發送的幀必須不經過掩碼處理。所以此處瀏覽器發送的幀必為1,服務器發送的幀必為0,否則應斷開WebSocket連接
9~15Bit:
payload length 負載長度,單位字節如果負載長度0~125字節,則此處就是負載長度的字節數,如果負載長度在126~65535之間,則此處的值為126,16~32Bit表示負載的真實長度。如果負載長度在65536~2的64次方-1時,16~80Bit表示負載的真實長度。其中負載長度包括應用數據長度和擴展數據的長度
payload length 后面4個字節可能是掩碼的key(如果掩碼位是1則有這4個字節的key,否則沒有),掩碼計算方法將在后面給出。
接下來就是負載的數據了,他們可能需要根據掩碼的key進行編碼(僅瀏覽器需要掩碼),如果存在擴展數據,需要放在應用數據之前
二、掩碼計算
如果你只需要做瀏覽器端的編程,可以忽略以下內容,瀏覽器會自動計算掩碼。如果你需要做服務端編程,則需要詳細閱讀下面內容,你必須根據掩碼計算的方法將瀏覽器發送的數據幀進行解碼操作。
掩碼的key值是一個由客戶端隨機選擇的32比特值。首先,瀏覽器必須從掩碼可用的key值中選擇一個值,用於封裝數據幀。掩碼的key必須是一個不可預測的值(例如從一個健壯的熵值中獲取),並且必須使服務器或代理不能簡單的預測出接下來的key值,以防止有人使用惡意的應用通過監聽網絡選擇key值。
掩碼操作不會影響負載的長度,通過下面的算法可以進行編碼和解碼,編碼和解碼的步驟完全一樣:
- 將負載和掩碼分成8位位組(分割成字節)
- 將負載每一個字節與掩碼的每個字節做循環位異或操作
- 將結果得到的每一個字節拼接到一起即為掩碼計算后的數據
使用PHP計算代碼如下:
function parseMask($source, $mask){
$len = strlen($source);
$dest = '';
for($i=0; $i<$len; $i++){ $dest .= chr(ord($source[$i]) ^ ord($mask[$i%4])); } return $dest; }
下一篇將介紹WebSocket幾種控制幀的格式及用法
2015年1月25日WebSocket 留下評論
WebSocket協議詳解及應用(四)-WebSocket握手協議之通信建立與錯誤處理
上一篇介紹了服務器端如何處理握手協議並正確返回響應頭,本篇主要介紹瀏覽器在服務器返回的握手協議有問題時如何進行處理。注:本文介紹的內容已在瀏覽器內部實現,不需要做任何編碼工作,以下內容只介紹原理和協議本身。
一、連接異常的情況
以下情況,客戶端必須停止握手並斷開連接:
- 服務器域名不能解析
- 數據包不能成功的傳遞到服務器
- 服務器指定端口禁止連接
- 服務器TLS傳輸失敗,例如服務器證書無法驗證
- 服務器無法完成握手協議,例如目標服務器不是WebSocket服務器(返回了200或其他非101狀態碼)
- 服務器握手成功,但一些選項引起客戶端放棄連接(如服務器提供了客戶端無法識別子協議)
- 服務器在完成握手協議后意外關閉了連接
以上所有的情況都會使WebSocket以1006(連接意外斷開)的退出碼斷開連接。
二、握手成功后瀏覽器的工作
當整個握手階段無任何異常時,服務器與瀏覽器已經建立連接,此時瀏覽器會完成一些初始化工作。
首先將readyState屬性設置為OPEN(WebSocket的常量,值為1),readyState是WebSocket的一個屬性,代表連接的狀態,包含CONNECTING、OPEN、CLOSING、CLOSED等4個狀態。其中OPEN即連接已建立,可以進行數據通信了。
如果extensions非空,則設置此屬性的值。
如果protocol的值非空,則設置此屬性的值。
如果返回了cookie設置的響應頭,則需要將指定的cookie設置到WebSocket構造函數的第二個參數URL下。
最后觸發WebSocket的onopen事件處理函數。
三、WebSocket對象的屬性及方法介紹
調用WebSocket構造方法后,會返回一個WebSocket對象,類似如下:
URL: "ws://localhost/"
binaryType: "blob"
bufferedAmount: 0
extensions: ""
onclose: null
onerror: null
onmessage: null
onopen: null
protocol: ""
readyState: 3
url: "ws://localhost/"
CLOSED: 3
CLOSING: 2
CONNECTING: 0
OPEN: 1
close: function close() { [native code] }
send: function send() { [native code] }
binaryType為二進制使用哪種類型的數據結構,取值可以是blob或arraybuffer.
bufferedAmount為緩存中剩余的字節數,有時數據並不是准備好才傳輸到網絡上,而是一邊生成一邊傳遞,這時需要將數據緩存到一定大小再發送,並且需要定時將緩存中剩余的內容發送出去。bufferedAmount就記錄了send函數中的應用數據(UTF-8的字符串或二進制的數據)隊列中還未被發送到網絡的字節數。如果連接關閉,該屬性值會在send方法被調用時增長(一旦連接關閉,該屬性值就不會重置為0)
onclose在連接關閉時觸發
onerror在發生錯誤時觸發
onmessage在瀏覽器接到服務器發來的數據時觸發
onopen在連接建立時觸發
readyState連接狀態,上文中已介紹
CLOSED連接已經關閉
CLOSING連接關閉中,此時處於半關閉狀態,瀏覽器已經發送了關閉指令,但此時服務器還未響應關閉指令。瀏覽器不能再收發數據了。
CONNECTING正在連接中,瀏覽器已經發送了握手請求,等待服務器返回握手響應。
OPEN已建立連接,可以傳輸數據。
close方法用於瀏覽器主動關閉連接,它會發送操作碼8到服務器,它接受2個可選參數,關閉碼和關閉原因。有關close的詳細用法將在以后的文章中詳細介紹。
send方法用於瀏覽器向服務器發送數據,它支持多種數據類型的傳輸,send的用法也會在后面的文章中詳細介紹。
下篇會詳細介紹WebSocket數據幀的格式,主要針對RFC6455的5.2節(基礎數據幀協議)進行翻譯和介紹。
2015年1月22日WebSocket 留下評論
WebSocket協議詳解及應用(三)-WebSocket握手協議之服務器響應
本文主要介紹服務端應用在接收到WebSocket握手后,如何構造正確的響應包。本文部分翻譯自rfc6455。
一、響應包Sec-WebSocket-Accept字段值的計算
上篇說道瀏覽器發送的最重要的一個請求頭是Sec-WebSocket-Key,服務端程序需要根據RFC6455中的算法計算Sec-WebSocket-Accept的值。我們以瀏覽器發送了Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==為例,介紹如何計算響應值。
首先服務端程序要將Sec-WebSocket-Key的值與一個魔法字符串”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″拼接到一起,得到”dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11″,關於魔法字符串的來歷,有興趣的同學可以看一下RFC4122(實際是一個GUID)。
第二步,將上一步中合並的字符串使用sha1計算sha1值,這里如果使用PHP的sha1函數進行計算,要注意sha1的第二個參數必須顯式的給出true值,否則sha1的結果是一個16進制的字符串,而不是二進制數值,其他語言如果有類似的情況也要注意,求出的結果是二進制,而不是轉換后的16進制值。
第三步,將第二步中的二進制值使用base64進行編碼,使用PHP計算方法如下:base64_encode(sha1($secWebSocketKey."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
最后得出的結果為”s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”,將這個結果作為Sec-WebSocket-Accept的值,放入響應頭中。
二、響應頭的構成
1.HTTP狀態碼
握手響應的HTTP狀態碼為101 Switching Protocols,代表協議轉換,如HTTP/1.1 101 Switching Protocols
2.Upgrade字段
Upgrade: websocket固定,代表轉換為WebSocket協議,同瀏覽器請求頭
3.Connection字段
Connection: Upgrade固定,同瀏覽器請求頭
4.Sec-WebSocket-Accept
通過上面的方法計算出的值
5.Sec-WebSocket-Protocol
可選返回頭,根據瀏覽器發送的子協議返回。如果瀏覽器發送了多個子協議,這里可以選擇一個或多個進行返回,此處是子協議協商的過程
三、服務端響應步驟
- 判斷Origin是否可信,上一篇中說到,WebSocket不存在跨域問題,在任何域中都可以與其他域建立WebSocket連接。但出於某些原因,我們不希望一些其他的網站連接服務器,這時可以驗證Origin是否在訪問源白名單中。
- 通過Cookie驗證身份,由於WebSocket的握手協議仍是通過HTTP協議進行的,它會向服務端發送目標域下(當前運行的WebSocket服務器)的cookie。服務端程序可以通過cookie將該socket與用戶身份綁定到一起,就不需要在以后傳輸身份信息
- 讀取Sec-WebSocket-Version的值判斷協議版本,如果不是13,則需要用其他方法解析(本系列只介紹13版本,其他版本的握手及通信不在介紹范圍內)
- 通過Sec-WebSocket-Key計算Sec-WebSocket-Accept的值
- 讀取Sec-WebSocket-Protocol,根據服務器實現情況返回支持的一個或多個子協議名,使用半角逗號分隔
- 將第二節中的響應頭發送給瀏覽器
四、關於Sec-WebSocket-Extensions
在WebSocket的請求和響應頭中,還有一個可選的字段Sec-WebSocket-Extensions,目前很少使用,現將RFC6455內容翻譯如下:
“Sec-WebSocket-Extensions”字段僅用在WebSocket連接握手階段。它先由客戶端(瀏覽器)發送到服務器,然后再由服務器傳回客戶端,以便協商連接過程中的協議層擴展集。
“Sec-WebSocket-Extensions”字段可能在HTTP請求中出現多次(等價於出現一次,但有多個值),但是最多只能在HTTP響應中出現一次。
五、握手階段的錯誤處理
WebSocket協議中並未明確規定當服務器遇到錯誤(偽造)的握手請求該如何處理,你可以直接斷開客戶端的連接,但這樣並不是比較好的方法,推薦使用下面的做法(瀏覽器會忽略出101之外的狀態碼,返回非101狀態碼瀏覽器會自動切斷連接):
1.當客戶端請求中缺少host、Upgrade、Connection、Sec-WebSocket-Key、Sec-WebSocket-Version時返回狀態碼400(語法格式錯誤)
2.當Origin不在可信源中時,返回403(權限)
3.當身份驗證(cookie)出錯時,返回401或403
4.當version或protocol不支持時,返回501(尚未實現)
下一篇將介紹瀏覽器在收到服務器返回的響應頭后會做些什么,以及響應錯誤的情況及處理
2015年1月21日WebSocket 留下評論
WebSocket協議詳解及應用(二)-WebSocket握手協議之瀏覽器請求
目前WebSocket協議版本已更新到13,本系列文章均以WebSocket 13版本為例。
一、通過WebSocket API與服務器進行握手
WebSocket的構造方法中有兩個參數,其原型如下:WebSocket(url, [protocols])
,url為需要連接的地址。WebSocket的協議頭的寫法有2個,一個是ws://
,另一個是wss://
,它們的區別就是后者相當於https,是加密的。如果url沒有加端口號,當協議頭為ws時默認為80端口,當協議頭為wss時默認為443端口。如果需要連接其他端口,則需要向http協議一樣,加上端口號,如ws://localhost:12345
。url其他部分與標准URL一致,以下URL是合法的:ws://localhost:12345/path/?queryString=queryString
。具體URL標准將在下面協議詳解中詳細說明。
第二個參數protocols為可選參數,代表WebSocket協議的子協議(可以是用戶自定義的)。你可能已經注意到了第二個參數起名為protocols最后有個s,它可以是一個字符串亦可以是一個數組,如果protocols是一個字符串,則它等價於一個單一值的數組。例如,WebSocket('ws://localhost','subprotocol')
等價於WebSocket('ws://localhost',['subprotocol'])
二、WebSocket URL解析
以下內容部分翻譯自W3C規范
WebSocket URL解析組件解析URL步驟如下,這些步驟會返回一個主機名、端口號、資源名和一個安全標識符否則返回失敗:
- 如果URL不是一個絕對URL,算法失敗
- 將URL字符串轉化為UTF-8格式
- 如果URL的模式不是ws或wss,則算法失敗
- 如果解析的URL結果中存在非空的片段,則算法失敗
- 如果是ws模式的URL,則將安全標識符設為假,如果是wss模式的URL則將安全標示符置為真
- 解析host名
- 如果為顯示的聲明端口號,則認為端口號是隱式聲明的
- 如果端口號是隱式聲明的,當安全標示符為真時,端口號是443,否則為80
- 如果資源名是非空的,則將其設置到結果中,否則將/作為資源名
- 如果URL請求字符串非空,則將請求字符串加入結果中,並使用?拼接(同HTTP中URL的請求字符串寫法)
- 將主機名、端口號、資源名和安全標示符返回
三、瀏覽器發送握手包
通過本文第一部分我們可知,瀏覽器端發送握手請求的API非常簡單,只需要new出一個WebSocket的對象,最少情況只需要帶一個參數,如var ws = new WebSocket('ws://localhost');
我們在http://dev.w3.org中打開控制台使用var ws = new WebSocket('ws://localhost','test');
瀏覽器會發出類似如下請求(注意:這里不會存在跨域問題,不需要在同域下即可,因為WebSocket是在TCP層面上傳輸數據,而”域”的概念在應用層):
GET ws://localhost/ HTTP/1.1
Host: localhost
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://dev.w3.org
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol:test
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4
Sec-WebSocket-Key: 32pdAhmqFrFZik/MP7fU8A==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
其實這個就是一個標准的HTTP協議,WebSocket協議中的握手過程是HTTP協議,而一旦握手成功后,就不在是HTTP協議,而是直接通過TCP傳輸數據。
請求頭中大部分內容與HTTP協議一致,這里不再解釋,只解釋那些不曾出現在普通請求的請求頭。
Connection: Upgrade是固定的,表示需要轉換為其他協議
Upgrade: websocket是固定的,說明要轉換成的協議時WebSocket
Sec-WebSocket-Version: 13代表WebSocket的版本,目前版本是13
Sec-WebSocket-Key: 32pdAhmqFrFZik/MP7fU8A==這個是握手的認證串,服務端需要將此key進行一定處理后返回,再由瀏覽器驗證有效性,必須符合算法結果才可正常建立連接(類似交換證書,但實際上只是簡單的哈希計算)
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits代表客戶端支持的擴展類型
Sec-WebSocket-Protocol:test為子協議,是否存在取決於構造方法的第二個參數,如果第二個參數是個數組,則此處的值為數組按一個逗號和一個空格分隔,相當於[].join(', ');
其中Sec-WebSocket-Key是非常重要的請求頭,我們會在服務端處理這個請求頭,只有處理正確,瀏覽器才會正確的與服務器建立連接。
下一篇將會介紹服務器端如何處理瀏覽器的請求頭,並返回正確響應頭以及如何處理錯誤的請求頭
2015年1月20日WebSocket 留下評論
WebSocket協議詳解及應用(一)-初識WebSocket
一、什么是WebSocket
WebSocket是一個允許Web應用程序(通常指瀏覽器)與服務器進行雙向通信的協議。HTML5的WebSocket API主要是為瀏覽器端提供了一個基於TCP協議實現全雙工通信的方法。
二、優點
1.全雙工通信
在傳統的Web應用中,瀏覽器與服務器交互都是半雙工通信(但並不完全是半雙工通信,服務器無法主動向瀏覽器推送)。即同一時間內數據流向是單一的,瀏覽器向服務器發送請求后需要等待服務器返回數據。
而在WebSocket中,瀏覽器和服務器之間可隨時進行通信,不必等待對方傳送完畢,瀏覽器接收到服務器的數據后會自動觸發onmessage事件。
2.實時性
傳統的Web應用很難做到實時通信,通常是用長連接或輪詢的方式進行。對於服務器來說,輪詢法是被動傳輸數據,即使數據有更新,但瀏覽器還未發送請求,則消息無法進行實時推送。
3.更少的數據傳輸及更少的請求數
傳統Web應用中瀏覽器與服務器進行數據交互通常需要經過以下幾個步驟:
- DNS查詢
- TCP三次握手
- 傳送HTTP請求頭
- 傳送HTTP請求體(如果有)
- 服務器處理后傳送響應頭
- 服務器傳送響應體
- 斷開TCP連接
在WebSocket中進行交互通常為以下幾個步驟:
- DNS查詢
- TCP三次握手
- WebSocket握手
- 瀏覽器發送請求
- 服務器發送響應
- 斷開TCP連接
從上面可以看到如果僅是一次通信,二者差異並不是很大,甚至WebSocket比普通方式還要多一次握手。但在需要頻繁交互數據時,WebSocket的優勢就顯露出來了。
例如,當有10次數據交互時,前者要建立10個TCP連接(HTTP 1.0需要建立10次,HTTP 1.1可以通過長連接keep-alive復用TCP連接),然后要發送10次請求頭(包含Cookie等信息,可能會達到K級別),接收的響應信息可能才幾個字節(如某些心跳包),這樣會極大的浪費帶寬等資源。試想,如果你在做一個聊天應用,想要獲取當前在線人數,你需要向服務器發送你的全部cookie(至少要幾百個字節),除此之外HTTP頭中還要包含其他信息,如URL、host等,這些都是必不可少的。最后服務器返回了幾百個字節,但其中真正需要用到的只有不到10字節(只需要知道在線人數,其他信息都是無用的)。通過WebSocket,瀏覽器可以向服務器發送1~2字節的請求(不需要帶上cookie驗證身份,可以在握手時進行認證,一旦TCP連接建立,則在連接上的通信都是認證過身份的數據,這也是它的好處之一:便於服務端識別客戶端的狀態),這個請求僅包含一個特定的控制碼(由開發者實現的應用層協議指定),服務器只需返回特定的返回碼及數據即可,一切無用的字節都被省去。
三、應用場景
1.數據傳輸實時性要求較高
Web聊天室、貼吧直播貼、微博話題牆、專題討論、實時網絡攻擊展示(已應用)等
2.推送類應用
網站消息通知、郵箱新郵件提醒
3.監控在線狀態、精確統計在線時長
統計用戶行為
4.遠程調試代碼、雲指令系統
部分移動端調試工具是基於WebSocket開發(此處為WebSocket協議而非WebSocket API)
5.其他用途
網絡(包括內網)嗅探(端口掃描)、僵屍網絡及后門(雲指令系統)