A:Meteor是一個很新的開發框架,我以爲它設計得十分巧妙。前端
B:怎麼個巧妙之處?web
A:它的先後端所有使用JS,作到了真正的先後端統一;前端瀏覽器裏存有一份後臺開放出來的數據庫的拷貝,快;使用WebSocket協議來作數據傳輸協議,來同步先後端的數據庫,實現了真正的實時同步。數據庫
B:哦?WebSocket是什麼東西?真實時?那底層是否是仍是輪訓?和HTTP的長鏈接有什麼不一樣?後端
A:(開始心虛)它是一個新的基於TCP的應用層協議,只須要一次鏈接,之後的數據不須要從新創建鏈接,能夠直接發送,它是基於TCP的,屬於和HTTP相同的地位(呃,開始胡謅了),底層不是輪訓,和長鏈接的區別……這個就不清楚了。瀏覽器
B:它的傳輸過程大體是什麼樣子的呢?緩存
A:首先握手鍊接(又是胡謅),好像能夠基於HTTP創建鏈接(以前用過Socket.io,即興胡謅),創建了鏈接以後就能夠傳輸數據了,還包括斷掉以後重連等機制。安全
B:看起來和HTTP長鏈接作的事情差很少嘛,好像就是一種基於HTTP和Socket的協議啊。服務器
A:呃……(我仍是回去看看書吧)websocket
有時候看事情確實太流於表面,瞭解到了每一個事物的大體輪廓,但不求甚解,和朋友聊天說出來也鮮有人會刨根問底,致使了不少基礎知識並不牢靠,因而回來大體把HTTP和WebSocket協議的RFC文檔(RFC2616 和 RFC6455),恰好對HTTP的傳輸過程一直有點模糊,這裏把兩個協議的異同總結一下。網絡
仔細去看這兩個協議,其實都很是簡單,但任何一個事情想作到完美都會慢慢地變得異常複雜,各類細節。這裏只會簡單地描述兩個協議的結構,並不會深刻到很深的細節之處,對於理解http已經足夠了。
HTTP的地址格式以下:
1 2 |
http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]] 協議和host不分大小寫 |
一個HTTP消息多是request或者response消息,兩種類型的消息都是由開始行(start-line),零個或多個header域,一個表示header域結束的空行(也就是,一個以CRLF爲前綴的空行),一個可能爲空的消息主體(message-body)。一個合格的HTTP客戶端不該該在消息頭或者尾添加多餘的CRLF,服務端也會忽略這些字符。
header的值不包括任何前導或後續的LWS(線性空白),線性空白可能會出如今域值(filed-value)的第一個非空白字符以前或最後一個非空白字符以後。前導或後續的LWS可能會被移除而不會改變域值的語意。任何出如今filed-content之間的LWS可能會被一個SP(空格)代替。header域的順序不重要,但建議把經常使用的header放在前邊(協議裏這麼說的)。
RFC2616中這樣定義HTTP Request 消息:
1 2 3 4 5 6 |
Request = Request-Line *(( general-header | request-header(跟本次請求相關的一些header) | entity-header ) CRLF)(跟本次請求相關的一些header) CRLF [ message-body ] |
一個HTTP的request消息以一個請求行開始,從第二行開始是header,接下來是一個空行,表示header結束,最後是消息體。
請求行的定義以下:
1 2 3 4 5 6 7 8 |
//請求行的定義 Request-Line = Method SP Request-URL SP HTTP-Version CRLF
//方法的定義 Method = "OPTIONS" | "GET" | "HEAD" |"POST" |"PUT" |"DELETE" |"TRACE" |"CONNECT" | extension-method
//資源地址的定義 Request-URI ="*" | absoluteURI | abs_path | authotity(CONNECT) |
Request消息中使用的header能夠是general-header或者request-header,request-header(後邊會講解)。其中有一個比較特殊的就是Host,Host會與reuqest Uri一塊兒來做爲Request消息的接收者判斷請求資源的條件,方法以下:
響應消息跟請求消息幾乎如出一轍,定義以下:
1 2 3 4 5 6 |
Response = Status-Line *(( general-header | response-header | entity-header ) CRLF) CRLF [ message-body ] |
能夠看到,除了header不使用request-header以外,只有第一行不一樣,響應消息的第一行是狀態行,其中就包含大名鼎鼎的返回碼。
Status-Line的內容首先是協議的版本號,而後跟着返回碼,最後是解釋的內容,它們之間各有一個空格分隔,行的末尾以一個回車換行符做爲結束。定義以下:
1 |
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF |
返回碼是一個3位數,第一位定義的返回碼的類別,總共有5個類別,它們是:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- 1xx: Informational - Request received, continuing process
- 2xx: Success - The action was successfully received, understood, and accepted
- 3xx: Redirection - Further action must be taken in order to complete the request
- 4xx: Client Error - The request contains bad syntax or cannot be fulfilled
- 5xx: Server Error - The server failed to fulfill an apparently valid request |
RFC2616中接着又給出了一系列返回碼的擴展,這些都是咱們平時會用到的,可是那些只是示例,HTTP1.1不強制通訊各方遵照這些擴展的返回碼,通訊各方在返回碼的實現上只須要遵照以上邊定義的這5種類別的定義,意思就是,返回碼的第一位要嚴格按照文檔中所述的來,其餘的隨便定義。
任何人接收到一個不認識的返回碼xyz,均可以把它當作x00來對待。對於不認識的返回碼的響應消息,不能夠緩存。
RFC2616中定義了4種header類型,在通訊各方都承認的狀況下,請求頭能夠被擴展的(可信的擴展只能等到協議的版本更新),若是接收者收到了一個不認識的請求頭,這個頭將會被當作實體頭。4種頭類型以下:
1.通用頭(General Header Fields):可用於request,也可用於response的頭,但不可做爲實體頭,只能做爲消息的頭。
1 2 3 4 5 6 7 8 9 |
general-header = Cache-Control ; Section 14.9 | Connection ; Section 14.10 | Date ; Section 14.18 | Pragma ; Section 14.32 | Trailer ; Section 14.40 | Transfer-Encoding ; Section 14.41 | Upgrade ; Section 14.42 | Via ; Section 14.45 | Warning ; Section 14.46 |
2.請求頭(Request Header Fields):被請求發起端用來改變請求行爲的頭。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
request-header = Accept ; Section 14.1 | Accept-Charset ; Section 14.2 | Accept-Encoding ; Section 14.3 | Accept-Language ; Section 14.4 | Authorization ; Section 14.8 | Expect ; Section 14.20 | From ; Section 14.22 | Host ; Section 14.23 | If-Match ; Section 14.24 | If-Modified-Since ; Section 14.25 | If-None-Match ; Section 14.26 | If-Range ; Section 14.27 | If-Unmodified-Since ; Section 14.28 | Max-Forwards ; Section 14.31 | Proxy-Authorization ; Section 14.34 | Range ; Section 14.35 | Referer ; Section 14.36 | TE ; Section 14.39 | User-Agent ; Section 14.43 |
3.響應頭(Response Header Fields):被服務器用來對資源進行進一步的說明。
1 2 3 4 5 6 7 8 9 |
response-header = Accept-Ranges ; Section 14.5 | Age ; Section 14.6 | ETag ; Section 14.19 | Location ; Section 14.30 | Proxy-Authenticate ; Section 14.33 | Retry-After ; Section 14.37 | Server ; Section 14.38 | Vary ; Section 14.44 | WWW-Authenticate ; Section 14.47 |
4.實體頭(Entity Header Fields):若是消息帶有消息體,實體頭用來做爲元信息;若是沒有消息體,就是爲了描述請求的資源的信息。
1 2 3 4 5 6 7 8 9 10 11 |
entity-header = Allow ; Section 14.7 | Content-Encoding ; Section 14.11 | Content-Language ; Section 14.12 | Content-Length ; Section 14.13 | Content-Location ; Section 14.14 | Content-MD5 ; Section 14.15 | Content-Range ; Section 14.16 | Content-Type ; Section 14.17 | Expires ; Section 14.21 | Last-Modified ; Section 14.29 | extension-header |
若是有Transfer-Encoding頭,那麼消息體解碼完了就是實體主體,若是沒有Transfer-Encoding頭,消息體就是實體主體。
1 2 |
message-body = entity-body | <entity-body encoded as per Transfer-Encoding> |
在request消息中,消息頭中含有Content-Length或者Transfer-Encoding,標識會有一個消息體跟在後邊。若是請求的方法不該該含有消息體(如OPTION),那麼request消息必定不能含有消息體,即便客戶端發送過去,服務器也不會讀取消息體。
在response消息中,是否存在消息體由請求方法和返回碼來共同決定。像1xx,204,304不會帶有消息體。
消息體長度的肯定有一下幾個規則,它們順序執行:
從消息體中獲得實體主體,它的類型由兩個header來定義,Content-Type和Content-Encoding(一般用來作壓縮)。若是有實體主體,則必須有Content-Type,若是沒有,接收方就須要猜想,猜不出來就是用application/octet-stream。
HTTP1.1的鏈接默認使用持續鏈接(persistent connection),持續鏈接指的是,有時是客戶端會須要在短期內向服務端請求大量的相關的資源,若是不是持續鏈接,那麼每一個資源都要創建一個新的鏈接,HTTP底層使用的是TCP,那麼每次都要使用三次握手創建TCP鏈接,將形成極大的資源浪費。
持續鏈接能夠帶來不少的好處:
HTTP1.1的服務器使用TCP的流量控制來控制HTTP的流量,HTTP1.1的客戶端在收到服務器鏈接中發過來的error信息,就要立刻關閉此連接。關於HTTP鏈接還有不少細節,以後再詳述。
只從RFC發佈的時間看來,WebSocket要晚近不少,HTTP 1.1是1999年,WebSocket則是12年以後了。WebSocket協議的開篇就說,本協議的目的是爲了解決基於瀏覽器的程序須要拉取資源時必須發起多個HTTP請求和長時間的輪訓的問題……而建立的。
WebSocket協議還很年輕,RFC文檔相比HTTP的發佈時間也很短,它的誕生是爲了建立一種「雙向通訊」的協議,來做爲HTTP協議的一個替代者。那麼首先看一下它和HTTP(或者HTTP的長鏈接)的區別。
上一篇中提到WebSocket的目的就是解決網絡傳輸中的雙向通訊的問題,HTTP1.1默認使用持久鏈接(persistent connection),在一個TCP鏈接上也能夠傳輸多個Request/Response消息對,可是HTTP的基本模型仍是一個Request對應一個Response。這在雙向通訊(客戶端要向服務器傳送數據,同時服務器也須要實時的向客戶端傳送信息,一個聊天系統就是典型的雙向通訊)時通常會使用這樣幾種解決方案:
HTTP的長鏈接模型
WebSocket的目的是取代HTTP在雙向通訊場景下的使用,並且它的實現方式有些也是基於HTTP的(WS的默認端口是80和443)。現有的網絡環境(客戶端、服務器、網絡中間人、代理等)對HTTP都有很好的支持,因此這樣作能夠充分利用現有的HTTP的基礎設施,有點向下兼容的意味。
簡單來說,WS協議有兩部分組成:握手和數據傳輸。
出於兼容性的考慮,WS的握手使用HTTP來實現(此文檔中提到將來有可能會使用專用的端口和方法來實現握手),客戶端的握手消息就是一個「普通的,帶有Upgrade頭的,HTTP Request消息」。因此這一個小節到內容大部分都來自於RFC2616,這裏只是它的一種應用形式,下面是RFC6455文檔中給出的一個客戶端握手消息示例:
1 2 3 4 5 6 7 8 |
GET /chat HTTP/1.1 //1 Host: server.example.com //2 Upgrade: websocket //3 Connection: Upgrade //4 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== //5 Origin: http://example.com //6 Sec-WebSocket-Protocol: chat, superchat //7 Sec-WebSocket-Version: 13 //8 |
能夠看到,前兩行跟HTTP的Request的起始行如出一轍,而真正在WS的握手過程當中起到做用的是下面幾個header域。
Upgrade:upgrade是HTTP1.1中用於定義轉換協議的header域。它表示,若是服務器支持的話,客戶端但願使用現有的「網絡層」已經創建好的這個「鏈接(此處是TCP鏈接)」,切換到另一個「應用層」(此處是WebSocket)協議。
Connection:HTTP1.1中規定Upgrade只能應用在「直接鏈接」中,因此帶有Upgrade頭的HTTP1.1消息必須含有Connection頭,由於Connection頭的意義就是,任何接收到此消息的人(每每是代理服務器)都要在轉發此消息以前處理掉Connection中指定的域(不轉發Upgrade域)。
若是客戶端和服務器之間是經過代理鏈接的,那麼在發送這個握手消息以前首先要發送CONNECT消息來創建直接鏈接。
Sec-WebSocket-*:第7行標識了客戶端支持的子協議的列表(關於子協議會在下面介紹),第8行標識了客戶端支持的WS協議的版本列表,第5行用來發送給服務器使用(服務器會使用此字段組裝成另外一個key值放在握手返回信息裏發送客戶端)。
Origin:做安全使用,防止跨站攻擊,瀏覽器通常會使用這個來標識原始域。
若是服務器接受了這個請求,可能會發送以下這樣的返回信息,這是一個標準的HTTP的Response消息。101表示服務器收到了客戶端切換協議的請求,而且贊成切換到此協議。RFC2616規定只有切換到的協議「比HTTP1.1更好」的時候才能贊成切換。
1 2 3 4 5 |
HTTP/1.1 101 Switching Protocols //1 Upgrade: websocket. //2 Connection: Upgrade. //3 Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= //4 Sec-WebSocket-Protocol: chat. //5 |
ws協議默認使用80端口,wss協議默認使用443端口。
1 2 3 4 5 6 7 |
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
host = <host, defined in [RFC3986], Section 3.2.2> port = <port, defined in [RFC3986], Section 3.2.3> path = <path-abempty, defined in [RFC3986], Section 3.3> query = <query, defined in [RFC3986], Section 3.4> |
在握手以前,客戶端首先要先創建鏈接,一個客戶端對於一個相同的目標地址(一般是域名或者IP地址,不是資源地址)同一時刻只能有一個處於CONNECTING狀態(就是正在創建鏈接)的鏈接。從創建鏈接到發送握手消息這個過程大體是這樣的:
1 2 |
CONNECT example.com:80 HTTP/1.1 Host: example.com |
若是客戶端沒有處於代理環境中,它就要首先創建一個到達目標地址的直接的TCP鏈接。
4.若是上一步中的TCP鏈接創建失敗,則此WebSocket鏈接失敗。
5.若是協議是wss,則在上一步創建的TCP鏈接之上,使用TSL發送握手信息。若是失敗,則此WebSocket鏈接失敗;若是成功,則之後的全部數據都要經過此TSL通道進行發送。
1 2 |
ws://example.com/chat GET /chat HTTP/1.1 |
3.此Request消息中Request-URI部分(RFC2616中的概念)所定義的資型必須和WS協議的Uri中定義的資源相同。
4.此Request消息中必須含有Host頭域,其內容必須和WS的Uri中定義的相同。
5.此Request消息必須包含Upgrade頭域,其內容必須包含websocket關鍵字。
6.此Request消息必須包含Connection頭域,其內容必須包含Upgrade指令。
7.此Request消息必須包含Sec-WebSocket-Key頭域,其內容是一個Base64編碼的16位隨機字符。
8.若是客戶端是瀏覽器,此Request消息必須包含Origin頭域,其內容是參考RFC6454。
9.此Request消息必須包含Sec-WebSocket-Version頭域,在此協議中定義的版本號是13。
10.此Request消息可能包含Sec-WebSocket-Protocol頭域,其意義如上文中所述。
11.此Request消息可能包含Sec-WebSocket-Extensions頭域,客戶端和服務器可使用此header來進行一些功能的擴展。
12.此Request消息可能包含任何合法的頭域。如RFC2616中定義的那些。
服務端指的是全部參與處理WebSocket消息的基礎設施,好比若是某服務器使用Nginx(A)來處理WebSocket,而後把處理後的消息傳給響應的服務器(B),那麼A和B都是這裏要討論的服務端的範疇。
若是請求是HTTPS,則首先要使用TLS進行握手,若是失敗,則關閉鏈接,若是成功,則以後的數據都經過此通道進行發送。
以後服務端能夠進行一些客戶端驗證步驟(包括對客戶端header域的驗證),若是須要,則按照RFC2616來進行錯誤碼的返回。
若是一切都成功,則返回成功的Response握手消息。
此握手消息是一個標準的HTTP Response消息,同時它包含了如下幾個部分:
一旦這個握手發出去,服務端就認爲此WebSocket鏈接已經創建成功,處於OPEN狀態。它就能夠開始發送數據了。
Sec-WebSocket-Version能夠被通訊雙方用來支持更多的協議的擴展,RFC6455中定義的值爲13,WebSocket的客戶端和服務端可能回自定義更多的版本號來支持更多的功能。其使用方法如上文所述。
WebSocket中全部發送的數據使用幀的形式發送。客戶端發送的數據幀都要通過掩碼處理,服務端發送的全部數據幀都不能通過掩碼處理。不然對方須要發送關閉幀。
一個幀包含一個幀類型的標識碼,一個負載長度,和負載。負載包括擴展內容和應用內容。
幀類型是由一個4位長的叫Opcode的值表示,任何WebSocket的通訊方收到一個位置的幀類型,都要以鏈接失敗的方式斷開此鏈接。
RFC6455中定義的幀類型以下所示:
1.Opcode == 0 繼續
表示此幀是一個繼續幀,須要拼接在上一個收到的幀以後,來組成一個完整的消息。因爲這種解析特性,非控制幀的發送和接收必須是相同的順序。
2.Opcode == 1 文本幀
3.Opcode == 2 二進制幀
4.Opcode == 3 – 7 將來使用(非控制幀)
5.Opcode == 8 關閉鏈接(控制幀)
此幀可能會包含內容,以表示關閉鏈接的緣由。
通訊的某一方發送此幀來關閉WebSocket鏈接,收到此幀的一方若是以前沒有發送此幀,則須要發送一個一樣的關閉幀以確認關閉。若是雙方同時發送此幀,則雙方都須要發送迴應的關閉幀。
理想狀況服務端在確認WebSocket鏈接關閉後,關閉相應的TCP鏈接,而客戶端須要等待服務端關閉此TCP鏈接,但客戶端在某些狀況下也能夠關閉TCP鏈接。
6.Opcode == 9 Ping
相似於心跳,一方收到Ping,應當當即發送Pong做爲響應。
7.Opcode == 10 Pong
若是通訊一方並無發送Ping,可是收到了Pong,並不要求它返回任何信息。Pong幀的內容應當和收到的Ping相同。可能會出現一方收到不少的Ping,可是隻須要響應最近的那一次就能夠了。
8.Opcode == 11 – 15 將來使用(控制幀)
具體的每一項表明什麼意思在這裏就不作詳細的闡述了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 ... | +---------------------------------------------------------------+ |
一樣做爲應用層的協議,WebSocket在現代的軟件開發中被愈來愈多的實踐,和HTTP有不少類似的地方,這裏將它們簡單的作一個純我的、非權威的比較:
相同點