計算機網絡協議(三)——UDP、TCP、Socket

概述


這個專欄的計算機網絡協議,我是在極客時間上學習 已經有三萬多人購買的劉超老師趣談網絡協議專欄,講的特別好,像看小說一樣學習到了平時很枯燥的知識點,計算機網絡的書籍太枯燥,感興趣的同學可以去付費購買,絕對物超所值,本文就是對自己學習專欄的總結,評論區可以留下你的問題,咱們一起討論!


傳輸層中有兩個重要的協議,UDP和TCP,這也是在開發中經常用到的協議,同樣也是面試的重點。本篇將分爲三節進行介紹:

  • UDP協議
  • TCP協議
  • 套接字Socket

一、UDP協議

很多人都會被問到 TCP和UDP的區別,那麼大部分人都會回答,TCP面向連接,UDP面向無連接;

建立連接:是爲了在客戶端和服務端維護連接,而建立一定的數據結構來維護雙方交互的狀態,用這樣的數據結構來保證所謂的面向連接的特性;

簡單介紹下TCP和UDP之間的區別:

  • TCP 提供可靠交付,UDP繼承了IP包的特性,不保證不丟失,不保證按時到達;
  • TCP是面向字節流的,發送的時候發的是一個流,沒頭沒尾的。UDP繼承了IP的特性,基於數據報的,一個個發,一個個收;
  • TCP是可以有擁堵控制的,可以根據網絡環境調整自己的行爲;UDP就是應用讓我發,我就發,管它洪水滔天;
  • TCP是一個有狀態的服務,通俗的講就是有腦子的,可以精確的記着,自己發送了沒有,接收到沒有,發送到哪個了,應該接收到哪個了,錯一點兒都不行;UDP其實是一個無狀態服務,無腦子,天真無邪的發出去就發出去唄;

UDP的包頭
UDP的包頭格式很簡單,只有源端口號和目標端口號:
UDP包頭


UDP的三大特點

  • 溝通簡單,秉承性善論,相信網絡通路默認就是很容易送達的,不容易被丟棄的;
  • 輕信他人,不會建立連接,雖然有端口號,但是監聽在這個地方,誰都可以傳給他數據,也可以傳給任何人數據;
  • 愣頭青,做事不懂權變,不會根據網絡的情況進行發包的擁塞控制,無論網絡丟包丟成啥樣了,它該怎麼發還怎麼發;

UDP的三大使用場景

  • 需要資源少,在網絡情況比較好的內網,或者對於丟包不敏感的應用;
  • 不需要一對一溝通,建立連接,而是可以廣播的應用;UDP的不面向連接的功能,可以使得可以承載廣播或者多播的協議。DHCP就是一種廣播的形式,就是基於UDP協議的;
  • 需要處理速度快,時延低,可以容忍少數丟包,即便網絡堵塞,也毫不退縮,一往無前的時候;UDP簡單、處理速度快,不像TCP一樣,操那麼多心;TCP在網絡不好出現丟包的時候,擁塞控制策略會主動的退縮,降低發送速度,這就相當於本來環境就差,還自斷臂膀,用戶本來就卡,這下更卡了

基於UDP的實際應用

  • 網頁或者APP的訪問,訪問網頁和手機APP都是基於HTTP協議(基於TCP)的,建立連接需要多次交互,比較耗時,Google提出了QUIC實現快速連接建立、減少重傳時延,自適應擁塞控制;
  • 流媒體的協議,直播協議多使用RTMP(基於TCP),當數據丟包或者網絡不好,影響直播的實時性,很多直播應用,都基於UDP實現了自己的視頻傳輸協議;
  • 實時遊戲,採用自定義的可靠UDP協議,自定義重傳策略,能夠把丟包產生的延遲降到最低,儘量減少網絡問題對遊戲性造成的影響;
  • IoT物聯網,物聯網通信協議Thread,就是基於UDP協議的,解決了物聯網領域終端資源少,實時性要求高的問題;
  • 移動通信領域:4G網絡裏,移動流量上網的數據面對的協議GTP-U是基於UDP的;

總結:

  • 如果將TCP比作成熟的社會人,UDP則是頭腦簡單的小朋友;TCP複雜,UDP簡單;TCP維護連接,UDP誰都相信;TCP會堅持知進退;UDP鐵憨憨一個,勇往直前;
  • UDP簡單但有簡單的用法。它可以用在環境簡單、需要多播、應用層自己控制傳輸的地方。例如DHCP、VXLAN、QUIC

