鍍金池/ 教程/ iOS/ IP,TCP 和 HTTP
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

IP,TCP 和 HTTP

當(dāng) app 和服務(wù)器進行通信的時候,大多數(shù)情況下,都是采用 HTTP 協(xié)議。HTTP 最初是為 web 瀏覽器而定制的,如果在瀏覽器里輸入 http://www.objc.io ,瀏覽器會通過 HTTP 協(xié)議和 www.objc.io 所對應(yīng)的服務(wù)器進行通信。

HTTP是運行在應(yīng)用層上的應(yīng)用協(xié)議,而不同的層級上都有相應(yīng)的協(xié)議在運行。層級的堆棧關(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)。本文會關(guān)注應(yīng)用層 (application layer)、傳輸層 (transport layer) 和網(wǎng)絡(luò)層 (internet layer),它們分別代表了典型的 HTTP 的應(yīng)用的 HTTP,TCP 以及 IP。在 IP 之下的是數(shù)據(jù)連接和物理層級,比如像 Ethernet 的實現(xiàn)之類的東西(Ethernet 擁有一個數(shù)據(jù)連接部分以及一個物理部分)。

如上文所述,我們只關(guān)注應(yīng)用層,傳輸層和互聯(lián)網(wǎng)層的部分,更確切的說,著重探討一種特殊的混合模式:基于 IP 的 TCP,以及基于 TCP 實現(xiàn)的 HTTP。這就是我們每天使用的 app 的基本網(wǎng)絡(luò)配置。

通過本文,希望大家能夠?qū)TTP工作原理有一個細致的了解,知道一些常見的 HTTP 問題的產(chǎn)生原因,從而能在實踐中盡量避免這些問題的發(fā)生。

其實在互聯(lián)網(wǎng)上傳遞數(shù)據(jù)的方式并不只 HTTP 一種。HTTP 之所以被廣泛使用的原因是其非常穩(wěn)定、易用,即便是防火墻一般也是允許 HTTP 協(xié)議穿透的。

接下來我們從最低的一層談起,說說 IP 網(wǎng)絡(luò)協(xié)議。

IP網(wǎng)絡(luò)協(xié)議 (IP-Internet Proctocol)

TCP/IP 中的 IP 是網(wǎng)絡(luò)協(xié)議 (Internet Protocol) 的縮寫。從字面意思便知,它是互聯(lián)網(wǎng)眾多協(xié)議的基礎(chǔ)。

IP 實現(xiàn)了分組交換網(wǎng)絡(luò)。在協(xié)議下,機器被叫做 主機 (host),IP 協(xié)議明確了 host 之間的資料包(數(shù)據(jù)包)的傳輸方式。

所謂數(shù)據(jù)包是指一段二進制數(shù)據(jù),其中包含了發(fā)送源主機和目標主機的信息。IP 網(wǎng)絡(luò)負責(zé)源主機與目標主機之間的數(shù)據(jù)包傳輸。IP 協(xié)議的特點是 best effort(盡力服務(wù),其目標是提供有效服務(wù)并盡力傳輸)。這意味著,在傳輸過程中,數(shù)據(jù)包可能會丟失,也有可能被重復(fù)傳送導(dǎo)致目標主機收到多個同樣的數(shù)據(jù)包。

IP 網(wǎng)絡(luò)中的主機都配有自己的地址,被稱為 IP 地址。每個數(shù)據(jù)包中都包含了源主機和目標主機的 IP 地址。IP 協(xié)議負責(zé)路徑計算,即 IP 數(shù)據(jù)包在網(wǎng)絡(luò)中的傳輸傳輸時,數(shù)據(jù)包所經(jīng)過的每一個主機節(jié)點都會讀取數(shù)據(jù)包中的目標主機地址信息,以便選擇朝什么地方傳送數(shù)據(jù)包。

今天,絕大多數(shù)的數(shù)據(jù)包仍舊是 IPv4(Internet Protocol version 4 網(wǎng)際協(xié)議版本 4)的,每一個 IPv4 地址是長度為 32 位。常見采用 dotted-decimal(點分十進制)表示法,具體形式如:198.51.100.42。

新的 IPv6 標準也正在逐漸推廣中。它有更大的地址空間:長度為 128 位,這使得數(shù)據(jù)包在網(wǎng)絡(luò)中傳輸時的尋址更容易一些。另外,由于有更多的地址可以分配,諸如網(wǎng)絡(luò)地址轉(zhuǎn)換等問題也迎刃而解。IPv6 的表示形式為:八組十六進制數(shù)以冒號分割,比如:2001:0db8:85a3:0042:1000:8a2e:0370:7334。

IP Hearder

一個 IP 數(shù)據(jù)包通常包含 header (報頭信息) 和 payload (有效載荷)。

payload 中的內(nèi)容即是要傳輸?shù)恼嬲畔ⅲ?header 承載的是與傳輸數(shù)據(jù)有關(guān)的元數(shù)據(jù) (metadata)。

IPv4 Header

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é)(不包含極少用到的可選項信息)。

