一次網絡請求是要經過很多層的,如底層的物理層,再上面的鏈路層、網絡層、傳輸層以及應用層。當然我們一般工作是針對應用層,但是也需要對傳輸層有很深刻的了解,傳輸層個人感覺也是最復雜的。下圖是TCP/IP協議分層圖,注意,雖然ARP和RARP協議都划分在鏈路層,實際上IP、ARP和RARP數據報都需要以太網驅動程序來封裝成幀;同樣的,ICMP和IGMP協議雖然划分在網絡層,實際它們都需要IP協議來封裝成數據報。在看這篇文章前推薦先看《TCP/IP協議之初識》。


圖1中沒有畫出的物理層,指的是電信號的傳遞方式,比如現在以太網通用的網線(雙絞線)、早期以太網采用的的同軸電纜(現在主要用於有線電視)、光纖等都屬於物理層的概念。物理層的能力決定了最大傳輸速率、傳輸距離、抗干擾性等。簡單來說物理層都是硬件部分。
鏈路層有以太網、令牌環網等標准,鏈路層負責網卡設備的驅動、幀同步、沖突檢測、數據差錯校驗等工作,現在我們平時接觸到的基本是以太網。交換機是工作在鏈路層的網絡設備,可以在不同的鏈路層網絡之間轉發數據幀(比如十兆以太網和百兆以太網之間、以太網和令牌環網之間),由於不同鏈路層的幀格式可能不同,交換機要將進來的數據包拆掉鏈路層首部重新封裝之后再轉發。
網絡層則主要是經常提及的IP協議,IP協議不保證數據傳輸的可靠性,數據包在傳輸過程中可能丟失,可靠性可以在上層協議或應用程序中提供支持。路由器是工作在第三層的網絡設備,同時兼有交換機的功能,可以在不同的鏈路層接口之間轉發數據包,因此路由器需要將進來的數據包拆掉網絡層和鏈路層兩層首部並重新封裝。
傳輸層則是TCP和UDP協議,TCP協議保證數據收發的可靠性,丟失的數據包自動重發,上層應用程序收到的總是可靠的數據流。UDP協議不面向連接,也不保證可靠性。
應用層則是我們自己的應用程序。而在應用程序里面發送的數據,在網絡上則不僅僅是那些數據本身,還有各個協議頭部,數據包的封裝過程如圖二所示。各層協議頭部和內容在接下來會通過一個例子來分析。

二 基於TCP協議的編程
1 以太網幀和ARP協議
從圖2可以看到,數據最終都是封裝成以太網幀在網絡中傳輸。以太網幀的格式如圖3所示:

在TCP編程中,兩端通信之前,需要先通過ARP(地址解析協議)協議來確定指定IP的機器的物理地址,也就是它的網卡的硬件地址(MAC),MAC地址長度為48位,在網卡出廠的時候固化在網卡里面的,可以通過命令 ifconfig
來查看網卡地址。ARP協議的數據報格式如下:

比如當我們運行命令 ping 192.168.1.100
,那么需要先arp協議獲取192.168.1.100
這個ip對應的MAC地址,下面是一個ARP協議的請求和響應包。通常操作系統會有ARP緩存,所以一次請求后,只要緩存沒有過期,下次就可以從緩存中取而不需要發送ARP請求來獲取目的IP地址的MAC地址了。
請求包

響應包

