在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

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

IP,TCP 和 HTTP

當(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é)議。

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

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。

IP Hearder

一個(gè) IP 數(shù)據(jù)包通常包含 header (報(bào)頭信息) 和 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é)(不包含極少用到的可選項(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 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)過多年來對(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ù)包的條目

Fragmentation (數(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 - 傳輸控制協(xié)議 (Trasnmission Control Protocol)

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é)議。

TCP Segments (TCP 報(bào)文段)

主機(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 連接

連接管理是 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ì)通過三次握手來完成連接建立,具體過程是這樣的:

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

SYNsynchronize 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)。

ACKacknowledgment (確認(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 代表PUSHFFIN。還有一些其他的標(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ù)端。

其他選項(xiàng)

當(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ì)說明。

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

一旦建立了連接,雙方就可以互發(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)。

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

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。

請(qǐng)求與響應(yīng)

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)求信息體。

一個(gè)簡單請(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)求的客戶端類型。

一個(gè)簡單響應(yī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)前緩存信息。

關(guān)閉緩存

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 信息的大小。

HTTPS - 安全的 HTTP

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

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

TLS 1.2

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

證書鎖定 (Certificate Pinning)

如果不確定數(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ò)就是典型的例子。

結(jié)束連接

另一個(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ù)器最好還是支持管線化。

超時(shí)處理

我們都有在網(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 策略。

總結(jié)

利用 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 的工作原理才能更好的去滿足用戶的期望。