header 信息中最關(guān)鍵的是源和目標 IP 地址。除此之外,版本信息是 4,代表 IPv4。protocol(協(xié)議區(qū))代表 payload 采用的傳輸協(xié)議。TCP 的協(xié)議號是 6。Total Length(總長度區(qū))標明了 header 加 payload 整個數(shù)據(jù)包的大小。

詳情參看維基百科中關(guān)于 IPv4 的條目,里面有關(guān)于 header 各個區(qū)域信息的詳細介紹。

IPv6 Header

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)過多年來對 IPv4 使用的總結(jié),如今 IPv6 的 header 信息簡化了許多。

除了源和目標地址這種必備信息外,IPv6 提供專門的 next header 區(qū)域來指明緊接 header 的數(shù)據(jù)是什么。也就是說,IPv6 允許在數(shù)據(jù)包中將 header 鏈接起來。每一個被鏈接的 IPv6 header 都會有一個 next header 字段,直到到達實際的 payload 數(shù)據(jù)。比如說,當(dāng) next header 的值為 6 (TCP 的協(xié)議號) 時,數(shù)據(jù)包的其他信息就是 TCP 協(xié)議要傳輸?shù)臄?shù)據(jù)。

同樣的,更多信息請參考維基百科上關(guān)于 IPv6 數(shù)據(jù)包的條目

Fragmentation (數(shù)據(jù)分片)

由于底部鏈路層對所傳輸?shù)臄?shù)據(jù)幀有最大長度限制(最大傳輸單元,MTU),所以有時候 IPv4 需要對所傳數(shù)據(jù)包進行分片。具體表現(xiàn)為,如果數(shù)據(jù)包尺寸超過了所要經(jīng)過的數(shù)據(jù)鏈路的最大傳輸限制,路由就會對數(shù)據(jù)包進行分片。當(dāng)分片數(shù)據(jù)包到達目標主機后,可以根據(jù)分片信息進行數(shù)據(jù)重組。當(dāng)然,數(shù)據(jù)發(fā)送源有權(quán)決定路由是否啟用對傳輸數(shù)據(jù)包進行分片,假如所傳輸?shù)臄?shù)據(jù)超過了輸送限制,又禁止了路由分片,發(fā)送源會收到 ICMP(Internet Control Message Protocol,Internet報文控制協(xié)議) 的數(shù)據(jù)幀超長報告信息。

在IPv6中,如果數(shù)據(jù)包超限制,路由會直接丟棄數(shù)據(jù)包并且向發(fā)送源回傳 ICMP6數(shù)據(jù)幀超長報告信息。源和目標兩端會基于這個特性來進行路徑 MTU 發(fā)現(xiàn),以此尋找兩端之間最大傳輸單元(maximum transfer unit)所在的路由。找到 MTU 路由后,僅當(dāng)上層數(shù)據(jù)包的最小 payload 確實超過了 MTU,IPv6 才會進行分片傳輸。對于 IPv6 下的 TCP 來說,這不會造成什么問題。

TCP - 傳輸控制協(xié)議 (Trasnmission Control Protocol)

TCP 層位于 IP 層之上,是最受歡迎的因特網(wǎng)通訊協(xié)議之一,人們通常用 TCP/IP 來泛指整個因特網(wǎng)協(xié)議族。

剛剛提到,IP 協(xié)議允許兩個主機之間傳送單一數(shù)據(jù)包。為了保證對所傳送數(shù)據(jù)包達到盡力服務(wù)的目的,最終的傳輸?shù)慕Y(jié)果可能是數(shù)據(jù)包亂序、重復(fù)甚至丟包。

TCP 是基于 IP 層的協(xié)議。但是 TCP 是可靠的、有序的、有錯誤檢查機制的基于字節(jié)流傳輸?shù)膮f(xié)議。這樣當(dāng)兩個設(shè)備上的應(yīng)用通過 TCP 來傳遞數(shù)據(jù)的時候,總能夠保證目標接收方收到的數(shù)據(jù)的順序和內(nèi)容與發(fā)送方所發(fā)出的是一致的。TCP 做的這些事看起來稀松平常,但是比起 IP 層的粗曠處理方式已經(jīng)是有顯著的進步了。

應(yīng)用程序之間可以通過 TCP 建立鏈接。TCP 建立的是雙向連接,通信雙方可以同時進行數(shù)據(jù)的傳輸。連接的雙方都不需要操心數(shù)據(jù)是否分塊,或者是否采用了盡力服務(wù)等。TCP 會確保所傳輸?shù)臄?shù)據(jù)的正確性,即接受方收到的數(shù)據(jù)與發(fā)出方的數(shù)據(jù)一致。

HTTP 是典型的 TCP 應(yīng)用。用戶瀏覽器(應(yīng)用 1)與 web 服務(wù)器(應(yīng)用 2)建立連接后,瀏覽器可以通過連接發(fā)送服務(wù)請求,web 服務(wù)器可以通過同樣的連接對請求做出響應(yīng)。

同一個 host 主機上可以有多個應(yīng)用同時使用 TCP 協(xié)議。TCP 用不同的端口來區(qū)分應(yīng)用。作為連接的兩端,發(fā)送源和接收目標分別擁有自己的 IP 地址和端口號。憑借這樣一對 IP 地址和端口號,就可以唯一標識一個連接。