對照前面貼出來的以太網幀格式,很容易分析這兩個數據包。如以太網幀的頭部包含的14個字節,分別是目的地址,源地址以及協議類型。最初因為不知道目的ip地址的mac地址,所以目的地址填的是ff:ff:ff:ff:ff:ff
進行廣播,協議類型是0x0806。而ARP協議的內容可以參照上圖,分布是硬件類型(以太網,標志1),協議類型為IPV4(0x0800),硬件地址長度(6個字節),協議地址長度(IPV4,4個字節),操作碼(ARP請求類型為1,ARP響應類型為2),發送方的MAC地址(本機mac地址),發送方ip地址(請求包里是192.168.1.106,響應包是192.168.1.100),目標MAC地址(請求時不知道目標MAC地址,所以填全0,響應包會填寫目標MAC地址),目標IP地址(請求包是192.168.1.100,響應包是192.168.1.106)。
2 Socket編程
接下來要開始socket編程了,先寫一個簡單的客戶端-服務端。
#服務端:server.py import socket def start_server(ip, port): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((ip, port)) sock.listen(1) while True: conn, cliaddr = sock.accept() print 'server connect from: ', cliaddr while True: data = conn.recv(1024) if not data: print 'client closed:', cliaddr break conn.send(data.upper()) conn.close() except Exception, ex: print 'exception occured:', ex finally: sock.close() if __name__ == "__main__": start_server('127.0.0.1', 7777) #客戶端:client.py from socket import * import sys def start_client(ip, port): try: sock = socket(AF_INET, SOCK_STREAM, 0) sock.connect((ip, port)) print 'connected' while True: data = sys.stdin.readline().strip() print 'input data:', data if not data: break sock.send(data) result = sock.recv(1024) if not result: print 'other side has closed' else: print 'response from server:%s' % result sock.close() except Exception, ex: print ex if __name__ == "__main__": start_client('127.0.0.1', 7777)
TCP的通信流程如下圖所示,可以看到前面建立連接有三次握手,后面關閉連接有四次握手。注意圖中是C語言的函數,對應到python的里面發送用的是send函數,讀取用的是recv函數。要注意的是,一個TCP連接的套接字對是一個四元組,包括源IP,源端口,目的IP,目的端口。客戶端和服務端的狀態並不是同步的,如果客戶端的ACK發送失敗,可能客戶端的連接是ESTABLISHED,而服務端對應連接還是SYN_RCVD狀態。

SYN表示建立連接,
FIN表示關閉連接,
ACK表示響應,
PSH表示有 DATA數據傳輸,
RST表示連接重置。
其中,ACK是可能與SYN,FIN等同時使用的,比如SYN和ACK可能同時為1,它表示的就是建立連接之后的響應,如果只是單個的一個SYN,它表示的只是建立連接。TCP的幾次握手就是通過這樣的ACK表現出來的。 但SYN與FIN是不會同時為1的,因為前者表示的是建立連接,而后者表示的是斷開連接。RST一般是在FIN之后才會出現為1的情況,表示的是連接重置。一般地,當出現FIN包或RST包時,我們便認為客戶端與服務器端斷開了連接;而當出現SYN和SYN+ACK包時,我們認為客戶端與服務器建立了一個連接。PSH為1的情況,一般只出現在 DATA內容不為0的包中,也就是說PSH為1表示的是有真正的TCP數據包內容被傳遞。TCP的連接建立和連接關閉,都是通過請求-響應的模式完成的。
三 TCP通信流程實例分析
接下來要做的就是通過wireshark來觀察這個流程。先在一個終端運行 python server.py
,然后在第二個終端運行python client.py
,這個時候我們看到第二個終端輸出connected
,表示連接上了。wireshark輸出如下:

