TCP/IP網絡協定棧
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiInBnauQGafhTOjdDZ5UmMxYDNiRjM1EWMlBTNwQTMzYWZkFWMjVzYtIjdvwFM48CXt92YucWbphmeuEzYpB3Lc9CX6MHc0RHaiojIsJye.jpg)
TCP/IP網絡協定棧分為四層, 從下至上依次是:
-
鍊路層
其實在鍊路層下面還有實體層, 指的是電信号的傳輸方式, 比如常見的雙絞線網線, 光纖, 以及早期的同軸電纜等, 實體層的設計決定了電信号傳輸的帶寬, 速率, 傳輸距離, 抗幹擾性等等。
在鍊路層本身, 主要負責将資料跟實體層互動, 常見工作包括網卡裝置的驅動, 幀同步(檢測什麼信号算是一個新幀), 沖突檢測(如果有沖突就自動重發), 資料差錯校驗等工作。
鍊路層常見的有
,以太網
的标準。令牌環網
-
網絡層
網絡層的IP協定是構成Internet的基礎。該層次負責将資料發送到對應的目标位址, 網絡中有大量的路由器來負責做這個事情, 路由器往往會拆掉鍊路層和網絡層對應的資料頭部并重新封裝。IP層不負責資料傳輸的可靠性, 傳輸的過程中資料可能會丢失, 需要由上層協定來保證這個事情。
-
傳輸層
網絡層負責的是點到點的協定, 即隻到某台主機, 傳輸層要負責端到端的協定, 即要到達某個程序。
典型的協定有TCP/UDP兩種協定, 其中TCP協定是一種面向連接配接的, 穩定可靠的協定, 會負責做資料的檢測, 分拆和重新按照順序組裝, 自動重發等。而UDP就隻負責将資料送到對應程序, 幾乎沒有任何邏輯, 也就是說需要應用層自己來保證資料傳輸的可靠性。
-
應用層
即我們常見的HTTP, FTP協定等。
這四層協定對應的資料包封裝如下圖:
四層協定對應的通信過程如下圖:
鍊路層 以太網資料幀
以太網資料幀格式如下:
說明如下:
- 目的位址和源位址是指網卡的硬體位址(即MAC位址), 長度是48位, 出廠的時候固化的。
- 類型字段即上層協定類型, 目前有三種值: IP, ARP, RARP。
- 資料對應了上層協定傳輸的資料, 以太網規定資料大小是46~1500位元組, 最大值1500即以太網的最大傳輸單元(MTU), 不同網絡類型有不同MTU, 如果需要跨不同類型鍊路傳輸的話, 就需要對資料進行重新分片。
- CRC是資料的校驗碼, 確定資料傳輸正确
ARP協定
在網絡通信過程中, 源主機的應用程式隻知道目的應用程式的IP位址, 并不知道對方主機的硬體位址, 是以在資料發送之前, 需要先找到目标及其的硬體位址, 這就是ARP協定所起的作用了。
每次在建立連接配接之前, 會在本地網絡廣播發送目的IP位址, 所有機器都會受到該請求, 目的機器發現該請求中的IP位址跟自己一樣, 就把自己的硬體位址傳回回去, 否則忽略該請求。
一般來說, 每台機器都維護的有一個ARP快取記錄, 存儲了近期的IP位址和硬體位址的映射關系, 可以用
arp -a
指令來檢視緩存表中内容。
如果目的機器和本機器不在同一個網段之内的話, 會将資料發送給網關來處理, 一般網關就是路由器, 此時網關會進行IP路由, 将ARP請求發送到目的網絡位址, 然後再依次将應答傳回給該發起請求的機器。
IP協定
IP協定資料包格式如下:
幾個字段解釋如下:
- TOS, 一共有8位, 其中3位用來表示該資料包的優先級, 目前已經不用; 還有4位表示可選的服務類型(最小延遲, 最大吞吐, 最大可靠性, 最低成本), 還有一位總是0;
- 标志位: 用來對每個IP包的分片關系進行辨別, 用于分片和重新組裝資料包;
- TTL(Time To Live), 是指一個資料包在網絡上的最多經過多少次轉發, 如果超過該數字, 就丢棄該包
- 8位協定, 上層可選協定為: TCP, UDP, ICMP, IGMP
IP位址的一共分為如下幾類:
在網際網路剛出來的時候, 大部分組織都申請的B類網絡位址, 導緻B類位址很快就用完了, 但是A類又有很多空閑的位址, 而每個路由器又必須掌握所有網絡的資訊, 随着C類網絡的增多, 路由器中的路由表項數也就越來越多了。
針對這種情況, 後來人們發現, 絕大部分内部網絡的機器都不需要一個獨立的公網IP的, 這些機器通過一個公網IP跟外部連接配接, 在自己的網絡内部為每台機器申請一個私有IP, 内部再建設一個路由器, 做内網IP位址的定位即可。
私有IP的出現大大解決了IP浪費的問題, 是以我們日常中可以看到很多如192.168.xx這樣的IP, 這些IP都隻是區域網路内部IP, 不會浪費IP位址。
于是, RFC1918就規定了組建區域網路的私有IP位址規範:
- 10.*, 前面8為是網絡号, 共16,777,216個私有IP
- 172.16.*到172.31.*, 共1,048,576個私有IP
- 192.168.*, 共65536個私有IP
這些私有IP位址雖然沒有公網IP, 但是仍然可以通過NAT等技術來跟公網進行連接配接互動。
除了私有IP之外, 還有幾種特殊的IP位址:
- 127.*的IP位址用于本機環回測試, 這類位址的互動資料不會過網卡, 直接在核心過一遍協定就完成互動了
- 255.255.255.255, 這是個特殊IP, 代表在本地路由廣播
- 主機号部分全是0的位址代表一個網絡, 而不能代表某個主機(比如不能用192.168.0.0作為某台機器的IP)
- 主機号部分全是1的位址代表在該網絡内部廣播
TCP協定
資料包格式
TCP協定資料包如下:
部分字段解釋如下:
- 源端口号和目的端口号: 用來标注資料互動雙方程序
- 32位序号和32位确認序号: TCP是一個可靠的互動協定, 這兩個序号用做傳輸過程中資料的标記, 保證資料的傳輸順序以及重發
- URG/ACK/PSH/RST/SYN/FIN: 用來标記該請求包位于TCP連接配接中的什麼階段, 這6個字段下面會詳細解釋
互動過程
上圖中每次連接配接線上的數字标記了此次資料包中的關鍵資訊, 比如
-
代表: 請求包包含SYN标記, 32位序号為1000, 不包含資料, 帶有一個mss的選項, 其值為1460SYN,1000(0),<mss 1460>
-
代表: 請求包包含SYN和ACK标記, 32位序号為8000, 不會包含資料, 32位确認序号為1001, 同樣帶有mss選項SYN,8000(0),ACK,1001,<mss 1024>
那麼接下來我們看TCP協定的互動過程:
- 建立連接配接
- 用戶端發送包1, SYN代表請求建立連接配接, 第一個包序号為1000, 該序号的大小由作業系統核心維護, 每次發送都會自增, 自增數值就是發送的位元組數, 其中mss選項代表最大段尺寸, 這是為了避免不必要的底層協定的拆包解包;
- 伺服器傳回包2, 包含的ACK 1001, 代表小于1001序号的包我都收到了, 下次請求發送大于等于1001包; 在該包中同時包含SYN 8000(0), 這段跟用戶端互動的時候一樣, 隻是伺服器端這頭的序号為8000;
- 用戶端傳回包3, 裡面隻包含ACK 8001的包, 代表收到伺服器的建立連接配接的包了
- 交換資料
- 用戶端發送包4, 包含ACK 8001, 以及序号從1001~1020的20個位元組的資料
- 伺服器傳回包5, 包含ACK 1021(因為包含20個位元組), 以及序号從8001~8010的10個位元組資料
- 用戶端傳回包6, 因為資料已經互動完畢, 是以隻包含一個ACK 8011
- 關閉連接配接
- 用戶端發送包7, 包含FIN标記, 1021
- 伺服器傳回包8, 隻是應答ACK 1022
- 伺服器再次傳回包9, 包含FIN标記, 8011序列
- 用戶端傳回包10, 包含ACK 8012
滑動視窗
如上講的都是一來一回的互動, 一般情況下可能會存在一方資料發得特别快, 另一方資料發得特别慢, 這種時候如果不做控制, 勢必會讓慢的這方資料處理不過來進而導緻丢包。
TCP協定中采用了
滑動視窗協定
來解決該問題, 類似上面的
mss
, 再增加一個新的選項
win
, 告訴對方自己的滑動視窗大小, 對方在發送資料的時候每次發送資料就知道對方到底視窗空間還夠不夠, 如果不夠了就不發了, 進而解決了一快一慢這種問題。
連接配接狀态
如下圖:
其他狀态都還好, 在工作中常會碰到TIME_WAIT連接配接過多的問題, 這裡把TIME_WAIT狀态單獨拿出來說一下。
TIME_WAIT是主動關閉方在收到被動關閉方發的FIN包之後處于的狀态, 這個包是主動關閉方收到的最後一個包了, 在收到這個包之後還不能直接就把連接配接給關閉了, 還得等待一段時間才能關閉, 等待時間為2MSL。
為什麼要等待一段時間呢? 主要是兩個原因:
- 在收到最後一個包之後主動關閉方還得發一個ACK回去, 這個ACK可能會丢包, 如果丢包, 對方還需要重新發最後一個FIN包, 如果收到重新發過來的FIN包的時候這邊廂連結已經關閉, 則會導緻連結異常終止;
- 不過第1點也不會造成太大的問題, 畢竟資料已經正常互動了。但是有另外一點風險更高, 就是如果不等待2MSL的話, 那麼如果正好一個新連結又建立在相同的端口上, 那麼上次的FIN包可能因為網絡原因而延時迷途的包這個時候才送達該端口, 導緻下一次連接配接出現問題;
是以一定要有一個TIME_WAIT的狀态等待一段時間, 等待的MSL時間RFC上面建議是2分鐘, 但是筆者實際工作中測試往往是30秒。
但是如果你的服務是一個高并發短連接配接服務, TIME_WAIT可能會導緻連接配接句柄被大量占用, 而你又相信服務内部是一個非常穩定的網絡服務, 或者即使有兩個連接配接互動出現故障也可以接受或者有應用層處理, 不希望有那麼多的TIME_WAIT狀态的連接配接, 一般有兩種方式:
- 在建立連接配接的時候使用SO_REUSEADDR選項
- 在
/etc/sysctl.conf
中加入如下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
然後執行
生效參數。/sbin/sysctl -p
UDP協定
UDP協定就簡單很多了, 基本上就隻包含源位址, 目的位址, 長度, 校驗, 資料。
互動過程也不再像TCP這樣經過很複雜的建立連接配接和關閉連接配接的過程了, 就直接每次都發送資料了, 這樣會有如下的一些問題:
- 發送端隻管發送資料, 如果在茫茫路由中該包丢了, 接收端并不知道
- 發送的多個包中, 在經過不同路由的時候, 可能達到時序跟發送的時候并不一樣, 是以接收端可能拿到的是不同順序的包
- 如果發送端很快, 而接收端很慢, 接收端處理不過來, 就會丢包
是以如前面所說, UDP協定并不保證資料的可靠性, 他一般用于一些高性能的場景, 且需要應用層再做一些簡單的封裝處理。