使用 HTTPS 的 web 服務(wù)器會監(jiān)聽 443 端口。瀏覽器作為發(fā)送源會啟用一個臨時端口結(jié)合自己的 IP 地址與目標服務(wù)器對應(yīng)的端口和 IP 地址建立 TCP 連接。

TCP 在 IPv4 和 IPv6 上是無差別運行的。所以,如果 IPv4 的 Protocol 或 IPv6 的 Next Hearder的協(xié)議號被設(shè)置成 6,表示執(zhí)行 TCP 協(xié)議。

TCP Segments (TCP 報文段)

主機之間傳輸?shù)臄?shù)據(jù)流一般先會被分塊,再轉(zhuǎn)化成 TCP 的報文段,最終會生成 IP 數(shù)據(jù)包中的 payload 載荷數(shù)據(jù)。

每個 TCP 報文段都有 header 信息和對應(yīng)的載荷 payload。payload 信息就是待傳輸?shù)臄?shù)據(jù)塊。TCP 報文段的 header 信息中主要包含的是源和目標端口號,至于說源和目標的 IP 地址信息則已經(jīng)包含在 IP header 信息中了。

TCP 的報文段 header 信息中還有報文序列號、確認號等其他一些用于管理連接的信息。

所謂序列號信息,其實就是為每個報文段分配的唯一編號。第一個報文段的序列號是隨機的,比如:1721092979,其后的每一個報文段的序列號都以此號為基礎(chǔ)依次加 1,1721092980,1721092981 等等。至于確認號,是目標端反饋給源的確認信息,通知源目前已經(jīng)接到哪些報文段了。由于 TCP 是雙向的,所以數(shù)據(jù)和確認信息發(fā)送也都是雙向的。

TCP 連接

連接管理是 TCP 的核心功能之一,而且協(xié)議需要解決由于IP層采用不可靠傳輸引發(fā)的一系列復(fù)雜問題。下面會分別介紹TCP的連接建立、數(shù)據(jù)傳輸以及連接終止的詳細過程。

TCP 連接全過程的狀態(tài)變化是很復(fù)雜的(參考 TCP 狀態(tài)圖)。但是大多數(shù)情況下還是比較簡單的。

連接建立

TCP 連接都是建立在兩個主機之間的。所以,每個連接建立過程中都存在兩個角色:一端(例如 web 服務(wù)器)監(jiān)聽連接,另一端(例如應(yīng)用)主動連接正在監(jiān)聽的一端(web 服務(wù)器)。服務(wù)器端的這種監(jiān)聽行為被稱為 passive open(被動打開)。客戶端主動連接服務(wù)器的行為被稱為 active open(主動打開)。

TCP 會通過三次握手來完成連接建立,具體過程是這樣的:

  1. 客戶端首先向服務(wù)端發(fā)送一個 SYN 包和一個隨機序列號 A
  2. 服務(wù)端收到后會回復(fù)客戶端一個 SYN-ACK 包以及一個確認號(用于確認收到 SYN)A+1,同時再發(fā)送一個隨機序列號 B
  3. 客戶端收到后會發(fā)送一個 ACK 包以及確認號(用于確認收到 SYN-ACK)B+1 和序列號 A+1 給服務(wù)端

SYNsynchronize sequence numbers (同步序列號) 的縮寫。兩端在傳遞數(shù)據(jù)時,所傳遞的每個 TCP 報文段都有一個序列號。就是利用這種機制,TCP 可以確保分塊傳輸?shù)臄?shù)據(jù)包最終都以正確的個數(shù)和順序抵達目標端。在正式傳輸開始之前,源和目標端需要同步確認第一個報文的序列號。

ACKacknowledgment (確認)的縮寫。當(dāng)某一端接到了報文包后,通過回傳已報文序列號來確認接收到報文這件事。

運行如下語句:

curl -4 http://www.apple.com/contact/

這是通過 curl 命令與 www.apple.com 的 80 端口創(chuàng)建一個 TCP 連接。

www.apple.com 所在服務(wù)器 23.63.125.15(注意,整個 IP 不是固定的)會監(jiān)聽 80 端口。我們自己的 IP 地址是 10.0.1.6,啟用的臨時端口 52181(這個端口是從可用端口中隨機選擇的)。利用 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

這里信息量很大。下面要逐個分析一下。

最左邊是系統(tǒng)時間。當(dāng)時執(zhí)行命令的時間是晚上18:31。后面的 IP 代表的是這些都是 IP 協(xié)議數(shù)據(jù)包。

接下來看這段 10.0.1.6.52181 > 23.63.125.15.80,這一對是源和目標端的 IP 地址+端口。第一行和第三行是客戶端發(fā)向服務(wù)端的信息,第二行是服務(wù)端發(fā)向客戶端的。tcpdump 會自動把端口號加到 IP 地址后頭,比如 10.0.1.6.52181 表示 IP 地址為 10.0.1.6,端口號為 52181。

Flags 表示 TCP 報文段 header 信息中的一些縮寫標識:S 代表 SYN,. 代表ACKP 代表PUSH,FFIN。還有一些其他的標識,這邊就不羅列了。注意上面三行 Flags 中先是攜帶 SYN ,接著是 SYN-ACK,最后是 ACK,這就是三次握手確認的全過程。

