使用者態協議棧分析
一、前言
在講網路協議棧前,先理解一個數據包在網路傳輸是一個怎麼樣的流程,如下圖所示。
正常的流程是網絡卡接收到資料後,把資料copy到協議棧(sk_buff),協議棧把sk_buff資料解析完後再把資料放到recv_buff,此時應用程式呼叫recv把資料從協議棧copy到應用程式;傳送資料包,則與之相反,應用程式呼叫send把資料包copy到send_buff,協議棧從send_buff取資料放到sk_buff,交給網絡卡傳送出去。這個過程有多次拷貝,為避免多次拷貝,使用dma的方式(零拷貝),把網絡卡的資料直接對映到記憶體,再由應用程式訪問記憶體。
二、資料包分析
從網絡卡接收到一幀完整的資料包,可以使用原生的socket、netmap、dpdk等,完整的一幀資料由乙太網頭、IP頭、tcp/udp頭、使用者資料構成,這些層級涉及到7層網路模型OSI,如udp協議分佈到7層OSI如下圖所示。
由上圖可知,乙太網頭屬於鏈路層、IP頭屬於網路層、UDP頭屬於傳輸層,而實際的使用者資料在應用層。
(1)乙太網頭
乙太網頭分佈如下圖所示。
對應結構體如下,由此可知MAC地址存在乙太網頭。
#define ETH_LEN 6 //14位元組乙太網頭---->鏈路層---->MAC地址, struct ethhdr{ unsigned char dst[ETH_LEN];//6位元組 目的地址即MAC地址 unsigned char src[ETH_LEN];//6位元組 源地址 unsigned short proto;//2位元組 協議型別,形容網路層使用的協議} //在計算上沒有那個韌體叫MAC地址,IP地址、埠 //所謂的MAC地址,IP地址、埠只不過是協議棧裡面一個欄位名而已,不要與韌體捆綁
(2)IP頭
IP頭結構如下圖所示。
其資料結構如下所示,IP地址在IP頭中,屬於網路層。
//iphdr(ip頭)---->網路層--->IP地址 struct iphdr{ unsigned char version:4,//4位版本 hdrlen:4;//4位首部長度 unsigned char tos;//8位服務型別 unsigned short totlen;//16位總長度,有65535也就是說一次可傳64k,注意MTU是1500這是網絡卡的限制,在網絡卡傳輸資料是它會分片傳送,一個片就是一個MTUunsigned short id;//16位標識,每一個數據包都有一個id,與tcp裡面的seq num沒有關係 unsigned short flag:3, offset:13; unsigned char ttl;// ttl = 64 - 路由數量/閘道器,當ttl為0,就會返回無法訪問目標地址,不可達 unsigned char proto;//8位協議型別,形容傳輸層使用什麼協議 unsigned short check;//16位首部校驗和,計算的是首部的校驗和,計算校驗前一定要賦值為0,再計算,否則接收端無法收到資料 unsigned int sip;//源ip unsigned int dip;//目的ip }
(3)協議頭
該層涉及到具體的不同協議,就有不同的結構,本文主要分析udp和tcp
(a)udp 協議結構如圖所示。
其資料結構如下,由此可知埠在協議頭裡面,屬於傳輸層。
//udp頭(8個位元組頭)---->傳輸層--->埠 struct udphdr{ unsigned short sport;//源埠 unsigned short dport;//目的埠 unsigned short length;//長度 unsigned short check;//校驗和 }
(b) tcp 協議頭如圖所示。
其資料結構如下,屬於傳輸層。
struct tcphdr{ unsigned short sport;//源埠 unsigned short dport;//目的埠 unsigned int seqnum;//序號:包的序號,唯一id,隨機起始值,之後就遞增 unsigned int acknum; unsigned char hdrlen:4,//頭長度 resv:4;//保留位 //以下的標識置1,對應的欄位有效 unsigned char cwr:1, ece:1, urg:1, ack:1, psh:1, rst:1, syn:1, fin:1; unsigned short win;//視窗大小 unsigned short check; unsigned short urg_pointer; }
(c)arp和icmp
arp和icmp頭定義如下。
//arp_head struct arphdr { unsigned short h_type; unsigned short h_proto; unsigned char h_addrlen; unsigned char protolen; unsigned short oper; unsigned char smac[ETH_ALEN];//源mac unsigned int sip;//源ip unsigned char dmac[ETH_ALEN];//目的mac unsigned int dip;//目的ip }; //icmp_head struct icmphdr { unsigned char type; unsigned char code; unsigned short check; unsigned short identifier; unsigned short seq; unsigned char data[32]; };
arp是地址解析協議,在區域網中,每一臺主機都會對區域網內每一臺機器進行廣播arp包,當收到對端主機arp請求包後,把本機的IP和MAC地址做為響應傳送回請求方,發出請求的主機便可獲得整個區域網內所有主機的IP和MAC地址,並儲存到arp表中,記錄著區域網所有機器的IP和MAC地址資訊;當arp表中某臺機器的arp資訊超時後,就會從arp表中刪除,導致收不到資料;所以在區域網內網路通訊看似是通過IP,其實是通過MAC地址。
ICMP是Internet控制報文協議,在命令列上ping + IP地址,此時傳送的就是向目標主機發送ICMP請求,目標主機收到ICMP求情後,就會響應ICMP,表明兩臺主機的網路是暢通的。
由以上每個協議頭的定義,得到udp、tcp、arp、icmp它們的協議packet可定義如下。
struct arppkt { struct ethhdr eh; struct arphdr arp;//arp頭屬於網路層,與IP頭同一層 }; struct icmppkt { struct ethhdr eh; struct iphdr ip; struct icmphdr icmp; }; struct udppkt { struct ethhdr eh;//14 struct iphdr ip;//20 struct udphdr udp;//8 //使用者資料,柔性陣列相當於一個標籤,指向使用者資料的首地址 unsigned char payload[0];//柔性陣列,使用條件:1.記憶體已經分配好,2.柔性陣列的長度可以通過其它方法計算出來 }; //sizeof(udppkt) = 44,為啥不是42,因為結構體設定了是以1個位元組對齊,導致有一個地方有2個位元組的空窗期 struct tcppkt { struct ethhdr eh; struct iphdr ip; struct tcphdr tcp; unsigned char payload[0]; };
三、深入理解網路協議棧
從網路協議棧是如何實現tcp連線、傳輸資料、斷開連線,經過這3個方面加深對網路協議棧瞭解。
(1)三次握手
tcp三次握手流程圖如下。
客戶端傳送syn包開始第一次握手,服務端收到syn後完成第一次握手;服務端傳送ack包開始第二次握手,acknum等於第一次握手seqnum+1,客戶端收到ack後完成第二次握手,客戶端傳送ack開始第三次握手,acknum等於第二次握手seqnum+1,服務端收到ack後完成第三次握手。
第一次握手完成時,服務端從IP頭、TCP頭獲取到源IP、目的IP、源埠、目的埠、協議等資訊構成五元組,存到半連線佇列節點中;當第三次握手完成,遍歷半連線佇列找到對應的節點,並把節點移動到全連線佇列中,應用層呼叫accept消費全連線佇列資料,並分配fd,全連線佇列每個節點可以叫tcp控制塊,fd與tcp控制塊一一對應。
在三次握手過程中存在3個狀態(狀態機):
(a)listen:伺服器處於listen狀態;
(b)syn_recv:伺服器接收到資料包之後進入syn_recv狀態;
(c)established:在接收完資料後進入established狀態。
以上3個狀態存在tcp控制塊中。
應用層呼叫listen(fd, backlog),引數backlog有兩種理解,在linux系統中指的是半連線佇列的長度,在unix系統中指半連線佇列和全連線佇列大小之和。
如果第三次握手ack包丟失,那麼第二次握手會不會重發ack包,答案是不會重發,沒有重發得意義,但是包的超時可以設定。這就引出了超時怎麼計算,客戶端發包到服務端,服務端發包到客戶端,客戶端發包開始記錄一個時間,到客戶端收到包時也記錄一個時間,服務端也類似發包記錄一個時間到收包也記錄一個時間,這個往返的時間叫做RTT,當前RTT往返時間 = 上一次RTT*0.9 + 下一次RTT*0.1。
(2)資料傳輸
tcp傳輸並不是發一個包回一個ack再發下一個包,這樣速度很慢,實際是多個包一起發,再等待ack確認。這樣導致不能保證先發的資料就是先到,後發的資料後到,tcp為了保證順序,引入了超時重傳的機制。收到一個包,啟動一個200ms定時器,等待接收到下一個包,如果在200ms內收到,就會重置定時器等待接收下一個包,如果200ms沒收到就會超時,超時後,就會遍歷那個包沒有收到,並回一個ack確認訊息告知傳送端那沒有收到,讓傳送端從該資料包開始,包括之後的資料包都要重新發。
慢啟動,擁塞控制如下圖所示。
一開始數量是指數級增長(慢啟動),到達初始化的閾值後線性增長,增長到對方接收資料時,回ack的包超過RTT時間,這時網路擁塞,資料包太多來不及處理,此時降一半。
tcp重要的定時器有:
(a)超時重傳
(b)堅持定時器,當cwin=0時,接收端告訴傳送端不能再發資料了,如果客戶端想再發送資料,就會啟動一個堅持定時器,發一個探測包給接收端,告訴對端你能不能接收資料,接收端 recv_buff不滿時,就會回ack告知傳送端可以發資料了。
(c)keepalive
(d)time_wait,4次揮手中避免最後一次ack丟失
(3)四次揮手
四次揮手的流程如下圖所示。
(a) 客戶端呼叫close(fd)傳送fin包,服務端收到fin包後,回ack包確認;
(b) 服務端在處理完緩衝區的資料後,呼叫close(fd)關閉對應的fd,傳送fin包;
(c) 客戶端收到fin包後,回ack包確認,等待2msl(2個數據包傳送週期)後釋放連線;
(d)服務端收到ack包後,釋放連線。
思考:
(1)服務端大量出現close_wait如何解?
原因:
服務端recv()返回0後,處理資料不及時,導致close呼叫不及時。
解決思路:
(a)檢查程式碼有沒有呼叫close;
(b)把處理資料做成非同步處理,即拋到執行緒非同步處理;
(2)客戶端出現大量的fin_wait_2如何解?
原因:
服務端recv返回0後,不呼叫close,客戶端就會出現fin_wait_2
解決思路:
(a)服務端處理;
(b)客戶端 kill;
(c)重新建個連線。
(3)客戶端出現大量的time_wait?
原因是客戶端傳送的最後一次ack包,服務端沒有收到,超時後服務端重發fin包,導致客戶端出現大量的time_wait。