當(dāng) app 和服務(wù)器進(jìn)行通信的時(shí)候,大多數(shù)情況下,都是采用 HTTP 協(xié)議。HTTP 最初是為 web 瀏覽器而定制的,如果在瀏覽器里輸入 http://www.objc.io ,瀏覽器會(huì)通過 HTTP 協(xié)議和 www.objc.io 所對(duì)應(yīng)的服務(wù)器進(jìn)行通信。
HTTP是運(yùn)行在應(yīng)用層上的應(yīng)用協(xié)議,而不同的層級(jí)上都有相應(yīng)的協(xié)議在運(yùn)行。層級(jí)的堆棧關(guān)系一般可以這么描述:
Application Layer -- e.g. HTTP
----
Transport Layer -- e.g. TCP
----
Internet Layer -- e.g. IP
----
Link Layer -- e.g. IEEE 802.2
所謂的 OSI(Open Systems Interconnection,開放式系統(tǒng)互聯(lián))模型定義了七層結(jié)構(gòu)。本文會(huì)關(guān)注應(yīng)用層 (application layer)、傳輸層 (transport layer) 和網(wǎng)絡(luò)層 (internet layer),它們分別代表了典型的 HTTP 的應(yīng)用的 HTTP,TCP 以及 IP。在 IP 之下的是數(shù)據(jù)連接和物理層級(jí),比如像 Ethernet 的實(shí)現(xiàn)之類的東西(Ethernet 擁有一個(gè)數(shù)據(jù)連接部分以及一個(gè)物理部分)。
如上文所述,我們只關(guān)注應(yīng)用層,傳輸層和互聯(lián)網(wǎng)層的部分,更確切的說,著重探討一種特殊的混合模式:基于 IP 的 TCP,以及基于 TCP 實(shí)現(xiàn)的 HTTP。這就是我們每天使用的 app 的基本網(wǎng)絡(luò)配置。
通過本文,希望大家能夠?qū)TTP工作原理有一個(gè)細(xì)致的了解,知道一些常見的 HTTP 問題的產(chǎn)生原因,從而能在實(shí)踐中盡量避免這些問題的發(fā)生。
其實(shí)在互聯(lián)網(wǎng)上傳遞數(shù)據(jù)的方式并不只 HTTP 一種。HTTP 之所以被廣泛使用的原因是其非常穩(wěn)定、易用,即便是防火墻一般也是允許 HTTP 協(xié)議穿透的。
接下來我們從最低的一層談起,說說 IP 網(wǎng)絡(luò)協(xié)議。
TCP/IP 中的 IP 是網(wǎng)絡(luò)協(xié)議 (Internet Protocol) 的縮寫。從字面意思便知,它是互聯(lián)網(wǎng)眾多協(xié)議的基礎(chǔ)。
IP 實(shí)現(xiàn)了分組交換網(wǎng)絡(luò)。在協(xié)議下,機(jī)器被叫做 主機(jī) (host),IP 協(xié)議明確了 host 之間的資料包(數(shù)據(jù)包)的傳輸方式。
所謂數(shù)據(jù)包是指一段二進(jìn)制數(shù)據(jù),其中包含了發(fā)送源主機(jī)和目標(biāo)主機(jī)的信息。IP 網(wǎng)絡(luò)負(fù)責(zé)源主機(jī)與目標(biāo)主機(jī)之間的數(shù)據(jù)包傳輸。IP 協(xié)議的特點(diǎn)是 best effort(盡力服務(wù),其目標(biāo)是提供有效服務(wù)并盡力傳輸)。這意味著,在傳輸過程中,數(shù)據(jù)包可能會(huì)丟失,也有可能被重復(fù)傳送導(dǎo)致目標(biāo)主機(jī)收到多個(gè)同樣的數(shù)據(jù)包。
IP 網(wǎng)絡(luò)中的主機(jī)都配有自己的地址,被稱為 IP 地址。每個(gè)數(shù)據(jù)包中都包含了源主機(jī)和目標(biāo)主機(jī)的 IP 地址。IP 協(xié)議負(fù)責(zé)路徑計(jì)算,即 IP 數(shù)據(jù)包在網(wǎng)絡(luò)中的傳輸傳輸時(shí),數(shù)據(jù)包所經(jīng)過的每一個(gè)主機(jī)節(jié)點(diǎn)都會(huì)讀取數(shù)據(jù)包中的目標(biāo)主機(jī)地址信息,以便選擇朝什么地方傳送數(shù)據(jù)包。
今天,絕大多數(shù)的數(shù)據(jù)包仍舊是 IPv4(Internet Protocol version 4 網(wǎng)際協(xié)議版本 4)的,每一個(gè) IPv4 地址是長度為 32 位。常見采用 dotted-decimal(點(diǎn)分十進(jìn)制)表示法,具體形式如:198.51.100.42。
新的 IPv6 標(biāo)準(zhǔn)也正在逐漸推廣中。它有更大的地址空間:長度為 128 位,這使得數(shù)據(jù)包在網(wǎng)絡(luò)中傳輸時(shí)的尋址更容易一些。另外,由于有更多的地址可以分配,諸如網(wǎng)絡(luò)地址轉(zhuǎn)換等問題也迎刃而解。IPv6 的表示形式為:八組十六進(jìn)制數(shù)以冒號(hào)分割,比如:2001:0db8:85a3:0042:1000:8a2e:0370:7334。
一個(gè) IP 數(shù)據(jù)包通常包含 header (報(bào)頭信息) 和 payload (有效載荷)。
payload 中的內(nèi)容即是要傳輸?shù)恼嬲畔?,?header 承載的是與傳輸數(shù)據(jù)有關(guān)的元數(shù)據(jù) (metadata)。
IPv4的 header 信息內(nèi)容如下:
IPv4 Header Format
Offsets Octet 0 1 2 3
Octet Bit 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31|
0 0 |Version |IHL |DSCP |ECN |Total Length |
4 32 |Identification |Flags |Fragment Offset |
8 64 |Time To Live |Protocol |Header Checksum |
12 96 |Source IP Address |
16 128 |Destination IP Address |
20 160 |Options (if IHL > 5) |
header 長度為 20 字節(jié)(不包含極少用到的可選項(xiàng)信息)。
header 信息中最關(guān)鍵的是源和目標(biāo) IP 地址。除此之外,版本信息是 4,代表 IPv4。protocol(協(xié)議區(qū))代表 payload 采用的傳輸協(xié)議。TCP 的協(xié)議號(hào)是 6。Total Length(總長度區(qū))標(biāo)明了 header 加 payload 整個(gè)數(shù)據(jù)包的大小。
詳情參看維基百科中關(guān)于 IPv4 的條目,里面有關(guān)于 header 各個(gè)區(qū)域信息的詳細(xì)介紹。
IPv6 的地址長度為 128 位。IPv6 的 header 信息內(nèi)容如下:
Offsets Octet 0 1 2 3
Octet Bit 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31|
0 0 |Version |Traffic Class |Flow Label |
4 32 |Payload Length |Next Header |Hop Limit |
8 64 |Source Address |
12 96 | |
16 128 | |
20 160 | |
24 192 |Destination Address |
28 224 | |
32 256 | |
36 288 | |
IPv6 header 采用固定長度 40 字節(jié)。經(jīng)過多年來對(duì) IPv4 使用的總結(jié),如今 IPv6 的 header 信息簡化了許多。
除了源和目標(biāo)地址這種必備信息外,IPv6 提供專門的 next header 區(qū)域來指明緊接 header 的數(shù)據(jù)是什么。也就是說,IPv6 允許在數(shù)據(jù)包中將 header 鏈接起來。每一個(gè)被鏈接的 IPv6 header 都會(huì)有一個(gè) next header 字段,直到到達(dá)實(shí)際的 payload 數(shù)據(jù)。比如說,當(dāng) next header 的值為 6 (TCP 的協(xié)議號(hào)) 時(shí),數(shù)據(jù)包的其他信息就是 TCP 協(xié)議要傳輸?shù)臄?shù)據(jù)。
同樣的,更多信息請(qǐng)參考維基百科上關(guān)于 IPv6 數(shù)據(jù)包的條目。
由于底部鏈路層對(duì)所傳輸?shù)臄?shù)據(jù)幀有最大長度限制(最大傳輸單元,MTU),所以有時(shí)候 IPv4 需要對(duì)所傳數(shù)據(jù)包進(jìn)行分片。具體表現(xiàn)為,如果數(shù)據(jù)包尺寸超過了所要經(jīng)過的數(shù)據(jù)鏈路的最大傳輸限制,路由就會(huì)對(duì)數(shù)據(jù)包進(jìn)行分片。當(dāng)分片數(shù)據(jù)包到達(dá)目標(biāo)主機(jī)后,可以根據(jù)分片信息進(jìn)行數(shù)據(jù)重組。當(dāng)然,數(shù)據(jù)發(fā)送源有權(quán)決定路由是否啟用對(duì)傳輸數(shù)據(jù)包進(jìn)行分片,假如所傳輸?shù)臄?shù)據(jù)超過了輸送限制,又禁止了路由分片,發(fā)送源會(huì)收到 ICMP(Internet Control Message Protocol,Internet報(bào)文控制協(xié)議) 的數(shù)據(jù)幀超長報(bào)告信息。
在IPv6中,如果數(shù)據(jù)包超限制,路由會(huì)直接丟棄數(shù)據(jù)包并且向發(fā)送源回傳 ICMP6 的數(shù)據(jù)幀超長報(bào)告信息。源和目標(biāo)兩端會(huì)基于這個(gè)特性來進(jìn)行路徑 MTU 發(fā)現(xiàn),以此尋找兩端之間最大傳輸單元(maximum transfer unit)所在的路由。找到 MTU 路由后,僅當(dāng)上層數(shù)據(jù)包的最小 payload 確實(shí)超過了 MTU,IPv6 才會(huì)進(jìn)行分片傳輸。對(duì)于 IPv6 下的 TCP 來說,這不會(huì)造成什么問題。
TCP 層位于 IP 層之上,是最受歡迎的因特網(wǎng)通訊協(xié)議之一,人們通常用 TCP/IP 來泛指整個(gè)因特網(wǎng)協(xié)議族。
剛剛提到,IP 協(xié)議允許兩個(gè)主機(jī)之間傳送單一數(shù)據(jù)包。為了保證對(duì)所傳送數(shù)據(jù)包達(dá)到盡力服務(wù)的目的,最終的傳輸?shù)慕Y(jié)果可能是數(shù)據(jù)包亂序、重復(fù)甚至丟包。
TCP 是基于 IP 層的協(xié)議。但是 TCP 是可靠的、有序的、有錯(cuò)誤檢查機(jī)制的基于字節(jié)流傳輸?shù)膮f(xié)議。這樣當(dāng)兩個(gè)設(shè)備上的應(yīng)用通過 TCP 來傳遞數(shù)據(jù)的時(shí)候,總能夠保證目標(biāo)接收方收到的數(shù)據(jù)的順序和內(nèi)容與發(fā)送方所發(fā)出的是一致的。TCP 做的這些事看起來稀松平常,但是比起 IP 層的粗曠處理方式已經(jīng)是有顯著的進(jìn)步了。
應(yīng)用程序之間可以通過 TCP 建立鏈接。TCP 建立的是雙向連接,通信雙方可以同時(shí)進(jìn)行數(shù)據(jù)的傳輸。連接的雙方都不需要操心數(shù)據(jù)是否分塊,或者是否采用了盡力服務(wù)等。TCP 會(huì)確保所傳輸?shù)臄?shù)據(jù)的正確性,即接受方收到的數(shù)據(jù)與發(fā)出方的數(shù)據(jù)一致。
HTTP 是典型的 TCP 應(yīng)用。用戶瀏覽器(應(yīng)用 1)與 web 服務(wù)器(應(yīng)用 2)建立連接后,瀏覽器可以通過連接發(fā)送服務(wù)請(qǐng)求,web 服務(wù)器可以通過同樣的連接對(duì)請(qǐng)求做出響應(yīng)。
同一個(gè) host 主機(jī)上可以有多個(gè)應(yīng)用同時(shí)使用 TCP 協(xié)議。TCP 用不同的端口來區(qū)分應(yīng)用。作為連接的兩端,發(fā)送源和接收目標(biāo)分別擁有自己的 IP 地址和端口號(hào)。憑借這樣一對(duì) IP 地址和端口號(hào),就可以唯一標(biāo)識(shí)一個(gè)連接。
使用 HTTPS 的 web 服務(wù)器會(huì)監(jiān)聽 443 端口。瀏覽器作為發(fā)送源會(huì)啟用一個(gè)臨時(shí)端口結(jié)合自己的 IP 地址與目標(biāo)服務(wù)器對(duì)應(yīng)的端口和 IP 地址建立 TCP 連接。
TCP 在 IPv4 和 IPv6 上是無差別運(yùn)行的。所以,如果 IPv4 的 Protocol 或 IPv6 的 Next Hearder的協(xié)議號(hào)被設(shè)置成 6,表示執(zhí)行 TCP 協(xié)議。
主機(jī)之間傳輸?shù)臄?shù)據(jù)流一般先會(huì)被分塊,再轉(zhuǎn)化成 TCP 的報(bào)文段,最終會(huì)生成 IP 數(shù)據(jù)包中的 payload 載荷數(shù)據(jù)。
每個(gè) TCP 報(bào)文段都有 header 信息和對(duì)應(yīng)的載荷 payload。payload 信息就是待傳輸?shù)臄?shù)據(jù)塊。TCP 報(bào)文段的 header 信息中主要包含的是源和目標(biāo)端口號(hào),至于說源和目標(biāo)的 IP 地址信息則已經(jīng)包含在 IP header 信息中了。
TCP 的報(bào)文段 header 信息中還有報(bào)文序列號(hào)、確認(rèn)號(hào)等其他一些用于管理連接的信息。
所謂序列號(hào)信息,其實(shí)就是為每個(gè)報(bào)文段分配的唯一編號(hào)。第一個(gè)報(bào)文段的序列號(hào)是隨機(jī)的,比如:1721092979,其后的每一個(gè)報(bào)文段的序列號(hào)都以此號(hào)為基礎(chǔ)依次加 1,1721092980,1721092981 等等。至于確認(rèn)號(hào),是目標(biāo)端反饋給源的確認(rèn)信息,通知源目前已經(jīng)接到哪些報(bào)文段了。由于 TCP 是雙向的,所以數(shù)據(jù)和確認(rèn)信息發(fā)送也都是雙向的。
連接管理是 TCP 的核心功能之一,而且協(xié)議需要解決由于IP層采用不可靠傳輸引發(fā)的一系列復(fù)雜問題。下面會(huì)分別介紹TCP的連接建立、數(shù)據(jù)傳輸以及連接終止的詳細(xì)過程。
TCP 連接全過程的狀態(tài)變化是很復(fù)雜的(參考 TCP 狀態(tài)圖)。但是大多數(shù)情況下還是比較簡單的。
TCP 連接都是建立在兩個(gè)主機(jī)之間的。所以,每個(gè)連接建立過程中都存在兩個(gè)角色:一端(例如 web 服務(wù)器)監(jiān)聽連接,另一端(例如應(yīng)用)主動(dòng)連接正在監(jiān)聽的一端(web 服務(wù)器)。服務(wù)器端的這種監(jiān)聽行為被稱為 passive open(被動(dòng)打開)??蛻舳酥鲃?dòng)連接服務(wù)器的行為被稱為 active open(主動(dòng)打開)。
TCP 會(huì)通過三次握手來完成連接建立,具體過程是這樣的:
SYN 是 synchronize sequence numbers (同步序列號(hào)) 的縮寫。兩端在傳遞數(shù)據(jù)時(shí),所傳遞的每個(gè) TCP 報(bào)文段都有一個(gè)序列號(hào)。就是利用這種機(jī)制,TCP 可以確保分塊傳輸?shù)臄?shù)據(jù)包最終都以正確的個(gè)數(shù)和順序抵達(dá)目標(biāo)端。在正式傳輸開始之前,源和目標(biāo)端需要同步確認(rèn)第一個(gè)報(bào)文的序列號(hào)。
ACK 是 acknowledgment (確認(rèn))的縮寫。當(dāng)某一端接到了報(bào)文包后,通過回傳已報(bào)文序列號(hào)來確認(rèn)接收到報(bào)文這件事。
運(yùn)行如下語句:
curl -4 http://www.apple.com/contact/
這是通過 curl 命令與 www.apple.com 的 80 端口創(chuàng)建一個(gè) TCP 連接。
www.apple.com 所在服務(wù)器 23.63.125.15(注意,整個(gè) IP 不是固定的)會(huì)監(jiān)聽 80 端口。我們自己的 IP 地址是 10.0.1.6,啟用的臨時(shí)端口 52181(這個(gè)端口是從可用端口中隨機(jī)選擇的)。利用 tcpdump(1) 輸出的三次握手過程是這樣的:
% sudo tcpdump -c 3 -i en3 -nS host 23.63.125.15
18:31:29.140787 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [S], seq 1721092979, win 65535, options [mss 1460,nop,wscale 4,nop,nop,TS val 743929763 ecr 0,sackOK,eol], length 0
18:31:29.150866 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [S.], seq 673593777, ack 1721092980, win 14480, options [mss 1460,sackOK,TS val 1433256622 ecr 743929763,nop,wscale 1], length 0
18:31:29.150908 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673593778, win 8235, options [nop,nop,TS val 743929773 ecr 1433256622], length 0
這里信息量很大。下面要逐個(gè)分析一下。
最左邊是系統(tǒng)時(shí)間。當(dāng)時(shí)執(zhí)行命令的時(shí)間是晚上18:31。后面的 IP 代表的是這些都是 IP 協(xié)議數(shù)據(jù)包。
接下來看這段 10.0.1.6.52181 > 23.63.125.15.80,這一對(duì)是源和目標(biāo)端的 IP 地址+端口。第一行和第三行是客戶端發(fā)向服務(wù)端的信息,第二行是服務(wù)端發(fā)向客戶端的。tcpdump 會(huì)自動(dòng)把端口號(hào)加到 IP 地址后頭,比如 10.0.1.6.52181 表示 IP 地址為 10.0.1.6,端口號(hào)為 52181。
Flags 表示 TCP 報(bào)文段 header 信息中的一些縮寫標(biāo)識(shí):S 代表 SYN,. 代表ACK,P 代表PUSH,F 是 FIN。還有一些其他的標(biāo)識(shí),這邊就不羅列了。注意上面三行 Flags 中先是攜帶 SYN ,接著是 SYN-ACK,最后是 ACK,這就是三次握手確認(rèn)的全過程。
另外,第一行中客戶端發(fā)送了一個(gè)隨機(jī)序列號(hào) 1721092979 (就是上文所說的A)給服務(wù)器。第二行展示的是服務(wù)器回傳給客戶端的確認(rèn)號(hào) 1721092980 (A+1) 和一個(gè)隨機(jī)序列號(hào) 673593777 (B)。 最后在第三行,客戶端將自己的確認(rèn)號(hào) 673593778 (B+1) 發(fā)還給服務(wù)端。
當(dāng)然,在連接建立過程中還會(huì)配置一些其他的信息。比如第一行中客戶端發(fā)送的內(nèi)容:
[mss 1460,nop,wscale 4,nop,nop,TS val 743929763 ecr 0,sackOK,eol]
還有第二行服務(wù)端發(fā)送的:
[mss 1460,sackOK,TS val 1433256622 ecr 743929763,nop,wscale 1]
其中 TS val / ecr 是 TCP 用來創(chuàng)建 RTT 往返時(shí)間 (round-trip time) 的。TS val 是發(fā)送方的 時(shí)間戳 (time stamp),ecr 是相應(yīng)應(yīng)答 (echo reply) 時(shí)間戳,通常情況下就是發(fā)送方收到的最后時(shí)間戳。TCP 以 RTT 作為其擁塞控制算法 (congestion-control algorithms) 的依據(jù)。
連接的兩端都發(fā)送 sackOK。這樣會(huì)啟用選擇性確認(rèn) (Selective Acknowledgement) 機(jī)制,使連接雙方能夠確認(rèn)收到的字節(jié)范圍。一般情況下,確認(rèn)機(jī)制只是確認(rèn)接受方已收到的數(shù)據(jù)的字節(jié)總數(shù)。RFC 2018 第 3 部分有對(duì) SACK 的詳細(xì)闡述。
mss 選項(xiàng)聲明了最大報(bào)文長度 (Maximum Segment Size),表示接收端希望接收的單個(gè)報(bào)文的最大長度(以字節(jié)為單位)。wscale 是 窗口放大因子 (window scale factor),稍后會(huì)詳細(xì)說明。
一旦建立了連接,雙方就可以互發(fā)數(shù)據(jù)了。發(fā)送端所發(fā)出的每個(gè)報(bào)文段都有一個(gè)序列號(hào),這個(gè)序列號(hào)與當(dāng)下已傳送的字節(jié)總數(shù)有關(guān)。接收端會(huì)針對(duì)已接收的數(shù)據(jù)包向源端發(fā)送確認(rèn)報(bào)文,確認(rèn)信息同樣是由報(bào)文 header 所攜帶的 ACK。
假設(shè)現(xiàn)在傳送的信息是除最后一個(gè)報(bào)文 5 字節(jié)外,其他都是 10 字節(jié)。具體是這樣的:
host A sends segment with seq 10
host A sends segment with seq 20
host A sends segment with seq 30 host B sends segment with ack 10
host A sends segment with seq 35 host B sends segment with ack 20
host B sends segment with ack 30
host B sends segment with ack 35
整個(gè)機(jī)制是雙向運(yùn)轉(zhuǎn)的。A 主機(jī)會(huì)持續(xù)的發(fā)送數(shù)據(jù)包。B 收到數(shù)據(jù)包后會(huì)向 A 發(fā)送確認(rèn)信息。A 發(fā)送數(shù)據(jù)包的過程不需要等待 B 的確認(rèn)。
TCP 將流量控制和其他一系列復(fù)雜機(jī)制結(jié)合起來進(jìn)行擁塞控制。需要處理以下問題:針對(duì)丟失的報(bào)文采用重發(fā)機(jī)制,同時(shí)還需要?jiǎng)討B(tài)的調(diào)整發(fā)送報(bào)文的頻率。
流量控制的原則是發(fā)送方發(fā)送數(shù)據(jù)的速度不能比接收方處理數(shù)據(jù)的速度快。接收方,也就是所謂的 接收窗口 (receive window) 會(huì)告知發(fā)送方自身接收窗口數(shù)據(jù)緩沖區(qū)的大小。從上面 tcpdump 的輸出來看,窗口大小是 win 65535,wscale(窗口放大因子)是 4。這些數(shù)字的意思是說,10.0.1.6 主機(jī)的接收窗口大小是 4*64 kB = 256 kB,23.63.125.15 主機(jī)的 win 是 14480,wscale 是 1,接收窗口約為 14KB。總之,不管哪一方作為數(shù)據(jù)接收方,都會(huì)向?qū)Ψ酵▓?bào)自己的接收窗口大小。
擁塞控制要更復(fù)雜一些。所有擁塞控制的目標(biāo)都是要計(jì)算出當(dāng)前網(wǎng)絡(luò)中數(shù)據(jù)傳輸?shù)淖罴阉俾省K^最佳速率就是要達(dá)到一種微妙的平衡。一方面,是希望速度越快越好,另一方面,速度快意味著數(shù)據(jù)傳輸多,這樣處理性能會(huì)大打折扣甚至導(dǎo)致崩潰。而這種超負(fù)荷崩潰是分組交換網(wǎng)絡(luò)的固有特點(diǎn)。當(dāng)負(fù)載過大,數(shù)據(jù)包之間會(huì)產(chǎn)生擁塞,直接導(dǎo)致丟包率急速上升。
擁塞控制還需要充分考慮對(duì)流量的影響。RFC 5681 中對(duì) TCP 擁塞控制有 6,000 字左右的闡述。發(fā)送方要時(shí)刻關(guān)注來自接收方的確認(rèn)信息。要做到這點(diǎn)并不簡單,有的時(shí)候還需要一定的妥協(xié)。要知道底部 IP 協(xié)議數(shù)據(jù)包是無序傳輸?shù)?,?shù)據(jù)包會(huì)丟失也會(huì)重復(fù)。發(fā)送方需要評(píng)估 RTT 往返時(shí)間,然后基于 RTT 去確定是否收到了接收方的確認(rèn)信息。重發(fā)數(shù)據(jù)包也有很大代價(jià),除了連接延遲問題,網(wǎng)絡(luò)的負(fù)載也會(huì)發(fā)生明顯的波動(dòng)。導(dǎo)致 TCP 需要不停的去適應(yīng)當(dāng)前網(wǎng)絡(luò)情況。
更重要的是,TCP 連接本身是易變的。除了數(shù)據(jù)傳輸,連接的兩端還會(huì)不時(shí)的發(fā)送一些提醒和確認(rèn)信息以便可以適當(dāng)?shù)恼{(diào)整狀態(tài)來維持連接。
基于這種一直在相互協(xié)調(diào)中的連接關(guān)系,TCP 連接往往會(huì)是短暫而低效的。在建立連接的初期,TCP 協(xié)議算法還不能完全了解當(dāng)前網(wǎng)絡(luò)狀況。而在連接將要結(jié)束的時(shí)候,反饋給發(fā)送方的信息又可能不充分,這樣就很難對(duì)連接狀況做出實(shí)時(shí)的合理的評(píng)估。
之前展示了客戶端和服務(wù)端之間交換的三段報(bào)文。再看看關(guān)于連接的其他信息:
18:31:29.150955 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [P.], seq 1721092980:1721093065, ack 673593778, win 8235, options [nop,nop,TS val 743929773 ecr 1433256622], length 85
18:31:29.161213 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], ack 1721093065, win 7240, options [nop,nop,TS val 1433256633 ecr 743929773], length 0
客戶端 10.0.1.6 發(fā)送的第一段報(bào)文長度是 85 bytes (HTTP 請(qǐng)求)。由于在上一個(gè)報(bào)文發(fā)送后沒有收到來自服務(wù)端的信息,所以 ACK 確認(rèn)號(hào)的值不變。
服務(wù)端 23.63.125.15 只是對(duì)接收客戶端的數(shù)據(jù)進(jìn)行確認(rèn)回復(fù),沒有向客戶端發(fā)送數(shù)據(jù),所以 length 為 0。由于當(dāng)前連接是采用選擇性確認(rèn) (Selective acknowledgments),所以序列號(hào)和確認(rèn)號(hào)是之間的字節(jié)長度是從 1721092980 到 1721093065,也就是 85 bytes。接收方發(fā)送的 ACK 確認(rèn)號(hào)是 1721093065,這代表目前已接收的數(shù)據(jù)確認(rèn)累計(jì)到 1721093065 字節(jié)了。至于說為什么數(shù)字會(huì)如此之大,這要說到初次握手時(shí)發(fā)出的隨機(jī)數(shù),數(shù)字的范圍和那個(gè)初始數(shù)字是相關(guān)的。
這種模式會(huì)一直持續(xù)到全部數(shù)據(jù)傳送完成:
18:31:29.189335 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673593778:673595226, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190280 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673595226:673596674, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190350 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673596674, win 8101, options [nop,nop,TS val 743929811 ecr 1433256660], length 0
18:31:29.190597 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673596674:673598122, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190601 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673598122:673599570, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190614 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673599570:673601018, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190616 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673601018:673602466, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190617 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673602466:673603914, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190619 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673603914:673605362, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190621 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673605362:673606810, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190679 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673599570, win 8011, options [nop,nop,TS val 743929812 ecr 1433256660], length 0
18:31:29.190683 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673602466, win 7830, options [nop,nop,TS val 743929812 ecr 1433256660], length 0
18:31:29.190688 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673605362, win 7830, options [nop,nop,TS val 743929812 ecr 1433256660], length 0
18:31:29.190703 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673605362, win 8192, options [nop,nop,TS val 743929812 ecr 1433256660], length 0
18:31:29.190743 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673606810, win 8192, options [nop,nop,TS val 743929812 ecr 1433256660], length 0
18:31:29.190870 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673606810:673608258, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.198582 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [P.], seq 673608258:673608401, ack 1721093065, win 7240, options [nop,nop,TS val 1433256670 ecr 743929811], length 143
18:31:29.198672 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673608401, win 8183, options [nop,nop,TS val 743929819 ecr 1433256660], length
最終連接會(huì)終止(或結(jié)束)。連接的每一端都會(huì)發(fā)送 FIN 標(biāo)識(shí)給另一端來聲明結(jié)束傳輸,接著另一端會(huì)對(duì)收到 FIN 進(jìn)行確認(rèn)。當(dāng)連接兩端均發(fā)送完各自 FIN 和做出相應(yīng)的確認(rèn)后,連接將會(huì)徹底關(guān)閉:
18:31:29.199029 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [F.], seq 1721093065, ack 673608401, win 8192, options [nop,nop,TS val 743929819 ecr 1433256660], length 0
18:31:29.208416 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [F.], seq 673608401, ack 1721093066, win 7240, options [nop,nop,TS val 1433256680 ecr 743929819], length 0
18:31:29.208493 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673608402, win 8192, options [nop,nop,TS val 743929828 ecr 1433256680], length 0
這里值得注意的是第二行,23.63.125.15 發(fā)送了 FIN,同時(shí)在這個(gè)報(bào)文信息中還對(duì)第一行中另一端發(fā)送的 FIN 予以 ACK(以.代表)確認(rèn)。
1989 年,Tim Berners Lee 在 CERN(European Organization for Nuclear Research 歐洲原子核研究委員會(huì)) 擔(dān)任軟件咨詢師的時(shí)候,開發(fā)了一套程序,奠定了萬維網(wǎng)的基礎(chǔ)。HyperText Transfer Protocol(超文本轉(zhuǎn)移協(xié)議,即HTTP)是用于從 WWW 服務(wù)器傳輸超文本到本地瀏覽器的傳送協(xié)議。RFC 2616 定義了今天普遍使用的一個(gè)版本:HTTP 1.1。
HTTP 采用簡單的請(qǐng)求和響應(yīng)機(jī)制。在 Safari 輸入 http://www.apple.com 時(shí),會(huì)向 www.appple.com 所在的服務(wù)器發(fā)送一個(gè) HTTP 請(qǐng)求。服務(wù)器會(huì)對(duì)請(qǐng)求做出一個(gè)響應(yīng),將請(qǐng)求結(jié)果信息返回給 Safari。
每一個(gè)請(qǐng)求都有一個(gè)對(duì)應(yīng)的響應(yīng)信息。請(qǐng)求和響應(yīng)遵從同樣的格式。第一行是請(qǐng)求行或者響應(yīng)狀態(tài)行。接下來是 header 信息,header 信息之后會(huì)有一個(gè)空行??招兄笫?body 請(qǐng)求信息體。
當(dāng) Safari 加載 HTML 頁面 http://www.objc.io/about.html 的時(shí)候,先是發(fā)送 HTTP 請(qǐng)求到 www.objc.io,請(qǐng)求的內(nèi)容是:
GET /about.html HTTP/1.1
Host: www.objc.io
Accept-Encoding: gzip, deflate
Connection: keep-alive
If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
If-Modified-Since: Mon, 10 Feb 2014 18:08:48 GMT
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.74.9 (KHTML, like Gecko) Version/7.0.2 Safari/537.74.9
Referer: http://www.objc.io/
DNT: 1
Accept-Language: en-us
第一行是請(qǐng)求行。它包含三部分信息:動(dòng)作,資源信息,還有 HTTP 的版本。
本例中,動(dòng)作是 GET。所謂動(dòng)作也就是常說的 HTTP 請(qǐng)求方法。資源信息表明所請(qǐng)求的資源。例子中的資源信息是 /about.html,這表示我們想 get 服務(wù)器的在 /about.html 位置中的文檔。當(dāng)前 HTTP 版本是 HTTP/1.1。
接下來 10 行是 HTTP header 信息。跟著是一行空行。例子中的請(qǐng)求沒有 body 信息。
header 的作用是向服務(wù)器傳遞一些額外的輔助信息,它的內(nèi)容比較寬泛。維基百科中有常用 HTTP header 關(guān)鍵字信息的清單。例子中的 header 信息 Host: www.objc.io 表示告訴服務(wù)器,本次請(qǐng)求的服務(wù)器名稱是什么。這樣可以讓同一個(gè)服務(wù)器處理針對(duì)多個(gè)域名的請(qǐng)求。
下面是一些常見的header信息:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us
服務(wù)器可能具備返回多種媒體類型的能力,Accept 表示 Safari 希望接收的媒體格式類型。text/html 是互聯(lián)網(wǎng)媒體類型(Internet media types),也被稱為 MIME 類型或者是內(nèi)容類型 (Content-types)。q=0.9 表示 Safari 對(duì)給定媒體類型的優(yōu)先級(jí)要求。Accept-Language 代表 Safari 希望接收的自然語言清單。這會(huì)要求服務(wù)器盡可能的根據(jù)清單要求去匹配相應(yīng)的語言。
Accept-Encoding: gzip, deflate
通過這個(gè)header,Safari 告訴服務(wù)器可以對(duì)響應(yīng) body 做壓縮處理。如果 header 信息中沒有設(shè)置壓縮標(biāo)識(shí),那么服務(wù)器就必須返回沒有壓縮過的信息。壓縮可以大大減少數(shù)據(jù)的傳輸量,在文本信息 (比如 HTML) 中尤為明顯。
If-Modified-Since: Mon, 10 Feb 2014 18:08:48 GMT
If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"
這兩行信息表明 Safari 已經(jīng)對(duì)請(qǐng)求結(jié)果做過緩存。如果服務(wù)器上的待請(qǐng)求內(nèi)容在 2 月 10 號(hào)以后發(fā)生過變化或者是 ETag 與 a54907f38b306fe3ae4f32c003ddd507 不匹配,這就表示請(qǐng)求結(jié)果與當(dāng)前緩存信息不一致,需要服務(wù)器返回最新的請(qǐng)求結(jié)果。
User-Agent 是告知服務(wù)器當(dāng)前發(fā)送請(qǐng)求的客戶端類型。
作為上面請(qǐng)求的響應(yīng),服務(wù)器的返回是:
HTTP/1.1 304 Not Modified
Connection: keep-alive
Date: Mon, 03 Mar 2014 21:09:45 GMT
Cache-Control: max-age=3600
ETag: "a54907f38b306fe3ae4f32c003ddd507"
Last-Modified: Mon, 10 Feb 2014 18:08:48 GMT
Age: 6
X-Cache: Hit from cloudfront
Via: 1.1 eb67cb25620df959ba21a943fbc49ef6.cloudfront.net (CloudFront)
X-Amz-Cf-Id: dDSBgR86EKBemW6el-pBI9kAnuYJEaPQYEqGmBnilD12CbixCuZYVQ==
第一行是狀態(tài)行。它包括 HTTP 版本,狀態(tài)碼 (304) 和狀態(tài)信息。
HTTP 定義了一系列狀態(tài)碼,它們各有用途。本例中的 304 表示所請(qǐng)求的信息自上次訪問以來沒有變化。
響應(yīng)中沒有包含 body 信息。也就是說服務(wù)器通知客戶端:你的版本已經(jīng)是最新了,可以直接使用當(dāng)前緩存信息。
用 curl 發(fā)送一個(gè)請(qǐng)求:
% curl http://www.apple.com/hotnews/ > /dev/null
curl 沒有使用本地緩存。整個(gè)請(qǐng)求會(huì)是這樣的:
GET /hotnews/ HTTP/1.1
User-Agent: curl/7.30.0
Host: www.apple.com
Accept: */*
這個(gè)請(qǐng)求與之前 Safari 發(fā)的請(qǐng)求很類似。但是 curl 請(qǐng)求的 header 信息中沒有 If-None-Match,所以服務(wù)器必須將請(qǐng)求結(jié)果返回。
此處 curl 頭信息中聲明的 Accept: */* 表示可以接收任何媒體類型。
來自 www.apple.com 的響應(yīng):
HTTP/1.1 200 OK
Server: Apache
Content-Type: text/html; charset=UTF-8
Cache-Control: max-age=424
Expires: Mon, 03 Mar 2014 21:57:55 GMT
Date: Mon, 03 Mar 2014 21:50:51 GMT
Content-Length: 12342
Connection: keep-alive
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf-8" />
后面還會(huì)有一些,現(xiàn)在收到的響應(yīng)里 body 中包含了 HTML 文檔信息。
Apple 服務(wù)器響應(yīng)的狀態(tài)碼是 200,這是標(biāo)準(zhǔn)的表示 HTTP 請(qǐng)求成功的狀態(tài)碼。
服務(wù)器同時(shí)還告知響應(yīng)媒體類型是 text/html;字符集 charset=UTF-8;內(nèi)容長度 Content-Length:12342,代表了 body 信息的大小。
Transport Layer Security (安全傳輸層協(xié)議,TLS) 是一種基于 TCP 的加密協(xié)議。它支持兩件事:傳輸?shù)膬啥丝梢曰ハ囹?yàn)證對(duì)方的身份,以及加密所傳輸?shù)臄?shù)據(jù)。基于 TLS 的 HTTP 請(qǐng)求就是 HTTPS。
用 HTTPS 去替代 HTTP,在安全方面會(huì)有顯著的提升。也許你還會(huì)采用一些其他的安全措施,總之這都會(huì)為安全通信提供保障。
如果服務(wù)器支持的話,你應(yīng)該將 TLSMinimumSupportedProtocol 設(shè)置為 kTLSProtocol12,以要求使用 TLS 1.2 版本。這能有效的防御中間人攻擊。
如果不確定數(shù)據(jù)接收方的身份,那么即便對(duì)所傳輸數(shù)據(jù)進(jìn)行加密也沒什么意義。服務(wù)器的證書可以表明服務(wù)器的身份,只允許和持有某個(gè)特定證書的一方建立連接,就就是證書鎖定。
如果一個(gè)客戶端通過 TLS 和服務(wù)器建立連接,操作系統(tǒng)會(huì)驗(yàn)證服務(wù)器證書的有效性。當(dāng)然,有很多手段可以繞開這個(gè)校驗(yàn),最直接的是在 iOS 設(shè)備上安裝證書并且將其設(shè)置為可信的。這種情況下,實(shí)施中間人攻擊也不是什么難事。
可以使用證書鎖定來規(guī)避這種風(fēng)險(xiǎn)(或者說是將風(fēng)險(xiǎn)降到最低)。當(dāng)建立 TLS 連接后,應(yīng)立即檢查服務(wù)器的證書,不僅要驗(yàn)證證書的有效性,還需要確定證書和其持有者是否匹配。考慮到應(yīng)用和服務(wù)器需要同時(shí)升級(jí)證書的要求,這種方式比較適合應(yīng)用在訪問自家服務(wù)器的情況下。
為了實(shí)現(xiàn)證書鎖定,在建立連接的過程中需要對(duì)服務(wù)器進(jìn)行信任檢查 (server trust)。每當(dāng)通過 NSURLSession 創(chuàng)建了連接,NSURLSession 的代理就會(huì)收到一個(gè) -URLSession:didReceiveChallenge:completionHandler: 的調(diào)用。傳遞的參數(shù) NSURLAuthenticationChallenge 有一個(gè)屬性 protectionSpace,它是 NSURLProtectionSpace 的實(shí)例,它有一個(gè) serverTrust 屬性。
serverTrust 是一個(gè) SecTrustRef 對(duì)象。Security 框架提供了很多方法用于驗(yàn)證 SecTrustRef。AFNetworking 項(xiàng)目中的 AFSecurityPolicy 就是一個(gè)不錯(cuò)的使用。一如既往的提醒大家,如果要自己構(gòu)建安全驗(yàn)證相關(guān)的代碼,請(qǐng)一定要認(rèn)真做好代碼審查,千萬不要再出現(xiàn)諸如 goto fail; 這類 bug。
現(xiàn)在大家對(duì) IP,TCP 和 HTTP 的工作原理有了一定的了解了。下面說說還可以做些什么以及一些相關(guān)注意事項(xiàng)。
TCP 連接容易在兩個(gè)時(shí)點(diǎn)出現(xiàn)問題:初始設(shè)置,以及通過連接傳輸?shù)淖詈笠徊糠謭?bào)文。
連接設(shè)置可能會(huì)非常耗時(shí)。正如前文所說,TCP 建立連接的過程中需要進(jìn)行三次握手。這個(gè)過程中本身沒有太多的數(shù)據(jù)需要傳遞。但是,對(duì)于移動(dòng)網(wǎng)絡(luò)來說,從手機(jī)端向服務(wù)器端發(fā)送一個(gè)數(shù)據(jù)包普遍需要 250ms,也就是四分之一秒。推及到三次握手,也就是說在還沒有傳送任何數(shù)據(jù)之前,光建立連接就要花費(fèi) 750ms。
HTTPS 的情況更夸張,由于 HTTPS 是基于 TLS 的 HTTP,而 HTTP 又基于 TCP。TCP 連接就要執(zhí)行三次握手,然后到了 TLS 層還會(huì)再握手三次。估算一下,建立一個(gè) HTTPS 連接的耗時(shí)至少是創(chuàng)建一個(gè) HTTP 連接的兩倍。如果 RTT 時(shí)間是 500ms(假設(shè)單程 250ms),HTTPS 建立連接累計(jì)總耗時(shí)將達(dá)1.5秒。
不管建立連接后是要傳遞多少數(shù)據(jù),建立連接本身都太過耗時(shí)了。
另一個(gè)影響 TCP 連接的因素是傳送大規(guī)模數(shù)據(jù)。如果要在網(wǎng)絡(luò)情況未知的條件下傳送報(bào)文,TCP 需要偵測(cè)當(dāng)前網(wǎng)絡(luò)的能力。換句話說,TCP 得花費(fèi)一定的時(shí)間去計(jì)算此網(wǎng)絡(luò)的最佳傳輸速率。上文提到過,TCP 需要逐步調(diào)整以便找到最佳速度。這種算法被稱為 慢啟動(dòng) (slow-start)。還有一點(diǎn)值得注意,慢啟動(dòng)策略在那些數(shù)據(jù)鏈路層傳輸質(zhì)量較差的網(wǎng)絡(luò)環(huán)境中的表現(xiàn)更差,無線網(wǎng)絡(luò)就是典型的例子。
另一個(gè)問題主要存在于數(shù)據(jù)傳輸?shù)淖詈箅A段。每當(dāng)客戶端發(fā)起 HTTP 請(qǐng)求某些資源的時(shí)候,服務(wù)器會(huì)持續(xù)的向客戶端主機(jī)發(fā)送 TCP 報(bào)文數(shù)據(jù),客戶端收到數(shù)據(jù)后會(huì)給服務(wù)器反饋 ACK 確認(rèn)信息。假如某個(gè)報(bào)文在傳輸過程中發(fā)生丟包,那么服務(wù)器也就不會(huì)收到該包的確認(rèn) ACK。一旦服務(wù)器發(fā)現(xiàn)有數(shù)據(jù)包沒有 ACK 反饋,就會(huì)觸發(fā)快速重傳 (fast retransmit)。
每當(dāng)某個(gè)數(shù)據(jù)包丟失,數(shù)據(jù)接收方在收到下個(gè)數(shù)據(jù)包后發(fā)出的確認(rèn) ACK 與所接收的前一個(gè)數(shù)據(jù)包的確認(rèn) ACK 相同。那么數(shù)據(jù)發(fā)送方自然就會(huì)收到重復(fù)的 ACK。除了報(bào)文丟失,還有很多種網(wǎng)絡(luò)狀況會(huì)導(dǎo)致重復(fù) ACK 的問題。一般情況下,如果數(shù)據(jù)發(fā)送方連續(xù)收到 3 個(gè)重復(fù)的 ACK 就會(huì)立即進(jìn)行快速重發(fā)。
這所導(dǎo)致的問題將發(fā)生在數(shù)據(jù)傳輸?shù)氖瘴搽A段。如果發(fā)送方完成數(shù)據(jù)發(fā)送,接受方自然會(huì)停止發(fā)送 ACK 確認(rèn)。在最后四個(gè)報(bào)文傳輸?shù)倪^程中,快速重發(fā)算法是沒有辦法處理這四個(gè)報(bào)文的數(shù)據(jù)包的丟失問題的(因?yàn)椴粫?huì)收到三個(gè)相同的確認(rèn) ACK,所以不能界定傳輸丟包)。在常規(guī)網(wǎng)絡(luò)環(huán)境下,四個(gè)數(shù)據(jù)包相當(dāng)于 5.7kB 的數(shù)據(jù)規(guī)模。總之,在這最后 5.7kB 的傳輸?shù)倪^程中,快速重發(fā)機(jī)制是無效的。針對(duì)這種情況,TCP 會(huì)啟用其他機(jī)制來偵測(cè)丟包問題。對(duì)于這種情況,重傳操作可能要消耗幾秒鐘去執(zhí)行,這并不奇怪。
HTTP 有兩種策略來解決這些問題。最簡單的是 HTTP 持久連接 (persistent connection),也被稱為長連接 (keep-alive)。具體就是,每當(dāng) HTTP 完成一組請(qǐng)求-響應(yīng)處理后,還會(huì)繼續(xù)復(fù)用相同的 TCP 連接。而 HTTPS 會(huì)復(fù)用同樣的 TLS 連接:
open connection
client sends HTTP request 1 ->
<- server sends HTTP response 1
client sends HTTP request 2 ->
<- server sends HTTP response 2
client sends HTTP request 3 ->
<- server sends HTTP response 3
close connection
第二步就利用了 HTTP 管線 (pipelining) 處理,即允許客戶端利用同樣的連接并行發(fā)送多個(gè)請(qǐng)求,也就是說無需等待上一個(gè)請(qǐng)求的響應(yīng)完成可以發(fā)下一個(gè)請(qǐng)求。這表示能同時(shí)處理請(qǐng)求和響應(yīng),請(qǐng)求處理的順序采用先進(jìn)先出原則,響應(yīng)結(jié)果會(huì)按照請(qǐng)求發(fā)出的順序依次返還給客戶端。
稍微簡化一下,看起來會(huì)是這樣:
open connection
client sends HTTP request 1 ->
client sends HTTP request 2 ->
client sends HTTP request 3 ->
client sends HTTP request 4 ->
<- server sends HTTP response 1
<- server sends HTTP response 2
<- server sends HTTP response 3
<- server sends HTTP response 4
close connection
注意,服務(wù)器發(fā)出的響應(yīng)是實(shí)時(shí)的,不會(huì)等到接收完全部請(qǐng)求才處理。
可以利用這個(gè)特點(diǎn)來提升 TCP 的效率。只需要在建立連接初始階段執(zhí)行握手,而后一直復(fù)用同樣的連接,這樣 TCP 就可以最大限度的利用帶寬。此種情況下,擁塞控制也會(huì)隨之提升。因?yàn)榭焖僦匕l(fā)機(jī)制無法處理的最末四個(gè)報(bào)文丟失情況只會(huì)發(fā)生在使用本連接的最后一個(gè)請(qǐng)求-響應(yīng)中,而不是像之前那樣每一個(gè)請(qǐng)求-響應(yīng)都需要建立自己的連接,每個(gè)連接中都可能出現(xiàn)最后四個(gè)報(bào)文丟失的問題。
HTTP 管線化對(duì)高網(wǎng)絡(luò)延遲連接的通訊性能提升尤為顯著,在你的 iPhone 沒有通過 Wi-Fi 訪問網(wǎng)絡(luò)的時(shí)候,此類網(wǎng)絡(luò)連接就屬于高延遲范疇。實(shí)際上,有調(diào)查顯示,在移動(dòng)網(wǎng)絡(luò)環(huán)境下,SPDY 的通訊性能并不優(yōu)于 HTTP 管線。
RFC 2616 指明,在與同一個(gè)服務(wù)器通訊的時(shí)候,如果啟用了 HTTP 管線,建議啟用兩個(gè)連接。按照說明所述,這樣能獲得最優(yōu)響應(yīng)效率,能最大限度避免擁塞。增加更多的連接也不會(huì)再對(duì)性能有什么明顯改善。
遺憾的是,還是有相當(dāng)多的服務(wù)器不支持管線化。由于這個(gè)原因,HTTP 管線在 NSURLSession 中默認(rèn)是關(guān)閉的。如果想要啟用 HTTP 管線,需要將 NSURLSessionConfiguration 中的 HTTPShouldUsePipelining 設(shè)置為 YES。另外,建議服務(wù)器最好還是支持管線化。
我們都有在網(wǎng)絡(luò)不太好的情況下使用 app 的經(jīng)歷。很多 app 大概 15 秒左右就會(huì)結(jié)束請(qǐng)求并且反饋一個(gè)超時(shí)信息。這種設(shè)計(jì)其實(shí)是很不友好的。應(yīng)該給用戶一個(gè)他們可以理解的友好提示,諸如“你好,現(xiàn)在網(wǎng)絡(luò)狀況不太好,您需要多等一會(huì)兒?!薄5羌幢憔W(wǎng)絡(luò)狀況不好,只要連接還在,TCP 都會(huì)保證將請(qǐng)求發(fā)出去并且會(huì)一直等待響應(yīng)的返回,只是時(shí)間長短的問題。
從另一個(gè)角度來說:在較慢的網(wǎng)絡(luò)中,請(qǐng)求-響應(yīng)的RTT時(shí)間可能會(huì)有 17 秒。如果 15 秒就決定中止請(qǐng)求,就算用戶有足夠的耐心,他們也沒機(jī)會(huì)等到想要的操作結(jié)果。反過來,如果我們給出用戶相應(yīng)的提示信息,而他們又剛好愿意多等一會(huì),用戶可能會(huì)更喜歡使用這樣的應(yīng)用。
一直以來都有一種誤解,用重發(fā)請(qǐng)求來解決上面的問題。注意,這不是問題的關(guān)鍵,因?yàn)?TCP 有自己的重發(fā)機(jī)制。
正確的處理方式應(yīng)該是:每當(dāng)發(fā)起一個(gè)請(qǐng)求的時(shí)候,同時(shí)啟動(dòng)一個(gè) 10 秒計(jì)時(shí)器。如果請(qǐng)求在 10 秒之內(nèi)返回,就把計(jì)時(shí)器停掉。如果超過 10 秒,可以給用戶一個(gè)提示“網(wǎng)絡(luò)不好,請(qǐng)稍后?!保医ㄗh再給用戶一個(gè)取消按鈕,讓他們可以自行選擇等待還是取消請(qǐng)求,當(dāng)然提示信息的具體內(nèi)容和是否配備取消按鈕,這個(gè)可以視乎各 app 的情況去決定。總而言之,開發(fā)者最好不要直接替用戶做決定,比如直接中止他們的請(qǐng)求。
只要連接雙方的 IP 地址是不變的、可用的,連接就一定會(huì)是“活躍”的。如果把 iPhone 從 Wi-Fi 連接切換到 3G 網(wǎng)絡(luò),這樣連接就會(huì)變得不可用,因?yàn)槭謾C(jī)的 IP 地址發(fā)生了變化,基于原 IP 地址創(chuàng)建的路由自然是失效的。
看看第一個(gè)例子中發(fā)送的這段 header 信息:
If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"
這表示客戶端本地已經(jīng)針對(duì)所請(qǐng)求的資源做過緩存了,如果服務(wù)器上的資源有過更新,需要將最新的資源返回給客戶端,否則不需要返回。如果自己構(gòu)建客戶端和服務(wù)器的數(shù)據(jù)通信,建議充分利用這個(gè)機(jī)制。這種機(jī)制叫做 HTTP ETag,如果使用得當(dāng),會(huì)對(duì)通訊的速度有明顯的優(yōu)化。
記住“最快的請(qǐng)求是不發(fā)請(qǐng)求”。舉個(gè)極端的例子,拿一個(gè)請(qǐng)求來說,哪怕你有最好的網(wǎng)絡(luò),請(qǐng)求的數(shù)據(jù)量極小,有超快的服務(wù)器,你也不大可能在 50ms 內(nèi)拿到請(qǐng)求的響應(yīng)。這還只是一個(gè)請(qǐng)求。想想吧,如果有可能在本地創(chuàng)建相同的數(shù)據(jù),而且耗時(shí)小于 50ms,那就不要發(fā)這樣的請(qǐng)求。
針對(duì)已請(qǐng)求的資源,只要服務(wù)器上對(duì)應(yīng)的資源具備在一定時(shí)間內(nèi)不發(fā)生變化特性,建議在本地緩存起來。注意檢查 header 中緩存過期的相關(guān)屬性,也可以直接利用 NSURLSession 中的 NSURLRequestUseProtocolCachePolicy 策略。
利用 NSURLSession 發(fā) HTTP 請(qǐng)求是非常簡單便捷的。但是請(qǐng)求背后有很多技術(shù)點(diǎn)做支撐。只有知曉和理解其中的細(xì)節(jié)和內(nèi)涵才能更好的去優(yōu)化 HTTP 請(qǐng)求。用戶期望的是我們的 app 時(shí)時(shí)刻刻都是好用的。只有深刻理解 IP,TCP 和 HTTP 的工作原理才能更好的去滿足用戶的期望。