另外,第一行中客戶端發(fā)送了一個隨機序列號 1721092979 (就是上文所說的A)給服務(wù)器。第二行展示的是服務(wù)器回傳給客戶端的確認號 1721092980 (A+1) 和一個隨機序列號 673593777 (B)。 最后在第三行,客戶端將自己的確認號 673593778 (B+1) 發(fā)還給服務(wù)端。

其他選項

當(dāng)然,在連接建立過程中還會配置一些其他的信息。比如第一行中客戶端發(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 往返時間 (round-trip time) 的。TS val 是發(fā)送方的 時間戳 (time stamp),ecr相應(yīng)應(yīng)答 (echo reply) 時間戳,通常情況下就是發(fā)送方收到的最后時間戳。TCP 以 RTT 作為其擁塞控制算法 (congestion-control algorithms) 的依據(jù)。

連接的兩端都發(fā)送 sackOK。這樣會啟用選擇性確認 (Selective Acknowledgement) 機制,使連接雙方能夠確認收到的字節(jié)范圍。一般情況下,確認機制只是確認接受方已收到的數(shù)據(jù)的字節(jié)總數(shù)。RFC 2018 第 3 部分有對 SACK 的詳細闡述。

mss 選項聲明了最大報文長度 (Maximum Segment Size),表示接收端希望接收的單個報文的最大長度(以字節(jié)為單位)。wscale窗口放大因子 (window scale factor),稍后會詳細說明。

數(shù)據(jù)傳輸

一旦建立了連接,雙方就可以互發(fā)數(shù)據(jù)了。發(fā)送端所發(fā)出的每個報文段都有一個序列號,這個序列號與當(dāng)下已傳送的字節(jié)總數(shù)有關(guān)。接收端會針對已接收的數(shù)據(jù)包向源端發(fā)送確認報文,確認信息同樣是由報文 header 所攜帶的 ACK。

假設(shè)現(xiàn)在傳送的信息是除最后一個報文 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

整個機制是雙向運轉(zhuǎn)的。A 主機會持續(xù)的發(fā)送數(shù)據(jù)包。B 收到數(shù)據(jù)包后會向 A 發(fā)送確認信息。A 發(fā)送數(shù)據(jù)包的過程不需要等待 B 的確認。

TCP 將流量控制和其他一系列復(fù)雜機制結(jié)合起來進行擁塞控制。需要處理以下問題:針對丟失的報文采用重發(fā)機制,同時還需要動態(tài)的調(diào)整發(fā)送報文的頻率。

流量控制的原則是發(fā)送方發(fā)送數(shù)據(jù)的速度不能比接收方處理數(shù)據(jù)的速度快。接收方,也就是所謂的 接收窗口 (receive window) 會告知發(fā)送方自身接收窗口數(shù)據(jù)緩沖區(qū)的大小。從上面 tcpdump 的輸出來看,窗口大小是 win 65535,wscale(窗口放大因子)是 4。這些數(shù)字的意思是說,10.0.1.6 主機的接收窗口大小是 4*64 kB = 256 kB,23.63.125.15 主機的 win 是 14480,wscale 是 1,接收窗口約為 14KB??傊?,不管哪一方作為數(shù)據(jù)接收方,都會向?qū)Ψ酵▓笞约旱慕邮沾翱诖笮 ?/p>

擁塞控制要更復(fù)雜一些。所有擁塞控制的目標都是要計算出當(dāng)前網(wǎng)絡(luò)中數(shù)據(jù)傳輸?shù)淖罴阉俾?。所謂最佳速率就是要達到一種微妙的平衡。一方面,是希望速度越快越好,另一方面,速度快意味著數(shù)據(jù)傳輸多,這樣處理性能會大打折扣甚至導(dǎo)致崩潰。而這種超負荷崩潰是分組交換網(wǎng)絡(luò)的固有特點。當(dāng)負載過大,數(shù)據(jù)包之間會產(chǎn)生擁塞,直接導(dǎo)致丟包率急速上升。

擁塞控制還需要充分考慮對流量的影響。RFC 5681 中對 TCP 擁塞控制有 6,000 字左右的闡述。發(fā)送方要時刻關(guān)注來自接收方的確認信息。要做到這點并不簡單,有的時候還需要一定的妥協(xié)。要知道底部 IP 協(xié)議數(shù)據(jù)包是無序傳輸?shù)模瑪?shù)據(jù)包會丟失也會重復(fù)。發(fā)送方需要評估 RTT 往返時間,然后基于 RTT 去確定是否收到了接收方的確認信息。重發(fā)數(shù)據(jù)包也有很大代價,除了連接延遲問題,網(wǎng)絡(luò)的負載也會發(fā)生明顯的波動。導(dǎo)致 TCP 需要不停的去適應(yīng)當(dāng)前網(wǎng)絡(luò)情況。