二、TCP協議(上)

TCP秉承的是性惡論,天然認爲網絡環境是惡劣的,丟包、亂序、重傳、擁塞都是常見的事情,需要從算法層面來保證可靠性。


TCP包頭格式
TCP報文格式

  • 源端口號和目標端口號:知道誰發的和發給誰的;
  • 序號:編號是爲了解決亂序問題;
  • 確認序號:發出去的包應該有確認,沒有收到就應該重新發送,直到送達;
  • 狀態位SYN是發起一個連接、ACK是回覆、RST是重新連接、FIN是結束連接;
  • 窗口大小:TCP要做流量控制,通信雙方各聲明一個窗口,標識自己當前能夠的處
    理能力,別發送的太快,撐死我,也別發的太慢,餓死我;

通過對TCP頭的解析,我們知道要掌握TCP協議,重點應該關注以下幾個問題:

  • 順序問題 ,穩重不亂;
  • 丟包問題,承諾靠譜;
  • 連接維護,有始有終;
  • 流量控制,把握分寸;
  • 擁塞控制,知進知退;

2.1 TCP的三次握手

TCP中所有的問題,都要先建立連接,需要先看連接維護的問題,TCP的連接建立,常被稱爲三次握手;

A:您好B,我是A.
B:您好A,我是B.
A:您好B

採用 請求->應答->應答之應答的方式,保證二者的消息傳送都是有來有回的;

三次握手除了雙方建立連接外,主要還是爲了溝通一件事情,就是TCP包的序號的問題。 每個連接都要有不同的序號。這個序號的起始序號是隨着時間變化的,可以看成一個32位的計數器,每4ms加一,其時序圖如下:
狀態變化時序圖
1、剛開始客戶端和服務端都處於CLOSED狀態,服務端先監聽某個端口,處於LISTEN狀態

2、客戶端主動發起連接請求SYN=1,ACK=0,初始序號爲x,之後處於SYN-SENT狀態

3、服務端收到發起的連接請求,如果同意連接就返回SYN=1,ACK=1,確認號爲 x+1,同時也選擇一個初始的序號 y,之後處於SYN-RCVD狀態

4、客戶端收到服務端發送的SYN和ACK之後,發送ACK的ACK,確認號爲 y+1,序號爲 x+1。之後處於ESTABLISHED狀態,因爲它一發一收成功了;

5、服務端收到ACK的ACK之後,處於ESTABLISHED狀態,因爲它也一發一收了。


兩次握手或者四次不行嗎?

舉個例子:

在一個網絡環境不可靠的情況下,A發出一個連接請求,發出一個請求杳無音信就會一直髮,終於有一個包到B了,但是A還不知道會繼續發;

收到A的請求之後,B如果同意連接就會發送應答包給A;但是B的應答包也是一入網絡深似海啊,不知道能不能到A,所以當然不能認爲和A已經建立了連接;

還有一個問題就是,A和B建立起短暫的連接通信之後,A之前發送的請求包饒了地球不知道多少圈竟然又到了B,假如B認爲這是一個正常的連接請求,同意建立連接,但這個連接不會進行下去,也沒有個終結的時候,純屬單相思了,因而兩次握手肯定不行。

B發送的應答可能會發送多次,但是隻要一次到達A,A就認爲連接已經建立了,因爲對於A來講,他的消息有去有回。A會給B發送應答之應答,而B也在等這個消息,才能確認連接的建立,只有等到了這個消息,對於B來講,纔算它的消息有去有回。

當然A發給B的應答之應答也會丟,也會繞路,甚至B掛了。按理來說,還應該有個應答之應答之應答,這樣下去就沒底了。四次握手、還是四十次握手都是可以的,哪怕四百次握手也不能百分百保證可靠,只要雙方的消息都有去有回就可以了。

我們在程序設計的時候可以開啓keepalive機制,防止A建立連接後空着,不發數據;


2.2 TCP的四次揮手


過程如下:
A:B啊,我不想玩了;
B:哦,你不想玩了啊,我知道;
此時的A很可能是發送完最後的數據就準備不玩了,不能在ACK的時候就關閉連接,此時B還沒有忙完自己的事情,還是可以發送數據的,稱爲半關閉狀態
B:A啊,好吧,那我也不玩了,拜拜;
A:好的,拜拜;

斷開連接的時序圖如下所示:

