前言
作爲本系列的第一篇文章,讓我們首先聊聊協議棧模型(stack layer model)。還記得在大學課堂裏的時候學TCP/IP的時候,我其實並不能分清楚ISO/OSI分層模型和TCP/IP分層模型二者的區別. 就是下面這幅圖
可以看出,雖然層次劃分方式有區別,但大體上還是能對應上的。那麼,爲什麼要這麼分層?
爲什麼要分層
分層的好處在於,每一層可以各司其職,只關心自己這一層的功能,而不去關心上面下面需要做什麼。每一層使用下面提供的接口,同時爲上一層提供服務。每一層可能有多種實現,比如傳輸層(Transport)可以使用TCP
,也可以使用UDP
,它們都可以使用IP
作爲下層網絡層(Network)的實現。他們就像樂高積木一樣, 你可以將不同的積木拼接起來.
報文在層間穿梭
這樣說或許還比較抽象,我們來理一理報文在協議棧中的穿梭流程
以一個標準的使用TCP/IP
的HTTP
報文爲例
它由多個Header
和一個Data
部分組成,這些Header
就是協議棧中各層對在報文上留下的痕跡,越上層Header
越靠近Data
。
發送方向
發送數據時,應用層在原始數據前面貼上HTTP Header
,結果作爲整體下發給傳輸層(TCP
),TCP
纔不管報文的組成,對它來說,現在的報文都將作爲TCP
的Payload
。然後TCP
在現在的報文前面貼上TCP Header
,再作爲payload
下發給下層(IP
). 以此類推,報文前方又貼上了IP Header
和MAC Header
,最終從實際設備發送出去了。
那麼問題來了,爲什麼在上面的過程中,報文走的是傳輸層走的是
TCP
而不是UDP
,應用層走的是IP
而不是其他網絡層實現 ,最後貼的以太頭MAC
是貼的什麼?
答案是,在應用層其實就指定好了。有過一點socket
編程經驗應該知道,創建套接字的的接口是
int socket(int domain, int type, int protocol)
這裏的domain
和protocol
就決定了網絡層用哪個,比如IP
其他。
type
決定了傳輸層使用什麼,比如是TCP
還是UDP
。
由此可見, 向外發送的報文通過傳輸層和網絡層的路徑在socket
創建的時候就已經決定好了.
而MAC Header
中的源MAC
是根據實際發送報文的出接口決定的,這會涉及一些路由相關的東西,比如對於一臺有兩個網卡的電腦, 每個網卡有自己的MAC
地址, 路由過程會根據目的地選擇其中一個網卡作爲報文的實際出接口,那麼MAC Header
中的SMAC
就是該網卡的MAC
地址
至此, 我們就可以描繪出發送過程報文在內核協議棧中的穿梭路徑了
接收方向
再來看接收方向, 當報文由網卡驅動程序遞交給協議棧時, 就要開始它的協議棧旅程了. 和發送過程正好相反, 發送是一層一層貼Header
, 接收是一層一層脫Header
了.
還是上面的例子, 假設現在設備驅動程序收到完整的HTTP
以太幀了, 將報文上送給協議棧, 協議棧最下面的鏈路層(姑且這麼稱呼) 拿到報文了, 那麼他是要將其上送給網絡層 (比如IP
) ? 還是 ARP
或者 RARP
?
回想上面的報文圖, 強調一下: 由於層與層是各司其職的, 所以這裏鏈路層只能看到MAC Header
和後面的Payload of Ethernet Frame
, 而且, 它只能去理解前者, 對於後者, 它根本無法理解. 所以它必須從MAC Header
就能分析出該把這個報文給誰.
顯然, Dest MAC
和Src MAC
是用來描述設備地址的, 那麼答案很明顯了, 只有type
字段。type
字段描述了該以太幀的類型, 比如IP
就是0x0800
, ARP
是0x0806
也就是說,當我解析到報文type
是0x0800
時, 就要將報文脫去MAC Header
後, 扔給IP
.
IP
收到後怎麼辦呢, 在進行自己的處理之後, 它需要把報文上送給上層(傳輸層) . 給傳輸層誰呢, 是TCP
還是UDP
還是其他 ? 一樣的道理, 這個信息就在IP Header
裏, 下面是IPv4
版本的Header
(IPv6
一樣的道理)
看! 關鍵就是protocol
字段, 它指明瞭該使用的傳輸層協議. 比如TCP
是 6 , UDP
是 17. 在我們上面的例子中, 收到報文中這個字段一定是 6, 所以IP
將報文脫掉IP Header
後就會上送給TCP
.
TCP
收到後怎麼辦呢, 首先是自己的處理(這部分內容很多, 但我不打算在本文中談), 最後, TCP
會將報文上送給應用層. 一臺主機上,使用TCP
的應用可能有很多,那麼TCP
如何正確分發呢。 還是一樣,祕密就在TCP Header
中。
在一臺主機上,每個應用程序使用的端口號是不同的,正是通過端口號的唯一性,TCP
就能知道該送給哪個應用層。從這裏可以推斷出, 內核一定是有一張類似表的結構,記錄了使用的端口號和對應應用程序的信息。其實這個信息正是保存在內核的socket
結構裏的。在發送過程中,我們沒有關注TCP Header
中的src port
和dst port
,但其實 這兩個端口號也是保存在內核socket
結構中,TCP
在填寫TCP Header
時, 也正是根據它.
所以, TCP
根據Header
中的dst port
, 會去看哪個socket
結構上的src port
與它一致, 一致就說明應該這個報文脫掉TCP Header
後丟給這個socket
,而應用層程序就可以通過這個socket
讀取到數據了
鬆耦合
內核協議棧層與層之間是鬆耦合的. 比如對於網絡層IP
來說, 它的報文上送代碼一定不是這樣:
ip_local_deliver(skb)
{
protocol = get_protocol(skb)
switch (protocol):
case TCP:
tcp_rcv(skb);
break;
case UDP:
udp_rcv(skb);
break;
......
}
這樣並不是不能工作, 只是如果這樣, 當新添加一種傳輸層協議時, 還要在IP
的代碼裏新加一個case
, 這就不合理了. 所以, 內核採用的辦法是, 各種傳輸層實現一組形式一樣的接口, 註冊到內核中, IP
根據Header
中的protocol
, 一個一個看這個protocol
是否已經註冊了, 找到了就調用註冊的接收函數. 像這樣:
static struct net_protocol tcp_protocol {
.handler = tcp_v4_rcv,
}
static struct net_protocol udp_protocol {
.handler = udp_v4_rcv,
}
{
inet_add_protocol(&tcp_protocol, 6)
inet_add_protocol(&tcp_protocol, 17)
}
ip_local_deliver(skb)
{
protocol = get_protocol(skb) // TCP報文會返回 6 UDP報文會返回 17
transport = get(protocol); // TCP報文會返回 tcp_protocol UDP報文會返回 udp_protocol
transport->handler(skb);
}
這裏的struct net_protocol
就是每種傳輸層實現需要實現的接口. 這種統一的形式(比如上面都實現了handler
函數)使得IP
可以不用管系統中究竟有實現了哪些傳輸層.後續即使添加新的傳輸層實現, IP
的邏輯也不用變. 這就是鬆耦合帶來的好處.
上面只是網絡層<->傳輸層的例子, 實際上, 協議棧的層與層之間都是以這種形式工作的. 這也是內核網絡的設計美學.
總結
- 協議棧劃分了不同的層次,每個層次都有多種實現. 無論是發送還是接收,報文都按順序經過每一層的一種實現
- 發送過程穿梭路徑在套接字創建的時候就已經確定了, 層層貼頭
- 接收過程層層去頭, 頭中的信息指明瞭報文該送到上層哪個實現
- 協議棧層與層之間的鬆耦合體現了協議棧的設計美學