更重要的是,TCP 連接本身是易變的。除了數(shù)據(jù)傳輸,連接的兩端還會不時的發(fā)送一些提醒和確認信息以便可以適當(dāng)?shù)恼{(diào)整狀態(tài)來維持連接。

基于這種一直在相互協(xié)調(diào)中的連接關(guān)系,TCP 連接往往會是短暫而低效的。在建立連接的初期,TCP 協(xié)議算法還不能完全了解當(dāng)前網(wǎng)絡(luò)狀況。而在連接將要結(jié)束的時候,反饋給發(fā)送方的信息又可能不充分,這樣就很難對連接狀況做出實時的合理的評估。

之前展示了客戶端和服務(wù)端之間交換的三段報文。再看看關(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ā)送的第一段報文長度是 85 bytes (HTTP 請求)。由于在上一個報文發(fā)送后沒有收到來自服務(wù)端的信息,所以 ACK 確認號的值不變。

服務(wù)端 23.63.125.15 只是對接收客戶端的數(shù)據(jù)進行確認回復(fù),沒有向客戶端發(fā)送數(shù)據(jù),所以 length 為 0。由于當(dāng)前連接是采用選擇性確認 (Selective acknowledgments),所以序列號和確認號是之間的字節(jié)長度是從 1721092980 到 1721093065,也就是 85 bytes。接收方發(fā)送的 ACK 確認號是 1721093065,這代表目前已接收的數(shù)據(jù)確認累計到 1721093065 字節(jié)了。至于說為什么數(shù)字會如此之大,這要說到初次握手時發(fā)出的隨機數(shù),數(shù)字的范圍和那個初始數(shù)字是相關(guān)的。

這種模式會一直持續(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 

終止連接

最終連接會終止(或結(jié)束)。連接的每一端都會發(fā)送 FIN 標識給另一端來聲明結(jié)束傳輸,接著另一端會對收到 FIN 進行確認。當(dāng)連接兩端均發(fā)送完各自 FIN 和做出相應(yīng)的確認后,連接將會徹底關(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,同時在這個報文信息中還對第一行中另一端發(fā)送的 FIN 予以 ACK(以.代表)確認。

HTTP — 超文本傳輸協(xié)議 (Hypertext Transfer Protocol)

1989 年,Tim Berners Lee 在 CERN(European Organization for Nuclear Research 歐洲原子核研究委員會) 擔(dān)任軟件咨詢師的時候,開發(fā)了一套程序,奠定了萬維網(wǎng)的基礎(chǔ)。HyperText Transfer Protocol(超文本轉(zhuǎn)移協(xié)議,即HTTP)是用于從 WWW 服務(wù)器傳輸超文本到本地瀏覽器的傳送協(xié)議。RFC 2616 定義了今天普遍使用的一個版本:HTTP 1.1。

請求與響應(yīng)

HTTP 采用簡單的請求和響應(yīng)機制。在 Safari 輸入 http://www.apple.com 時,會向 www.appple.com 所在的服務(wù)器發(fā)送一個 HTTP 請求。服務(wù)器會對請求做出一個響應(yīng),將請求結(jié)果信息返回給 Safari。

每一個請求都有一個對應(yīng)的響應(yīng)信息。請求和響應(yīng)遵從同樣的格式。第一行是請求行或者響應(yīng)狀態(tài)行。接下來是 header 信息,header 信息之后會有一個空行??招兄笫?body 請求信息體。

一個簡單請求

當(dāng) Safari 加載 HTML 頁面 http://www.objc.io/about.html 的時候,先是發(fā)送 HTTP 請求到 www.objc.io,請求的內(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

第一行是請求行。它包含三部分信息:動作,資源信息,還有 HTTP 的版本。

本例中,動作是 GET。所謂動作也就是常說的 HTTP 請求方法。資源信息表明所請求的資源。例子中的資源信息是 /about.html,這表示我們想 get 服務(wù)器的在 /about.html 位置中的文檔。當(dāng)前 HTTP 版本是 HTTP/1.1。

接下來 10 行是 HTTP header 信息。跟著是一行空行。例子中的請求沒有 body 信息。

header 的作用是向服務(wù)器傳遞一些額外的輔助信息,它的內(nèi)容比較寬泛。維基百科中有常用 HTTP header 關(guān)鍵字信息的清單。例子中的 header 信息 Host: www.objc.io 表示告訴服務(wù)器,本次請求的服務(wù)器名稱是什么。這樣可以讓同一個服務(wù)器處理針對多個域名的請求。

下面是一些常見的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 對給定媒體類型的優(yōu)先級要求。Accept-Language 代表 Safari 希望接收的自然語言清單。這會要求服務(wù)器盡可能的根據(jù)清單要求去匹配相應(yīng)的語言。

Accept-Encoding: gzip, deflate

通過這個header,Safari 告訴服務(wù)器可以對響應(yīng) body 做壓縮處理。如果 header 信息中沒有設(shè)置壓縮標識,那么服務(wù)器就必須返回沒有壓縮過的信息。壓縮可以大大減少數(shù)據(jù)的傳輸量,在文本信息 (比如 HTML) 中尤為明顯。

If-Modified-Since: Mon, 10 Feb 2014 18:08:48 GMT
If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"

這兩行信息表明 Safari 已經(jīng)對請求結(jié)果做過緩存。如果服務(wù)器上的待請求內(nèi)容在 2 月 10 號以后發(fā)生過變化或者是 ETag 與 a54907f38b306fe3ae4f32c003ddd507 不匹配,這就表示請求結(jié)果與當(dāng)前緩存信息不一致,需要服務(wù)器返回最新的請求結(jié)果。

User-Agent 是告知服務(wù)器當(dāng)前發(fā)送請求的客戶端類型。

一個簡單響應(yī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 表示所請求的信息自上次訪問以來沒有變化。

響應(yīng)中沒有包含 body 信息。也就是說服務(wù)器通知客戶端:你的版本已經(jīng)是最新了,可以直接使用當(dāng)前緩存信息。

關(guān)閉緩存

curl 發(fā)送一個請求:

% curl http://www.apple.com/hotnews/ > /dev/null

curl 沒有使用本地緩存。整個請求會是這樣的:

GET /hotnews/ HTTP/1.1
User-Agent: curl/7.30.0
Host: www.apple.com
Accept: */*

這個請求與之前 Safari 發(fā)的請求很類似。但是 curl 請求的 header 信息中沒有 If-None-Match,所以服務(wù)器必須將請求結(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" />

后面還會有一些,現(xiàn)在收到的響應(yīng)里 body 中包含了 HTML 文檔信息。

Apple 服務(wù)器響應(yīng)的狀態(tài)碼200,這是標準的表示 HTTP 請求成功的狀態(tài)碼。

服務(wù)器同時還告知響應(yīng)媒體類型是 text/html;字符集 charset=UTF-8;內(nèi)容長度 Content-Length:12342,代表了 body 信息的大小。

HTTPS - 安全的 HTTP

Transport Layer Security (安全傳輸層協(xié)議,TLS) 是一種基于 TCP 的加密協(xié)議。它支持兩件事:傳輸?shù)膬啥丝梢曰ハ囹炞C對方的身份,以及加密所傳輸?shù)臄?shù)據(jù)?;?TLS 的 HTTP 請求就是 HTTPS。

用 HTTPS 去替代 HTTP,在安全方面會有顯著的提升。也許你還會采用一些其他的安全措施,總之這都會為安全通信提供保障。

TLS 1.2

如果服務(wù)器支持的話,你應(yīng)該將 TLSMinimumSupportedProtocol 設(shè)置為 kTLSProtocol12,以要求使用 TLS 1.2 版本。這能有效的防御中間人攻擊。

證書鎖定 (Certificate Pinning)

如果不確定數(shù)據(jù)接收方的身份,那么即便對所傳輸數(shù)據(jù)進行加密也沒什么意義。服務(wù)器的證書可以表明服務(wù)器的身份,只允許和持有某個特定證書的一方建立連接,就就是證書鎖定

如果一個客戶端通過 TLS 和服務(wù)器建立連接,操作系統(tǒng)會驗證服務(wù)器證書的有效性。當(dāng)然,有很多手段可以繞開這個校驗,最直接的是在 iOS 設(shè)備上安裝證書并且將其設(shè)置為可信的。這種情況下,實施中間人攻擊也不是什么難事。

可以使用證書鎖定來規(guī)避這種風(fēng)險(或者說是將風(fēng)險降到最低)。當(dāng)建立 TLS 連接后,應(yīng)立即檢查服務(wù)器的證書,不僅要驗證證書的有效性,還需要確定證書和其持有者是否匹配。考慮到應(yīng)用和服務(wù)器需要同時升級證書的要求,這種方式比較適合應(yīng)用在訪問自家服務(wù)器的情況下。

為了實現(xiàn)證書鎖定,在建立連接的過程中需要對服務(wù)器進行信任檢查 (server trust)。每當(dāng)通過 NSURLSession 創(chuàng)建了連接,NSURLSession 的代理就會收到一個 -URLSession:didReceiveChallenge:completionHandler: 的調(diào)用。傳遞的參數(shù) NSURLAuthenticationChallenge 有一個屬性 protectionSpace,它是 NSURLProtectionSpace 的實例,它有一個 serverTrust 屬性。

serverTrust 是一個 SecTrustRef 對象。Security 框架提供了很多方法用于驗證 SecTrustRef。AFNetworking 項目中的 AFSecurityPolicy 就是一個不錯的使用。一如既往的提醒大家,如果要自己構(gòu)建安全驗證相關(guān)的代碼,請一定要認真做好代碼審查,千萬不要再出現(xiàn)諸如 goto fail; 這類 bug。

綜合討論

現(xiàn)在大家對 IP,TCP 和 HTTP 的工作原理有了一定的了解了。下面說說還可以做些什么以及一些相關(guān)注意事項。

有效地使用連接

TCP 連接容易在兩個時點出現(xiàn)問題:初始設(shè)置,以及通過連接傳輸?shù)淖詈笠徊糠謭笪摹?/p>

建立連接

連接設(shè)置可能會非常耗時。正如前文所說,TCP 建立連接的過程中需要進行三次握手。這個過程中本身沒有太多的數(shù)據(jù)需要傳遞。但是,對于移動網(wǎng)絡(luò)來說,從手機端向服務(wù)器端發(fā)送一個數(shù)據(jù)包普遍需要 250ms,也就是四分之一秒。推及到三次握手,也就是說在還沒有傳送任何數(shù)據(jù)之前,光建立連接就要花費 750ms。

HTTPS 的情況更夸張,由于 HTTPS 是基于 TLS 的 HTTP,而 HTTP 又基于 TCP。TCP 連接就要執(zhí)行三次握手,然后到了 TLS 層還會再握手三次。估算一下,建立一個 HTTPS 連接的耗時至少是創(chuàng)建一個 HTTP 連接的兩倍。如果 RTT 時間是 500ms(假設(shè)單程 250ms),HTTPS 建立連接累計總耗時將達1.5秒。

不管建立連接后是要傳遞多少數(shù)據(jù),建立連接本身都太過耗時了。

另一個影響 TCP 連接的因素是傳送大規(guī)模數(shù)據(jù)。如果要在網(wǎng)絡(luò)情況未知的條件下傳送報文,TCP 需要偵測當(dāng)前網(wǎng)絡(luò)的能力。換句話說,TCP 得花費一定的時間去計算此網(wǎng)絡(luò)的最佳傳輸速率。上文提到過,TCP 需要逐步調(diào)整以便找到最佳速度。這種算法被稱為 慢啟動 (slow-start)。還有一點值得注意,慢啟動策略在那些數(shù)據(jù)鏈路層傳輸質(zhì)量較差的網(wǎng)絡(luò)環(huán)境中的表現(xiàn)更差,無線網(wǎng)絡(luò)就是典型的例子。

結(jié)束連接

另一個問題主要存在于數(shù)據(jù)傳輸?shù)淖詈箅A段。每當(dāng)客戶端發(fā)起 HTTP 請求某些資源的時候,服務(wù)器會持續(xù)的向客戶端主機發(fā)送 TCP 報文數(shù)據(jù),客戶端收到數(shù)據(jù)后會給服務(wù)器反饋 ACK 確認信息。假如某個報文在傳輸過程中發(fā)生丟包,那么服務(wù)器也就不會收到該包的確認 ACK。一旦服務(wù)器發(fā)現(xiàn)有數(shù)據(jù)包沒有 ACK 反饋,就會觸發(fā)快速重傳 (fast retransmit)

每當(dāng)某個數(shù)據(jù)包丟失,數(shù)據(jù)接收方在收到下個數(shù)據(jù)包后發(fā)出的確認 ACK 與所接收的前一個數(shù)據(jù)包的確認 ACK 相同。那么數(shù)據(jù)發(fā)送方自然就會收到重復(fù)的 ACK。除了報文丟失,還有很多種網(wǎng)絡(luò)狀況會導(dǎo)致重復(fù) ACK 的問題。一般情況下,如果數(shù)據(jù)發(fā)送方連續(xù)收到 3 個重復(fù)的 ACK 就會立即進行快速重發(fā)。

這所導(dǎo)致的問題將發(fā)生在數(shù)據(jù)傳輸?shù)氖瘴搽A段。如果發(fā)送方完成數(shù)據(jù)發(fā)送,接受方自然會停止發(fā)送 ACK 確認。在最后四個報文傳輸?shù)倪^程中,快速重發(fā)算法是沒有辦法處理這四個報文的數(shù)據(jù)包的丟失問題的(因為不會收到三個相同的確認 ACK,所以不能界定傳輸丟包)。在常規(guī)網(wǎng)絡(luò)環(huán)境下,四個數(shù)據(jù)包相當(dāng)于 5.7kB 的數(shù)據(jù)規(guī)模??傊?,在這最后 5.7kB 的傳輸?shù)倪^程中,快速重發(fā)機制是無效的。針對這種情況,TCP 會啟用其他機制來偵測丟包問題。對于這種情況,重傳操作可能要消耗幾秒鐘去執(zhí)行,這并不奇怪。

長連接和管線化

HTTP 有兩種策略來解決這些問題。最簡單的是 HTTP 持久連接 (persistent connection),也被稱為長連接 (keep-alive)。具體就是,每當(dāng) HTTP 完成一組請求-響應(yīng)處理后,還會繼續(xù)復(fù)用相同的 TCP 連接。而 HTTPS 會復(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ā)送多個請求,也就是說無需等待上一個請求的響應(yīng)完成可以發(fā)下一個請求。這表示能同時處理請求和響應(yīng),請求處理的順序采用先進先出原則,響應(yīng)結(jié)果會按照請求發(fā)出的順序依次返還給客戶端。

稍微簡化一下,看起來會是這樣:

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)是實時的,不會等到接收完全部請求才處理。

可以利用這個特點來提升 TCP 的效率。只需要在建立連接初始階段執(zhí)行握手,而后一直復(fù)用同樣的連接,這樣 TCP 就可以最大限度的利用帶寬。此種情況下,擁塞控制也會隨之提升。因為快速重發(fā)機制無法處理的最末四個報文丟失情況只會發(fā)生在使用本連接的最后一個請求-響應(yīng)中,而不是像之前那樣每一個請求-響應(yīng)都需要建立自己的連接,每個連接中都可能出現(xiàn)最后四個報文丟失的問題。

HTTP 管線化對高網(wǎng)絡(luò)延遲連接的通訊性能提升尤為顯著,在你的 iPhone 沒有通過 Wi-Fi 訪問網(wǎng)絡(luò)的時候,此類網(wǎng)絡(luò)連接就屬于高延遲范疇。實際上,有調(diào)查顯示,在移動網(wǎng)絡(luò)環(huán)境下,SPDY 的通訊性能并不優(yōu)于 HTTP 管線。

RFC 2616 指明,在與同一個服務(wù)器通訊的時候,如果啟用了 HTTP 管線,建議啟用兩個連接。按照說明所述,這樣能獲得最優(yōu)響應(yīng)效率,能最大限度避免擁塞。增加更多的連接也不會再對性能有什么明顯改善。

遺憾的是,還是有相當(dāng)多的服務(wù)器不支持管線化。由于這個原因,HTTP 管線在 NSURLSession 中默認是關(guān)閉的。如果想要啟用 HTTP 管線,需要將 NSURLSessionConfiguration 中的 HTTPShouldUsePipelining 設(shè)置為 YES。另外,建議服務(wù)器最好還是支持管線化。

超時處理

我們都有在網(wǎng)絡(luò)不太好的情況下使用 app 的經(jīng)歷。很多 app 大概 15 秒左右就會結(jié)束請求并且反饋一個超時信息。這種設(shè)計其實是很不友好的。應(yīng)該給用戶一個他們可以理解的友好提示,諸如“你好,現(xiàn)在網(wǎng)絡(luò)狀況不太好,您需要多等一會兒?!?。但是即便網(wǎng)絡(luò)狀況不好,只要連接還在,TCP 都會保證將請求發(fā)出去并且會一直等待響應(yīng)的返回,只是時間長短的問題。

從另一個角度來說:在較慢的網(wǎng)絡(luò)中,請求-響應(yīng)的RTT時間可能會有 17 秒。如果 15 秒就決定中止請求,就算用戶有足夠的耐心,他們也沒機會等到想要的操作結(jié)果。反過來,如果我們給出用戶相應(yīng)的提示信息,而他們又剛好愿意多等一會,用戶可能會更喜歡使用這樣的應(yīng)用。

一直以來都有一種誤解,用重發(fā)請求來解決上面的問題。注意,這不是問題的關(guān)鍵,因為 TCP 有自己的重發(fā)機制。

正確的處理方式應(yīng)該是:每當(dāng)發(fā)起一個請求的時候,同時啟動一個 10 秒計時器。如果請求在 10 秒之內(nèi)返回,就把計時器停掉。如果超過 10 秒,可以給用戶一個提示“網(wǎng)絡(luò)不好,請稍后?!?,我建議再給用戶一個取消按鈕,讓他們可以自行選擇等待還是取消請求,當(dāng)然提示信息的具體內(nèi)容和是否配備取消按鈕,這個可以視乎各 app 的情況去決定??偠灾_發(fā)者最好不要直接替用戶做決定,比如直接中止他們的請求。

只要連接雙方的 IP 地址是不變的、可用的,連接就一定會是“活躍”的。如果把 iPhone 從 Wi-Fi 連接切換到 3G 網(wǎng)絡(luò),這樣連接就會變得不可用,因為手機的 IP 地址發(fā)生了變化,基于原 IP 地址創(chuàng)建的路由自然是失效的。

緩存

看看第一個例子中發(fā)送的這段 header 信息:

If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"

這表示客戶端本地已經(jīng)針對所請求的資源做過緩存了,如果服務(wù)器上的資源有過更新,需要將最新的資源返回給客戶端,否則不需要返回。如果自己構(gòu)建客戶端和服務(wù)器的數(shù)據(jù)通信,建議充分利用這個機制。這種機制叫做 HTTP ETag,如果使用得當(dāng),會對通訊的速度有明顯的優(yōu)化。

記住“最快的請求是不發(fā)請求”。舉個極端的例子,拿一個請求來說,哪怕你有最好的網(wǎng)絡(luò),請求的數(shù)據(jù)量極小,有超快的服務(wù)器,你也不大可能在 50ms 內(nèi)拿到請求的響應(yīng)。這還只是一個請求。想想吧,如果有可能在本地創(chuàng)建相同的數(shù)據(jù),而且耗時小于 50ms,那就不要發(fā)這樣的請求。

針對已請求的資源,只要服務(wù)器上對應(yīng)的資源具備在一定時間內(nèi)不發(fā)生變化特性,建議在本地緩存起來。注意檢查 header 中緩存過期的相關(guān)屬性,也可以直接利用 NSURLSession 中的 NSURLRequestUseProtocolCachePolicy 策略。

總結(jié)

利用 NSURLSession 發(fā) HTTP 請求是非常簡單便捷的。但是請求背后有很多技術(shù)點做支撐。只有知曉和理解其中的細節(jié)和內(nèi)涵才能更好的去優(yōu)化 HTTP 請求。用戶期望的是我們的 app 時時刻刻都是好用的。只有深刻理解 IP,TCP 和 HTTP 的工作原理才能更好的去滿足用戶的期望。