從圖中可以看到三次握手的過程,這里我們拿出第一個SYN包來分析下數據包的格式,以太網幀的格式上面我們已經提過了,前面12個字節是目的地址和源地址,因為是本地地址,所以這都是00,然后兩個字節幀類型是0800,即IP協議。后面就是IP協議棧和TCP協議棧的內容。
先看IP協議棧的格式如圖7所示,數據包如圖8所示,第一個字節0x45中,前4位為版本IPV4,接着4位5為首部長度,代表的是4*5=20個字節,這是指的整個IP數據包的長度。第二個字節0x00為服務類型TOS,有3個位用來指定IP數據報的優先級,現在幾乎不用。然后的16位0x003c為IP包的總長度60(首部20字節+數據40字節),可以看到,從IP數據包的第一個字節45開始到最后一共是60個字節。緊接着的16位0xe963為標識,如果IP包大小超過了MTU,則需要進行拆分,這個標識字段就是用於標識哪些包分拆前是同一組的。接着的16位0x4000,前3位為標志位,其中最高位保留為0,第二位為DF(don't fragment)位為1,也就是不分片,第三位為MF(more fragments,更多分片)位為0,因為我們這里沒有分片; 接着的13位是片的偏移,這里沒有分片,所以為0。接下來的8位0x40為TTL,值為64(TTL在traceroute時就很有用,TTL是這樣用的:源主機為數據包設定一個生存時間,比如64,每過一個路由器就把該值減1,如果減到0就表示路由已經太長了仍然找不到目的主機的網絡,就丟棄該包,因此這個生存時間的單位不是秒,而是跳(hop)),再8位是協議字段,指示上層協議是TCP,UDP還是ICMP,IGMP等。我們這里是TCP,所以值為0x06。然后16位0x5356是首部校驗和,只校驗IP首部,數據的校驗由更高層協議負責。然后的32位7f000001是源IP地址127.0.0.1,而接着的32位是目的IP地址127.0.0.1,選項為空,然后接下來的是TCP協議棧內容。
1 三次握手數據包解析


TCP段格式和數據段實例如圖9,10所示。最開始16位0xdbb8為源端口56280,然后的16位0x1e61為目的端口7777。接着是32位序號0x2d4a6c26即759852070,注意我們看到wireshark中為了顯示友好,顯示的值為0,那是相對序號(可以在右鍵的Protocol Preference中取消相對序號選項就可以看到絕對序號了)。接着是32位的確認序號0x00000000。接着的16位中的前4位是首部長度0xa,也就是4*10=40
個字節。我們可以看到這里的TCP段正好是40個字節,也就是說沒有數據部分,只有首部。接着的6位是保留位,這16位的最后6位是6個標志位0x002,分布是URG,ACK,PSH,RST,SYN,FIN,其中URG先不管,ACK是確認標志,PSH是盡快推送數據到接收進程標志,RST是復位連接標志,SYN是同步序號標志,FIN是完成數據發送標志。我們這里看到只有SYN標志置位為1,表示是同步序號。而后16位0xaaaa,表示窗口大小為43690。再接着就是16位校驗和0xfe30,然后是16為緊急指針為0x0000,接着是選項字段。
TCP選項字段格式分為3部分,kind為選項的類型,length為該選項的總長度(這個總長度包括了kind和length這兩個字節在內),info為選項的值,下表是常見的選項值和含義,更多選項值參見參考資料2。
kind (1字節) | length (1字節) | info (n字節) | 含義 |
---|---|---|---|
0 | 空 | 空 | 表示選項表結束 |
1 | 空 | 空 | 空操作nop,一般用於填充tcp選項的總長度為4的倍數 |
2 | 4 | MSS | 最大段長度 |
3 | 3 | window scale | 滑動窗口擴大因子 |
4 | 2 | SACK | 選擇性確認 |
8 | 10 | timestamp | 時間戳值4字節+時間戳回顯應答4字節 |
可以看到選項字段先是 0x02 04 ff d7表示是MSS,長度為4字節,值為65495。MSS通常等於MTU-20-20,而MTU一般設置為1500,所以一般MSS為1460,當然,考慮到TCP的選項值可能會占據最多20個字節,所以MSS也可能是1460-12-8=1440。而通常的MTU為1500,lo特殊,為65536,所以這里的MSS不是1440,而是一個比較大的值0xffd7=65495。MTU是服務器可配置的,在TCP通信過程中客戶端和服務器端會協商最小的MSS作為最終值。TCP有了MSS限制,就可以保證在IP層不用分片了。再接着是0x0402是選擇性確認選項,類別為4,總長度為2字節,這里表示沒有info。接着的0x08 0a ff ff 85 45 00 00 00 00是時間戳,類別是8,總長度為10,內容為時間戳0xffff8545=4294935877,時間戳回顯值為0x0000000=0。然后是1字節的0x01,類別為1,表示空操作,用於填充的。接着是0x03 03 07。類別為3,長度為3,值為7,表示窗口擴大因子為7。


那么第二階段SYN+ACK和第三階段的ACK的數據包類似,分別如下所示,SYN+ACK中的標記是SYN和ACK置位,然后時間戳回顯為當前的時間;第三階段ACK數據包是ACK標記置位和時間戳回顯。


2 發送數據的數據包解析
下面看看發送數據的包,我們在client.py的終端輸入 haha,可以看到捕獲到4個數據包,第一個包是客戶端發往服務端的,PSH和ACK標志置位,同時數據為haha;第二個包是服務端發往客戶端的,ACK標志置位;第三個包還是服務端發往客戶端的,PSH和ACK標志置位,這是服務端發送的內容為HAHA;第四個包是客戶端發往服務端的,ACK標志置位,確認收到了數據。需要注意的是,客戶端和服務端各自維護了一個序號,這是因為TCP是全雙工通信。如果某種面向連接的協議是半雙工的,通訊過程只能采用一問一答的方式,收和發兩個方向不能同時傳輸,在同一時間只允許一個方向的數據傳輸,則只需要一套序號就夠了,不需要通訊雙方各自維護一套序號。服務端發往客戶端的ACK是5,這是因為收到的數據長度為4,所以新的請求序號為5(注意都是相對序號),同理看到后面客戶端發送數據的序號是5了,序號跟數據長度是相關的。

3 關閉連接四次握手數據包解析
關閉TCP連接時,會有四次握手,主動關閉的一方會處於TIME_WAIT狀態一段時間再徹底關閉。TIME_WAIT的時間是系統配置參數tcp_fin_timeout設定的,Linux里面一般是60秒,當然我們也可以調短它,關於為什么要有TIME_WAIT的狀態,主要原因有兩點:其一是為了可靠的實現TCP全雙工連接的終止,其二是為了允許老的重復分節在網絡中消逝。第一點,假設最后客戶端發送的ACK服務端沒有收到,則服務端會重發FIN,這個時候處於TIME_WAIT狀態的客戶端連接還可以重發ACK。否則,如果連接已經關閉,則客戶端會發送RST的一個分節,這樣服務端會解析為一個錯誤,這個不是我們想要的。第二點,如果沒有TIME_WAIT,那么可能在連接關閉后,新建立一個連接,IP地址和端口跟之前的一樣,TCP必須防止老的重復分組或者延遲的分組在該連接后終止后再現,不然無法區分分組來自老的連接,還是新的連接的,如下圖所示。當然這個情況很難出現,因為新的連接和老的連接必須是兩邊的IP地址和端口都一樣,而且老的分組的ISN編號也要有效。通常,新的連接會采用一個隨機的端口號,很難這么湊巧跟之前一樣,而要之前老的分組的序列號ISN也幾乎不可能有效。因此,TCP禁止處於TIME_WAIT狀態的端口發起新的連接,在TIME_WAIT時間過后,建立新的連接,基本可以保證該連接以前的老的化身的重復分組已經消逝。

在第二個終端,CTRL+C關閉client.py,可以看到wireshark捕獲到的數據包如下,這里看到服務端的FIN和ACK合並在了一個數據包里。而且可以看到客戶端連接處於TIME_WAIT狀態,過一段時間后才關閉。


四 總結
這是TCP/IP協議的第一篇,總結下各個協議的格式和數據包的分析,第二篇會重點分析下諸如SO_REUSEADDR, backlog
等參數的意義,特別是backlog參數,是查了好多資料才明白一二。