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,1000(0),<mss 1460>
代表: 請求包包含SYN標記, 32位序號為1000, 不包含數據, 帶有一個mss的選項, 其值為1460SYN,8000(0),ACK,1001,<mss 1024>
代表: 請求包包含SYN和ACK標記, 32位序號為8000, 不會包含數據, 32位確認序號為1001, 同樣帶有mss選項
那么接下來我們看TCP協議的交互過程:
-
建立連接
- 客戶端發送包1, SYN代表請求建立連接, 第一個包序號為1000, 該序號的大小由操作系統內核維護, 每次發送都會自增, 自增數值就是發送的字節數, 其中mss選項代表最大段尺寸, 這是為了避免不必要的底層協議的拆包解包;
- 服務器返回包2, 包含的ACK 1001, 代表小於1001序號的包我都收到了, 下次請求發送大於等於1001包; 在該包中同時包含SYN 8000(0), 這段跟客戶端交互的時候一樣, 只是服務器端這頭的序號為8000;
- 客戶端返回包3, 里面只包含ACK 8001的包, 代表收到服務器的建立連接的包了
至此, 連接建立完畢, 可以發送數據了, 該過程包含了客戶端和服務器各一次請求和應答, 服務器的請求和應答放到一個包中做了, 一共包含3次包發送, 所以該過程又被稱為三次握手。
-
交換數據
- 客戶端發送包4, 包含ACK 8001, 以及序號從1001~1020的20個字節的數據
- 服務器返回包5, 包含ACK 1021(因為包含20個字節), 以及序號從8001~8010的10個字節數據
- 客戶端返回包6, 因為數據已經交互完畢, 所以只包含一個ACK 8011
這一段主要是要理解TCP交互的序號管理邏輯, 因為是全雙工協議, 即服務器和客戶端可以同時像對方發送數據, 所以需要客戶端和服務器各維護一個序列號。如果是半雙工協議的話, 就只需要一方維護一個序號即可。
-
關閉連接
- 客戶端發送包7, 包含FIN標記, 1021
- 服務器返回包8, 只是應答ACK 1022
- 服務器再次返回包9, 包含FIN標記, 8011序列
- 客戶端返回包10, 包含ACK 8012
在建立連接的時候, 服務器的請求和應答是合並到了一個包當中。但是在關閉連接的過程中, 就必須分開兩個包來, 因為客戶端關閉連接之后就不能再發送數據了, 但是服務器還可以發送數據給客戶端, 直到服務器也發送FIN標記。
滑動窗口
如上講的都是一來一回的交互, 一般情況下可能會存在一方數據發得特別快, 另一方數據發得特別慢, 這種時候如果不做控制, 勢必會讓慢的這方數據處理不過來從而導致丟包。
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協議並不保證數據的可靠性, 他一般用於一些高性能的場景, 且需要應用層再做一些簡單的封裝處理。