在這裏插入圖片描述
雙方一開始都是處於建立連接的狀態:

  • A 發送連接釋放報文,FIN=1,就進入FIN_WAIT_1的狀態;
  • B 收到之後發出確認,此時 TCP 屬於CLOSE_WAIT(半關閉)狀態,B 能向 A 發送數據但是 A 不能向 B 發送數據;
  • 當 B 不再需要連接時,發送連接釋放報文,FIN=1就進入FIN_WAIT_2的狀態
  • A 收到後發出確認,進入 TIME-WAIT 狀態,等待 2 MSL(最大報文存活時間)後釋放連接;
  • B 收到 A 的確認後釋放連接;

四次揮手的原因

客戶端發送了 FIN 連接釋放報文之後,服務器收到了這個報文,就進入了 CLOSE-WAIT 狀態。這個狀態是爲了讓服務器端發送還未傳送完畢的數據,傳送完畢之後,服務器會發送 FIN 連接釋放報文。

TIME_WAIT

客戶端接收到服務器端的 FIN 報文後進入此狀態,此時並不是直接進入 CLOSED 狀態,還需要等待一個時間計時器設置的時間 2MSL。這麼做有兩個理由:

  • 確保最後一個確認報文能夠到達。如果 B 沒收到 A 發送來的確認報文,那麼就會重新發送連接釋放請求報文,A
    等待一段時間就是爲了處理這種情況的發生。
  • 等待一段時間是爲了讓本連接持續時間內所產生的所有報文都從網絡中消失,使得下一個新的連接不會出現舊的連接請求報文。

2.3 TCP狀態機

加黑加粗的部分,是上面說到的主要流程,其中阿拉伯數字的序號,是連接過程中的順
序,而大寫中文數字的序號,是連接斷開過程中的順序。加粗的實線是客戶端A的狀態變遷,加粗的虛線是服務端B的狀態變遷;
TCP狀態機


三、TCP協議(下)

參考了CS-Notes的博文,總結的很好!

TCP傳輸是可靠的,需要很多機制保證傳輸的可靠性,裏面也要有恆心,就是各種重傳的策略;還需要有智慧,裏面包含着大量的算法


如何成爲一個靠譜的協議?
TCP中爲了保證順序性,每一個包都有一個ID;建立連接的時候,會商定起始的ID是什麼,然後按照ID一個個發送。採用**累計確認或者累計應答(cumulative acknowledgment)**的方式去保證不丟包;

爲了記錄所有發送的包和接收的包,TCP也需要發送端和接收端分別都有緩存來保存這些記錄。發送端的緩存裏是按照包的ID一個個排列,根據處理的情況分成四個部分:

  • 發送了並且已經確認的;
  • 發送了並且尚未確認的;
  • 沒有發送,但是已經等待發送的;
  • 沒有發送,並且暫時還不會發送的;

3.1 可靠傳輸

TCP 使用超時重傳來實現可靠傳輸:如果一個已經發送的報文段在超時時間內沒有收到確認,那麼就重傳這個報文段。

一個報文段從發送再到接收到確認所經過的時間稱爲往返時間 RTT加權平均往返時間 RTTs 計算如下:
Alt

其中,0 ≤ a < 1,RTTs 隨着 a 的增加更容易受到 RTT 的影響。

超時時間 RTO 應該略大於 RTTs,TCP 使用的超時時間計算如下:

在這裏插入圖片描述

其中 RTTd 爲偏差的加權平均值


3.2 TCP滑動窗口

窗口是緩存的一部分,用來暫時存放字節流。發送方和接收方各有一個窗口,接收方通過 TCP 報文段中的窗口字段告訴發送方自己的窗口大小,發送方根據這個值和其它信息設置自己的窗口大小。

發送窗口內的字節都允許被髮送,接收窗口內的字節都允許被接收。如果發送窗口左部的字節已經發送並且收到了確認,那麼就將發送窗口向右滑動一定距離,直到左部第一個字節不是已發送並且已確認的狀態;接收窗口的滑動類似,接收窗口左部字節已經發送確認並交付主機,就向右滑動接收窗口。

接收窗口只會對窗口內最後一個按序到達的字節進行確認,例如接收窗口已經收到的字節爲 {31, 34, 35},其中 {31} 按序到達,而 {34, 35} 就不是,因此只對字節 31 進行確認。發送方得到一個字節的確認之後,就知道這個字節之前的所有字節都已經被接收。
在這裏插入圖片描述

