網路程式設計TCP/IP詳解

JonPan發表於2020-06-02

網路程式設計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模型

image.png

image.png

2. UDP 使用者資料包協議

2.1 UDP使用者資料包協議

無連線的簡單的面向資料包的運輸層協議。

  • 特點: UDP資料包文中包括目的埠號和源埠號資訊,由於通訊不需要連線,所以可以實現廣播傳送。UDP傳輸資料時有大小限制,每個被傳輸的資料包必須限定在64KB之內不可靠傳輸協議,傳送方所傳送的資料包並不一定以相同的次序到達接收方。傳輸速度快

  • 適用場景:UDP一般用於多點通訊和實時資料的業務,注重速度流暢

    • 語音廣播
    • 視訊會議系統
    • QQ
    • TFTP SNMP RIP(路由資訊協議,如報告股票市場,航空資訊)
    • DNS(域名解釋)
2.2 建立UDP網路程式流程:
  • 1.建立客戶端套接字
  • 2.傳送/接受資料
  • 3.關閉套接字

image.png

通訊流程:

20200531124111.png

3 TCP 傳輸控制協議

面向連線的、可靠的、基於位元組流的傳輸層通訊協議,由IETF的RFC 793定義。

  • 特性:
    • 面向連線,通訊雙方必須先建立連線,雙方都必須為該連線分配一定的核心資源,以管理連線的狀態和連線上的傳輸。
    • 可靠傳輸:
      • TCP採用傳送應答機制
      • 超時重傳:傳送端發出一個報文段之後就會啟動定時器,在定時時間內沒收到應答就重發這個報文段,為了保證不發生丟包,就給每一個包一個序號,同時序號也保證了傳送到接收端實體的包按序接收。然後接收端對已成功收到的包回一個 ACK包。如果傳送端在合理的RTT內未收到確認,對應的資料包將被假設為已丟失,將會進行重傳
      • 錯誤校驗:TCP用一個校驗和函式來檢驗資料是否有錯誤;在傳送和接收時都要計算校驗和。
      • 流量控制和阻塞管理:流量控制用來避免主機傳送得過快而使接收方來不及完全收下

tcpmode.png

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的資料包格式

image.png

  • 源埠和目的埠:各佔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

    image.png

    為什麼需要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報文段首部格式

    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協議頭部格式

image.png

可根據Wireshark抓包工具分析資料包含義 參考:TCP/IP協議族詳解(二)

應用程式使用TCP/IP協議傳輸資料時,資料要被送入協議棧經過逐層封裝,最後作為位元流在媒體上傳送,其過程示意圖如下所示:

image.png

注:從上圖可以看到乙太網幀的資料長度是有大小限制的,這個最大值稱為 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個位元組已經安全到達。

image.png

第一次握手:

客戶端向服務端傳送連線請求報文段,報文段的頭部中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報文連線的佇列生生耗盡,導致正常的連線請求得不到處理,目前只能進行減緩,別沒有解決補丁

  1. 在web應用程式中可以使用安全的CSRF令牌環節問題。CSRF攻擊將在伺服器造成持久的變化而沒有處理要求,除非使用了有效的CSRF令牌。

  2. 首保丟棄:可通過丟棄客戶端的第一個SYN報文來達到防禦的目的,TCP是一種可靠的協議,為了確保所有的資料包都能到達伺服器,設計了一個重傳機制。真實的客戶端訪問,在一定的時間內如果沒有收到伺服器的回覆,將會再次傳送SYN報文。

  3. 核心層面進行緩解:

    • 增大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四次揮手

image.png

第一揮手

若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 為什麼是三次握手,而不是兩次或四次?-[大閒人柴毛毛]

相關文章