那天和boss聊天,不經意間提到了Meteor,然後聊到了WebSocket,然後就有了以下對話,不得不說,看問題的方式不同,看到的東西也會大不相同。
A:Meteor是一個很新的開發框架,我覺得它設計得十分巧妙。
B:怎麼個巧妙之處?
A:它的前後端全部使用JS,做到了真正的前後端統一;前端瀏覽器裡存有一份後臺開放出來的資料庫的拷貝,快;使用WebSocket協議來做資料傳輸協議,來同步前後端的資料庫,實現了真正的實時同步。
B:哦?WebSocket是什麼東西?真實時?那底層是不是還是輪訓?和HTTP的長連線有什麼不同?
A:(開始心虛)它是一個新的基於TCP的應用層協議,只需要一次連線,以後的資料不需要重新建立連線,可以直接傳送,它是基於TCP的,屬於和HTTP相同的地位(呃,開始胡謅了),底層不是輪訓,和長連線的區別……這個就不清楚了。
B:它的傳輸過程大致是什麼樣子的呢?
A:首先握手連線(又是胡謅),好像可以基於HTTP建立連線(之前用過Socket.io,即興胡謅),建立了連線之後就可以傳輸資料了,還包括斷掉之後重連等機制。
B:看起來和HTTP長連線做的事情差不多嘛,好像就是一種基於HTTP和Socket的協議啊。
A:呃……(我還是回去看看書吧)
有時候看事情確實太流於表面,瞭解到了每個事物的大致輪廓,但不求甚解,和朋友聊天說出來也鮮有人會刨根問底,導致了很多基礎知識並不牢靠,於是回來大致把HTTP和WebSocket協議的RFC文件(RFC2616 和 RFC6455),剛好對HTTP的傳輸過程一直有點模糊,這裡把兩個協議的異同總結一下。
協議基礎
仔細去看這兩個協議,其實都非常簡單,但任何一個事情想做到完美都會慢慢地變得異常複雜,各種細節。這裡只會簡單地描述兩個協議的結構,並不會深入到很深的細節之處,對於理解http已經足夠了。
HTTP
HTTP的地址格式如下:
1 2 |
http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]] 協議和host不分大小寫 |
HTTP訊息
一個HTTP訊息可能是request或者response訊息,兩種型別的訊息都是由開始行(start-line),零個或多個header域,一個表示header域結束的空行(也就是,一個以CRLF為字首的空行),一個可能為空的訊息主體(message-body)。一個合格的HTTP客戶端不應該在訊息頭或者尾新增多餘的CRLF,服務端也會忽略這些字元。
header的值不包括任何前導或後續的LWS(線性空白),線性空白可能會出現在域值(filed-value)的第一個非空白字元之前或最後一個非空白字元之後。前導或後續的LWS可能會被移除而不會改變域值的語意。任何出現在filed-content之間的LWS可能會被一個SP(空格)代替。header域的順序不重要,但建議把常用的header放在前邊(協議裡這麼說的)。
Request訊息
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訊息的接收者判斷請求資源的條件,方法如下:
- 如果Request-URI是絕對地址(absoluteURI),這時請求裡的主機存在於Request-URI裡。任何出現在請求裡Host頭域值應當被忽略。
- 假如Request-URI不是絕對地址(absoluteURI),並且請求包括一個Host頭域,則主機由該Host頭域值決定。
- 假如由規則1或規則2定義的主機是一個無效的主機,則應當以一個400(錯誤請求)錯誤訊息返回。
Response訊息
響應訊息跟請求訊息幾乎一模一樣,定義如下:
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來對待。對於不認識的返回碼的響應訊息,不可以快取。
Header
RFC2616中定義了4種header型別,在通訊各方都認可的情況下,請求頭可以被擴充套件的(可信的擴充套件只能等到協議的版本更新),如果接收者收到了一個不認識的請求頭,這個頭將會被當做實體頭。4種頭型別如下:
- 通用頭(General Header Fields):可用於request,也可用於response的頭,但不可作為實體頭,只能作為訊息的頭。
123456789general-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 - 請求頭(Request Header Fields):被請求發起端用來改變請求行為的頭。
12345678910111213141516171819request-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 - 響應頭(Response Header Fields):被伺服器用來對資源進行進一步的說明。
123456789response-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 - 實體頭(Entity Header Fields):如果訊息帶有訊息體,實體頭用來作為元資訊;如果沒有訊息體,就是為了描述請求的資源的資訊。
1234567891011entity-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
訊息體(Message Body)和實體主體(Entity Body)
如果有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不會帶有訊息體。
訊息體的長度
訊息體長度的確定有一下幾個規則,它們順序執行:
- 所有不應該返回內容的Response訊息都不應該帶有任何的訊息體,訊息會在第一個空行就被認為是終止了。
- 如果訊息頭含有
Transfer-Encoding
,且它的值不是identity
,那麼訊息體的長度會使用chunked
方式解碼來確定,直到連線終止。 - 如果訊息頭中有
Content-Length
,那麼它就代表了entity-length
和transfer-length
。如果同時含有Transfer-Encoding
,則entity-length
和transfer-length
可能不會相等,那麼Content-Length
會被忽略。 - 如果訊息的媒體型別是
multipart/byteranges
,並且transfer-length
也沒有指定,那麼傳輸長度由這個媒體自己定義。通常是收發雙發定義好了格式, HTTP1.1客戶端請求裡如果出現Range頭域並且帶有多個位元組範圍(byte-range)指示符,這就意味著客戶端能解析multipart/byteranges
響應。 - 如果是Response訊息,也可以由伺服器來斷開連線,作為訊息體結束。
從訊息體中得到實體主體,它的型別由兩個header來定義,Content-Type
和Content-Encoding
(通常用來做壓縮)。如果有實體主體,則必須有Content-Type
,如果沒有,接收方就需要猜測,猜不出來就是用application/octet-stream
。
HTTP連線
HTTP1.1的連線預設使用持續連線(persistent connection),持續連線指的是,有時是客戶端會需要在短時間內向服務端請求大量的相關的資源,如果不是持續連線,那麼每個資源都要建立一個新的連線,HTTP底層使用的是TCP,那麼每次都要使用三次握手建立TCP連線,將造成極大的資源浪費。
持續連線可以帶來很多的好處:
- 使用更少的TCP連線,對通訊各方的壓力更小。
- 可以使用管道(pipeline)來傳輸資訊,這樣請求方不需要等待結果就可以傳送下一條資訊,對於單個的TCP的使用更充分。
- 流量更小
- 順序請求的延時更小。
- 不需要重新建立TCP連線就可以傳送error,關閉連線等資訊。
HTTP1.1的伺服器使用TCP的流量控制來控制HTTP的流量,HTTP1.1的客戶端在收到伺服器連線中發過來的error資訊,就要馬上關閉此連結。關於HTTP連線還有很多細節,之後再詳述。
WebSocket
只從RFC釋出的時間看來,WebSocket要晚近很多,HTTP 1.1是1999年,WebSocket則是12年之後了。WebSocket協議的開篇就說,本協議的目的是為了解決基於瀏覽器的程式需要拉取資源時必須發起多個HTTP請求和長時間的輪訓的問題……而建立的。
待續
本來是打算在一篇文章裡把HTTP和WebSocket兩個協議的大致細節理出來,然後進行對比。可是寫著寫著就發現篇幅可能會比較長,讀起來就不那麼友好了,那麼剛好就再寫第二篇吧。第二篇裡會將WebSocket的大致情況描述一下,然後和HTTP適用的場景進行對比。