3.3 TCP 流量控制

流量控制是爲了控制發送方發送速率,保證接收方來得及接收。

接收方發送的確認報文中的窗口字段可以用來控制發送方窗口大小,從而影響發送方的發送速率。將窗口字段設置爲 0,則發送方不能發送數據。

3.4 TCP 擁塞控制

如果網絡出現擁塞,分組將會丟失,此時發送方會繼續重傳,從而導致網絡擁塞程度更高。因此當出現擁塞時,應當控制發送方的速率。這一點和流量控制很像,但是出發點不同。流量控制是爲了讓接收方能來得及接收,而擁塞控制是爲了降低整個網絡的擁塞程度
在這裏插入圖片描述
TCP 主要通過四個算法來進行擁塞控制:慢開始、擁塞避免、快重傳、快恢復

發送方需要維護一個叫做擁塞窗口(cwnd)的狀態變量,注意擁塞窗口與發送方窗口的區別:擁塞窗口只是一個狀態變量,實際決定發送方能發送多少數據的是發送方窗口。

爲了便於討論,做如下假設:

  • 接收方有足夠大的接收緩存,因此不會發生流量控制;
  • 雖然 TCP 的窗口基於字節,但是這裏設窗口的大小單位爲報文段。

在這裏插入圖片描述

3.4.1 慢開始與擁塞避免

發送的最初執行慢開始,令 cwnd = 1,發送方只能發送 1 個報文段;當收到確認後,將 cwnd 加倍,因此之後發送方能夠發送的報文段數量爲:2、4、8 …

注意到慢開始每個輪次都將 cwnd 加倍,這樣會讓 cwnd 增長速度非常快,從而使得發送方發送的速度增長速度過快,網絡擁塞的可能性也就更高。設置一個慢開始門限 ssthresh,當 cwnd >= ssthresh 時,進入擁塞避免,每個輪次只將 cwnd 加 1。

如果出現了超時,則令 ssthresh = cwnd / 2,然後重新執行慢開始。

3.4.2 快重傳與快恢復

在接收方,要求每次接收到報文段都應該對最後一個已收到的有序報文段進行確認。例如已經接收到 M1M2,此時收到 M4,應當發送對 M2 的確認。

在發送方,如果收到三個重複確認,那麼可以知道下一個報文段丟失,此時執行快重傳,立即重傳下一個報文段。例如收到三個 M2,則 M3 丟失,立即重傳 M3。

在這種情況下,只是丟失個別報文段,而不是網絡擁塞。因此執行快恢復,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此時直接進入擁塞避免。

慢開始和快恢復的快慢指的是 cwnd 的設定值,而不是 cwnd 的增長速率。慢開始 cwnd 設定爲 1,而快恢復 cwnd 設定爲 ssthresh。

在這裏插入圖片描述

四、套接字Socket

在通信之前,雙方都要建立一個Socket。Socket編程進行的是端到端的通信,也只能是端到端協議之上網絡層和傳輸層的。

在網絡層中,Socket函數需要指定到底是IPv4還是IPv6,分別對應設置爲AF_INET和AF_INET6。還要指定到底是TCP還是UDPTCP協議是基於數據流的,所以設置爲SOCK_STREAM,而UDP是基於數據報的,因而設置爲SOCK_DGRAM


4.1 基於TCP協議的Socket程序函數調用過程

兩端創建Socket之後,TCP的服務端調用bind函數監聽一個端口, 給這個Socket賦予一個IP地址和端口

當服務端有了IP和端口號,就可以調用listen函數進行監聽。此時的客戶端就可以發起連接請求了;

在內核中爲每個Socket維護兩個隊列,分別是已經建立了連接、完成三次握手後處於established狀態的隊列;一個是還沒有完全建立連接的隊列,三次握手還沒完成,處於syn_rcvd的狀態。

接下來,服務端調用accept函數,拿出一個已經完成的連接進行處理。

在服務端等待的時候,客戶端可以通過connect函數發起連接。先在參數中指明要連接的IP地址和端口號,然後開始發起三次握手。內核會給客戶端分配一個臨時的端口。一旦握手成功,服務端的accept就會返回另一個Socket。

連接建立之後雙方開始通過read和write函數來讀寫數據,下圖是基於TCP協議的Socket程序函數調用過程:
在這裏插入圖片描述

4.2 基於UDP協議的Socket程序函數調用過程

