網路編程TCP/IP詳解
網路編程TCP/IP詳解
1. 網路通訊
中繼器:訊號放大器
集線器(hub):是中繼器的一種形式,區別在於集線器能夠提供多埠服務,多口中繼器,每個數據包的發送都是以廣播的形式進行的,容易阻塞網路。
網橋:區域網之間建立連接的橋樑,網橋是一種對幀進行轉發的技術,根據MAC分區塊,可隔離碰撞。網橋將網路的多個網段在數據鏈路層連接起來。
交換機(switch):工作在數據鏈路層,交換機與網橋的細微差別在於交換機常常用來連接獨立的電腦,而網橋連接的目標是LAN,所以交換機的埠較網橋多。而且集線器是以廣播形式發送數據包,交換機有一個智慧化的功能,可以根據相應的地址發送數據包。
- 轉發過濾: 當一個數據幀的目的地址在MAC地址中有映射時,它被轉發到連接目的節點的埠而不是所有埠
- 學習功能:乙太網交換機了解每一個埠相連設備的MAC地址,並將地址同相應的埠映射起來存放在交換機快取中的MAC地址表中。
路由器:連接多個邏輯上分開的網路,能夠判斷網路地址和選擇IP路徑,內部存儲路由表(配置路由「」),路由表可靜態設置,亦可動態設置(根據RIP路由解析協議自動記錄),每經過一次路由器,TTL值就會減1。
ping命令使用的是ICMP協議
ARP協議: 根據IP地址獲取mac地址
RARP協議:根據mac地址獲取IP地址
IP:標記邏輯上的地址
MAC:標記實際轉發數據時的設備地址
netmask:和IP地址一起來確定網路號,
默認網關:發送的IP不在同一個網段內,那麼會把這個數據轉發給默認網關。Mac地址,在兩個設備之間通訊時變化(路由器),IP地址在整個通訊過程中不會發生任何變化。
DNS伺服器:域名解析伺服器,根據域名解析IP地址
通訊領域的單工、半雙工、全雙工
- 單工通訊:傳輸數據只支援數據在一個方向上傳輸(收音機)
- 半雙工:傳輸允許在兩個方向上傳輸,但是,在某一時刻,只允許數據在一個方向上傳輸,實際上是一種切換方向的單工通訊放心,如:對講機,單行道
- 全雙工:允許數據同時在兩個方向上傳輸,同一時間,允許發送和接收數據。如:網卡,電話,手機,socket。軟體開發領域實現TCP的全雙工只能是通過多執行緒或者多進程來處理。
OSI模型
2. UDP 用戶數據報協議
2.1 UDP用戶數據報協議
無連接的簡單的面向數據報的運輸層協議。
-
特點: UDP數據報文中包括目的埠號和源埠號資訊,由於通訊不需要連接,所以可以實現廣播發送。UDP傳輸數據時有大小限制,每個被傳輸的數據報必須限定在64KB之內。不可靠傳輸協議,發送方所發送的數據報並不一定以相同的次序到達接收方。傳輸速度快。
-
適用場景:UDP一般用於多點通訊和實時數據的業務,注重速度流暢
- 語音廣播
- 影片會議系統
- TFTP SNMP RIP(路由資訊協議,如報告股票市場,航空資訊)
- DNS(域名解釋)
2.2 創建UDP網路程式流程:
- 1.創建客戶端套接字
- 2.發送/接受數據
- 3.關閉套接字
通訊流程:
3 TCP 傳輸控制協議
面向連接的、可靠的、基於位元組流的傳輸層通訊協議,由IETF的RFC 793定義。
- 特性:
- 面向連接,通訊雙方必須先建立連接,雙方都必須為該連接分配一定的內核資源,以管理連接的狀態和連接上的傳輸。
- 可靠傳輸:
- TCP採用發送應答機制
- 超時重傳:發送端發出一個報文段之後就會啟動定時器,在定時時間內沒收到應答就重發這個報文段,為了保證不發生丟包,就給每一個包一個序號,同時序號也保證了傳送到接收端實體的包按序接收。然後接收端對已成功收到的包回一個 ACK包。如果發送端在合理的RTT內未收到確認,對應的數據包將被假設為已丟失,將會進行重傳
- 錯誤校驗:TCP用一個校驗和函數來檢驗數據是否有錯誤;在發送和接收時都要計算校驗和。
- 流量控制和阻塞管理:流量控制用來避免主機發送得過快而使接收方來不及完全收下
3.1 創建TCP網路程式流程
-
服務端
-
# coding: utf-8 import socket # 創建tcp套接字 tcpserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM) addr = ('localhost', 7777) # 綁定ip tcpserver.bind(addr) # 開啟監聽 tcpserver.listen(5) # 接收客戶端請求 print(f'TCP 伺服器已開啟:{addr}') while True: newSocket, clientAddr = tcpserver.accept() while True: data = newSocket.recv(1024) if len(data) > 0: print('receive from [%s]:%d, data: %s' % (*clientAddr, data.decode('utf-8'))) else: break newSocket.send('thank you!'.encode('utf-8')) newSocket.close() tcpserver.close()
-
-
客戶端
-
# coding: utf-8 import socket tcpclient = socket.socket(socket.AF_INET, socket.SOCK_STREAM) dest = ('localhost', 7777) tcpclient.connect(dest) while True: sendData = input('send: #some msg#') if len(sendData) > 0: tcpclient.send(sendData.encode('utf-8')) else: break recvData = tcpclient.recv(1024) print(recvData.decode('utf-8')) tcpclient.close()
-
3.2 TCP的數據包格式
-
源埠和目的埠:各佔16bit=2位元組
-
序列號(Seq):佔32位=4位元組 range=[0:2^32] 表示數據的第一個位元組的序列號,TCP的數據互動式基於序列號(控制華東窗口),發送方通過序列號控制發送的數據,以及超時重傳,接收方通過序列號控制亂序重排。
接收方根據三次握手後確認的首位元組序列號+數據長度,計算得到最後一個位元組的序列號,並將其加1作為ack應答。
-
確認號(ACK):佔4個位元組,表示期望下次收到的序列號。比如伺服器收到客戶端發來的報文段,其序列號欄位值為501,並通過計算可知數據長度為200,所以伺服器可以算出最後一個位元組的序列號為700。這表明伺服器正確收到了客戶端發送的序列號到700為止的數據,因此,伺服器期望下次收到的序列號為701,並將其作為確認號放入應答報文段中
確認號和序列號範圍相同,當溢出時從0開始
-
數據偏移:佔4bit, 表示TCP報文段的第一個數據距離報文段起始處有多遠。數據偏移代表的是4位元組的倍數,由於4位二進位最大的可以表示15, 所以數據偏移最大值為4*15=60位元組,即TCP報文首部最大長度。最小為20位元組,偏移值=5。
-
保留 佔6位,占未使用,可能是預留其他控制標誌位,或者對齊位元組位
-
控制位,用於說明報文段的性質。每個控制欄位佔1位
-
緊急URG:開啟時表示此數據包處於緊急狀態應優先處理
-
確認標誌位ACK:開啟表明確認號有效,TCP規定連接建立後發送的所有報文段ACK位都必須置1
-
推送PSH:該控制位很少使用,因為TCP會自己決定什麼時候應該使用PUSH操作。
-
複位RST:用於複位,表示連接出現錯誤,應當立即關閉。當TCP接收到複位報文段後會通知應用程式連接被複位,隨後關閉連接
-
同步SYN:連接建立的過程中用於同步序列號,告知對方自己的起始序列號。可以根據對方的序列號初始化緩衝區起點(滑動窗口)
SYN=1,ACK=0時表示一個連接請求報文段,SYN=1,ACK=1表示一個連接接收報文段
-
終止FIN:用於釋放連接,報文段中FIN控制位為1表示已經將數據發送完畢。等待關閉連接
-
窗口:佔2個位元組,表示發送該報文段的一方能夠接收的位元組數,表明期望接受到的數據包位元組數,用於擁塞控制。窗口值範圍為[0:2^16−1]
-
校驗和:佔2個位元組,用於檢驗報文段是否出錯。發送方根據發送的報文段計算檢驗和填入報文段首部,接收方根據接收的報文段重新計算,如果不匹配,表明報文段出錯
-
緊急指針:佔2個位元組,表示緊急數據的個數。在緊急狀態下(URG打開),指出窗口中緊急數據的位置(末端)。
-
選項:用於支援一些特殊的變數,比如最大分組長度(MSS),MSS指的是數據的最大長度而不是TCP報文段長度。在將數據發送之前,會根據MSS將數據進行合理的切分,即單次發送的報文段中的數據不能超過MSS,所以MSS應該適當調大一些以降低網路中的報文段個數
查缺補漏
MSS(Maximum Segment Size):MSS 是TCP選項中最經常出現,也是最早出現的選項。MSS選項佔4byte。MSS是每一個TCP報文段中數據欄位的最大長度,注意:只是數據部分的欄位,不包括TCP的頭部。TCP在三次握手中,每一方都會通告其期望收到的MSS(MSS只出現在SYN數據包中)如果一方不接受另一方的MSS值則定位默認值536byte。
MSS值太小或太大都是不合適。太小,例如MSS值只有1byte,那麼為了傳輸這1byte數據,至少要消耗20位元組IP頭部+20位元組TCP頭部=40byte,這還不包括其二層頭部所需要的開銷,顯然這種數據傳輸效率是很低的。MSS過大,導致數據包可以封裝很大,那麼在IP傳輸中分片的可能性就會增大,接受方在處理分片包所消耗的資源和處理時間都會增大,如果分片在傳輸中還發生了重傳,那麼其網路開銷也會增大。因此合理的MSS是至關重要的。MSS的合理值應為保證數據包不分片的最大值,對於乙太網MSS可以達到1460byte,在IP層中有一個類似的概念,MTU(Maximum Transfer Unit)MTU=MSS+TCP Header + IP Header
為什麼需要MSS?
主要是為了最大程度的保證傳輸的高效和穩定性
那麼MTU和MSS又有什麼必然聯繫呢?雖然MTU限制了IP層的報文大小,但分層網路模型本來不就是為了對上層提供透明的服務么?即使一個很大的TCP報文傳遞給IP層,IP層也應該可以經過分段等手段成功傳輸報文才對。
理論上來說是沒錯的,UDP中就不存在MSS,UDP生成任意大的UDP報文,然後包裝成IP報文根據底層網路的MTU分段進行發送。MSS存在的本質原因就是TCP和UDP的根本不同:TCP提供穩定的連接。假設生成了很大的TCP報文,經過IP分段進行發送,而其中一個IP分段丟失了,則TCP協議需要重發整個TCP報文,造成了嚴重的網路性能浪費,而相對的由於UDP無保證的性質,即使丟失了IP分段也不會進行重發。所以說,MSS存在的核心作用,就是避免由於IP層對TCP報文進行分段而導致的性能下降。
通常將MSS設置為MTU-40(20位元組IP頭部+20位元組TCP頭部),在TCP建立連接時由連接雙方商定,雙方得到的MSS值可能並不相同,建立MSS所基於MTU的值基於路徑MTU發現機制獲取。
參考:
TCP Maximum Segment Size (MSS)
TCP Maximum Segment Size (MSS) and Relationship to IP Datagram Size
-
3.3 TCP/IP協議族詳解之IP協議
3.3.1 IP協議的功能:
- 路由定址
- 傳遞服務,有兩個特點:不可靠,可靠性由上層協議提供,如TCP協議,無連接(IP並不維護任何關於後續數據報的狀態資訊。每個數據報的處理是相互獨立的,這也就是說IP數據報可以不按發送順序接收)
- 數據包分段(Segment)和重組
3.3.2 IP協議頭部格式
可根據Wireshark抓包工具分析數據包含義 參考:TCP/IP協議族詳解(二)
應用程式使用TCP/IP協議傳輸數據時,數據要被送入協議棧經過逐層封裝,最後作為比特流在媒體上傳送,其過程示意圖如下所示:
註:從上圖可以看到乙太網幀的數據長度是有大小限制的,這個最大值稱為 MTU,所以當 IP 數據包長度大於 MTU 時會被拆成多個幀傳輸,稱為 「IP分片」——-Mr.su Blog
3.4 TCP 三次握手
拋出疑惑:為什麼是三次握手而不是二次或者四次握手?
TCP作為一種可靠傳輸控制協議,核心思想:既要保證數據可靠傳輸,又要提高傳輸的效率,而用三次就可以滿足以上兩方面的需求。
TCP的可靠性就是通過三次握手就是確認通訊雙方數據原點的初始序列號 (Initial Sequence Number)。
通俗的描述:客戶端A發出連接請求,由作業系統動態隨機選取一個32位長的序列號(Initial Sequence Number), 假設A的初始序列號是1000, 以該序列號為原點,對自己將要發送的每個位元組進行編號,1001, 1002…, 並把自己的初始序列號INS告訴B, 什麼樣的編號的數據是合法的,方便服務端B對A的每一個編號的位元組數據進行確認。如:如果A收到B確認編號為2001,則意味著位元組編號為1001-2000,共1000個位元組已經安全到達。
同理B也是類似的操作,假設B的初始序列號ISN為2000,以該序列號為原點,對自己將要發送的每個位元組的數據進行編號,2001,2002,2003…,並把自己的初始序列號ISN告訴A,以便A可以確認B發送的每一個位元組。如果B收到A確認編號為4001,則意味著位元組編號為2001-4000,共2000個位元組已經安全到達。
第一次握手:
客戶端向服務端發送連接請求報文段,報文段的頭部中SYN=1, ACK=0, seq=x。請求發送後,客戶端進入SYN-SENT狀態
- SYN=1, ACK=0 標識該報文段為連接請求報文
- seq=x, 標識本次TCP通訊客戶端數據位元組流的初始序列號
- TCP規定:SYN=1的報文段不能有數據部分,但要消耗掉一個位元組,一個序號
第二次握手:
服務端處於監聽狀態LISTEN,收到連接請求報文後,如果同意連接,返回一個應答 SYN=1, ACK=1, seq=y, ack=x+1, 進入SYN-RCVD狀態
第三次握手:
當客戶端收到伺服器的應答後,還要向服務端發送一個確認報文段,表示服務端發來的連接同意應答已經成功收到,且收到服務端的出示序列號y
確認報文為:ACK=1, seq=x+1, ack=y+1。
為什麼連接建立需要三次握手,而不是2次握手?
防止失效的連接請求報文段被服務端接收,從而產生錯誤,失效的連接請求:若客戶端向服務端發送的連接請求丟失,客戶端等待應答超時後就會再次發送連接請求,此時,上一個連接請求就是『失效的』—《電腦網路》謝希仁版
三次握手中存在的漏洞:SYN flood!,攻擊者通過向伺服器發起大量的SYN報文,把伺服器的SYN報文連接的隊列生生耗盡,導致正常的連接請求得不到處理,目前只能進行減緩,別沒有解決修補程式
-
在web應用程式中可以使用安全的CSRF令牌環節問題。CSRF攻擊將在伺服器造成持久的變化而沒有處理要求,除非使用了有效的CSRF令牌。
-
首保丟棄:可通過丟棄客戶端的第一個SYN報文來達到防禦的目的,TCP是一種可靠的協議,為了確保所有的數據包都能到達伺服器,設計了一個重傳機制。真實的客戶端訪問,在一定的時間內如果沒有收到伺服器的回復,將會再次發送SYN報文。
-
內核層面進行緩解:
- 增大tcp_max_syn_backlog
- 減小tcp_synack_retries
- 啟用tcp_syncookies: 當啟用tcp_syncookies時,backlog滿了後,linux內核生成一個特定的n值,而不並把客戶的連接放到半連接的隊列backlog里(即沒有存儲任何關於這個連接的資訊,不浪費記憶體)。當客戶端提交第三次握手的ACK包時,linux內核取出n值,進行校驗,如果通過,則認為這個是一個合法的連接。(加密的INS)
註:tcp_max_syn_backlog 在 syn_cookies 開啟時是無效的,這兩個選項存在衝突
3.5 TCP四次揮手
第一揮手
若A認為數據發送完成,就會向B發送連接釋放請求,該請求只有報文頭,頭重攜帶的主要參數為:FIN=1, seq=u, 此時A進入FIN-WAIT-1狀態
- FIN=1即TCP報文段中的控制位FIN置1表示該數據報為連接釋放請求
- seq=u, u-1是A向B發送的最後一個位元組的序號
第二次揮手
B收到連接釋放請求後,會通知相應的應用程式,告訴它連接已經釋放,此時B進入CLOSE_WAIT狀態, 報文頭:ACK=1, seq=v, ack=u+1
- ACK=1, 除了TCP連接請求報文段以外,TCP通訊過程中數據報的ACK控制為都為1
- seq=v, v-1表示B向A發送的最後一個位元組的序號
- ack=u+1 表示希望收到第u+1個位元組開始的報文段,已經成功接收了簽u個位元組數據
A收到該應答後進入 FIN_WAIT_2狀態,等待B發送連接釋放請求
第二次揮手後,A->B方向的連接已經釋放,A不會再發送數據,但B->A方向的連接仍然存在。
第三次揮手
當B向A發送完所有數據後,向A發送連接釋放請求,請求頭: FIN=1, ACK=1, seq=w, ack=u+1 B進入 LAST_ACK狀態
第四次揮手
A收到釋放請求後,向B發送確認應答,A進入TIME_WAIT狀態。該狀態會持續2MSL(Maximum Segment Lifetime)時間,(報文最大生存時間),若該時間段內B沒有發送請求的話,就進入CLOSED狀態,關閉TCP。當B收到確認應答後,也進入CLOSED狀態, 關閉TCP。
為什麼A要先進入TIME-WAIT狀態,等待2MSL時間後才進入CLOSED狀態?
為了保證B能收到A的確認應答。
若A發完確認應答後直接進入CLOSED狀態,那麼如果該應答丟失,B等待超時後就會重新發送連接釋放請求,但此時A已經關閉了,不會作出任何響應,因此B永遠無法正常關閉。在模擬tcpserver的時候,如果是伺服器先close的時候,在2MSL中(也就是2-4分鐘之內並不會馬上釋放埠)不過在實際應用中可以通過設置 SO_REUSEADDR選項達到不必等待2MSL時間結束再使用此埠。
參考TCP 為什麼是三次握手,而不是兩次或四次?-[大閑人柴毛毛]