UDP是沒有連接的,所以不需要三次握手,也就不需要調用listen和connect,但是,UDP的的交互仍然需要IP和端口號,因而也需要bind函數;但正是沒有連接狀態,每次通信的時候,都調用sendto和recvfrom,都可以傳入IP地址和端口;

下圖就是基於UDP協議的Socket程序函數調用過程
在這裏插入圖片描述


4.3 服務器如何支持高併發?

在學習了上面的Socket函數之後,可以寫一個簡單的網絡交互程序;

系統會用一個四元組來標識一個TCP連接

{本機IP, 本機端口, 對端IP, 對端端口}

最大TCP連接數=客戶端IP數×客戶端端口數,對IPv4,客戶端的IP數最多爲2的32次方,客戶端的端口數最多爲2的16次方,也就是服務端單機最大TCP連接數,約爲2的48次方。

當然最大的TCP連接數還要受到 Socket中的文件描述符以及內存的限制

如何在資源有限的情況下,進行更多的連接?


方案一:多進程式

你相當於一個代理,一旦監聽到請求,建立連接就會有一個已連接的Socket,這個時候可以採用fork函數創建一個子進程,將基於已連接Socket的交互交給這個新的子進程來做。子進程就可以通過這個已連接Socket和客戶端進行互通了,當通信完畢之後,就可以退出進程。父進程可以通過進程ID查看子進程是否完成項目,是否需要退出。

相當於來了一個項目,你就找一個外包公司幫你解決這個問題。

方案二:多線程式
上面這種方式的問題在於,每次有項目都找外包,這個是不划算的;

線程就相當於一個公司成立項目組,一個項目做完了,那這個項目組就可以解散,組成另外的項目組;

通過pthread_create創建一個線程,也是調用do_fork,新的線程也可以通過已連接Socket處理請求,從而達到併發處理的目的。
多線程式
基於進程或者線程模型都存在一個問題:
新到來一個TCP連接,就需要分配一個進程或者線程。一臺機器無法創建很多進程或者線程,就是C10K的問題;

C10K:
一臺機器要維護1萬個連接,就要創建1萬個進程或者線程,那麼操作系統是無法承受的。如果維持1億用戶在線需要10萬臺服務器,成本也太高了。


方案三:IO多路複用,一個線程維護多個Socket

簡述一下就是,一個項目組可以看多個項目,每個項目組都應該有個項目進度牆,將自己組看的項目列在那裏,然後每天通過項目牆看每個項目的進度,一旦某個項目有了進展,就派人去盯一下。

Socket是文件描述符,因而某個線程盯的所有的Socket,都放在一個文件描述符集合fd_set(項目進度牆)中,調用select函數來監聽文件描述符集合是否有變化,一旦有變化,就會依次查看每個文件描述符。那些發生變化的文件描述符在fd_set對應的位都設爲1,表示Socket可讀或者可寫,從而可以進行讀寫操作,然後再調用select,接着盯着下一輪的變化。

方案四:IO多路複用
方案三中採用select函數來查看fd_set是否有Socket發生變化,每次輪詢都會影響性能,且能查看的數量由FD_SETSIZE限制;

改成事件通知的方式,情況就會好很多,項目組不需要通過輪詢挨個盯着這些項目,而是當項目進度發生變化的時候,主動通知項目組,然後項目組再根據項目進展情況做相應的操作。

通過epoll多路複用模型,它不是通過輪詢的方式,而是通過註冊callback函數
方式,當某個文件描述符發送變化的時候,就會主動通知。
在這裏插入圖片描述
如上圖所示,進程打開了Socket m, n, x等多個文件描述符,現在需要通過epoll來監聽這些Socket是否都有事件發生。其中epoll_create創建一個epoll對象,對應着打開文件列表中那個的一項,通過紅黑樹來保存這個epoll要監聽的所有Socket。

epoll_ctl添加一個Socket的時候,其實是加入這個紅黑樹;當一個Socket來了一個事件的時候,可以從這個列表中得到epoll對象,並調用call back通知它。

這種通知方式使得監聽的Socket數據增加的時候,效率不會大幅度降低,能夠同時監聽的Socket的數目也非常的多;


總結

  • 需要記住TCP和UDP的Socket的編程中,客戶端和服務端都需要調用哪些函數;
  • 能夠支撐大量連接的高併發的服務端不容易,需要多進程、多線程,而epoll機制能解決C10K問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章