Socket網絡編程(一):Socket網絡編程理論知識

博客主頁html

1. 軟件結構

  1. C/S結構:全稱爲Client/Server結構,是指客戶端和服務器結構,常見的程序如:QQ、迅雷等
  2. B/S結構:全稱爲Browser/Server結構,是🈯️瀏覽器和服務器結構,常見瀏覽器有:谷歌、火狐等

兩種結構各有優點,可是不管哪一種結構,都離不開網絡的支持,網絡編程,就是在必定的協議下,實現兩臺計算機通訊的程序。java

2. 網絡通訊協議

咱們先來看這段代碼:程序員

public class HelloWorld {
  public static void main(String[] args){
    System.out.println("Hello World!");
  }
}

做爲程序員,這段代碼必定都很熟悉了。其實這段代碼也是一種協議,是人類和計算機溝通的協議,只有經過這種協議,計算機才知道咱們想讓它作什麼。面試

協議三要素
這種協議比較接近人類語言,機器不能直接讀懂,須要進行翻譯,翻譯的工做交給編譯器,也就是compile。這個過程比較複雜,其中的編譯原理很是複雜,我在這裏不進行詳述。

計算機語言做爲程序員控制一臺計算機工做的協議,具有了協議的三要素:算法

  • 語法,就是這一段內容要符合必定的規則和格式。例如,括號要成對,結束要使用分號等。
  • 語義,就是這一段內容要表明某種意義。例如數字減去數字是有意義的,數字減去文本通常來講就沒有意義。
  • 順序,就是先幹啥,後幹啥。例如,能夠先加上某個數值,而後再減去某個數值。

可是,要想打造互聯網世界的通天塔,只教給一臺機器作什麼是不夠的,你須要學會教給一大片機器作什麼。這就須要網絡協議。只有經過網絡協議,才能使一大片機器互相協做、共同完成一件事。編程

舉一個例子:打開瀏覽器輸入一個網站的地址,瀏覽器就會展現展現該網址的頁面內容。segmentfault

瀏覽器是如何識別的呢?是由於它收到了一段來自HTTP協議的「東西」瀏覽器

HTTP/1.1 200 OK
Date: Tue, 27 Mar 2018 16:50:26 GMT
Content-Type: text/html;charset=UTF-8
Content-Language: zh-CN

<!DOCTYPE html>
<html>
<head>
<base href="https://pages.kaola.com/" />
<meta charset="utf-8"/> <title>網易考拉3週年主會場</title>

它符合協議的三要素:服務器

  1. 符合語法,也就是說,只有按照上面那個格式來,瀏覽器才認。例如,上來是狀態,而後是首部,而後是內容。
  2. 符合語義,就是要按照約定的意思來。例如,狀態200,表述的意思是網頁成功返回。若是不成功,就是咱們常見的「404」。
  3. 符合順序,你一點瀏覽器,就是發送出一個HTTP請求,而後纔有上面那一串HTTP返回的東西。

經常使用的網絡協議
當在瀏覽器裏面輸入 https://www.taobao.com ,這是一個URL。瀏覽器只知道名字是「www.taobao.com」,可是不知道具體的地點,因此不知道應該如何訪問。因而,它打開地址簿去查找。可使用通常的地址簿協議DNS去查找,還可使用另外一種更加精準的地址簿查找協議HTTPDNS網絡

不管用哪種方法查找,最終都會獲得這個地址:218.98.31.235。這個是IP地址,是互聯網世界的「門牌號」。

不管是HTTP協議仍是HTTPS協議,裏面都會寫明「你要買什麼和買多少」。

DNS、HTTP、HTTPS所在的層咱們稱爲應用層。通過應用層封裝後,瀏覽器會將應用層的包交給下一層去完成,經過socket編程來實現。下一層是傳輸層。傳輸層有兩種協議,一種是無鏈接的協議UDP,一種是面向鏈接的協議TCP。所謂的面向鏈接就是,TCP會保證這個包可以到達目的地。若是不能到達,就會從新發送,直至到達。

TCP協議裏面會有兩個端口,一個是瀏覽器監聽的端口,一個是服務器監聽的端口。操做系統每每經過端口來判斷,它獲得的包應該給哪一個進程。

傳輸層封裝完畢後,瀏覽器會將包交給操做系統的網絡層。網絡層的協議是IP協議。在IP協議裏面會有源IP地址,即瀏覽器所在機器的IP地址和目標IP地址,也即服務器的IP地址。

操做系統知道了目標IP地址,就開始想如何根據這個門牌號找到目標機器。就要經過網關,操做系統在啓動的時候,就會被DHCP協議配置IP地址,以及默認的網關的IP地址192.168.1.1。

操做系統如何將IP地址發給網關呢?經過ARP協議,找到MAC地址。

因而操做系統將IP包交給了下一層,也就是MAC層。網卡再將包發出去。因爲這個包裏面是有MAC地址的,於是它可以到達網關。

網關收到包以後,會根據本身的知識,判斷下一步應該怎麼走。網關每每是一個路由器,到某個IP地址應該怎麼走,這個叫做路由表。

路由器有點像玄奘西行路過的一個個國家的一個個城關。每一個城關都連着兩個國家,每一個國家至關於一個局域網,在每一個國家內部,均可以使用本地的地址MAC進行通訊。

一旦跨越城關,就須要拿出IP頭來,裏面寫着貧僧來自東土大唐(就是源IP地址),欲往西天拜佛求經(指的是目標IP地址)。路過寶地,借宿一晚,明日啓行,請問接下來該怎麼走啊?

城關每每是知道這些「知識」的,由於城關和臨近的城關也會常常溝通。到哪裏應該怎麼走,這種溝通的協議稱爲路由協議,經常使用的有OSPF和BGP。

七層網絡模型OSI和TCP/IP四層模型對比:

3. 協議分類

通訊的協議仍是比較複雜的,java.net包中包含的類和接口,它們提供封裝了底層的通訊細節,能夠直接使用這些類和接口,專一網絡程序開發,而不用考慮通訊細節。

傳輸層裏比較重要的兩個協議:一個是TCP,一個是UDP。對於平常開發來講,最經常使用的就是這兩個協議。

3.1 UDP協議

那麼TCP和UDP有哪些區別呢?通常在面試的時候問這兩個協議的區別,大部分人會回答,TCP是面向鏈接的,UDP是面向無鏈接的。而後就沒有了~~~

什麼叫面向鏈接,什麼叫無鏈接呢?在互通以前,面向鏈接的協議會先創建鏈接。例如,TCP會三次握手,而UDP不會。那爲何要創建鏈接呢?你TCP三次握手,我UDP也能夠發三個包玩玩,有什麼區別嗎?

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

  1. TCP提供可靠交付。經過TCP鏈接傳輸的數據,無差錯、不丟失、不重複、而且按序到達。咱們都知道IP包是沒有任何可靠性保證的,一旦發出去,都只能隨它去。而UDP繼承了IP包的特性,不保證不丟失,不保證按順序到達。
  2. TCP是面向字節流的。發送的時候發的是一個流,沒頭沒尾。IP包可不是一個流,而是一個個的IP包。之因此變成了流,這也是TCP本身的狀態維護作的事情。而UDP繼承了IP的特性,基於數據報的,一個一個地發,一個一個地收。
  3. TCP是能夠有擁塞控制的。它意識到包丟棄了或者網絡的環境很差了,就會根據狀況調整本身的行爲,看看是否是發快了,要不要發慢點。UDP就不會,應用讓我發,我就發。
  4. TCP實際上是一個有狀態服務,通俗地講就是有腦子的,裏面精確地記着發送了沒有,接收到沒有,發送到哪一個了,應該接收哪一個了,錯一點兒都不行。而UDP則是無狀態服務。通俗地說是沒腦子的,天真無邪的,發出去就發出去了。

UDP包頭是什麼樣的?
咱們本身知道發的是一個UDP的包,而收到的那臺機器咋知道的呢?在IP頭裏面有個8位協議,這裏會存放,數據裏面究竟是TCP仍是UDP,固然這裏是UDP。因而,若是咱們知道UDP頭的格式,就能從數據裏面,將它解析出來。解析出來之後呢?數據給誰處理呢?

處理完傳輸層的事情,內核的事情基本就幹完了,裏面的數據應該交給應用程序本身去處理,但是一臺機器上跑着這麼多的應用程序,應該給誰呢?

不管應用程序寫的使用TCP傳數據,仍是UDP傳數據,都要監聽一個端口。正是這個端口,用來區分應用程序,要不說端口不能衝突呢。兩個應用監聽一個端口,到時候包給誰呀?因此,按理說,不管是TCP仍是UDP包頭裏面應該有端口號,根據端口號,將數據交給相應的應用程序。


UDP包最大長度計算:
從圖中可知:使用16位(2個字節)存儲長度信息,能夠存儲2^16=64k-1=65536-1=65535,自身協議佔用16位+16位+16位+16位=64位=8個字節,UDP包最大長度爲:65535-8=65507 byte

UDP的三大特色:

  1. 溝通簡單,不須要一肚子花花腸子(大量的數據結構、處理邏輯、包頭字段)。前提是它相信網絡世界是美好的,秉承性善論,相信網絡通路默認就是很容易送達的,不容易被丟棄的。
  2. 輕信他人。它不會創建鏈接,雖然有端口號,可是監聽在這個地方,誰均可以傳給他數據,他也能夠傳給任何人數據,甚至能夠同時傳給多我的數據。
  3. 愣頭青,作事不懂權變。不知道何時該堅持,何時該退讓。它不會根據網絡的狀況進行發包的擁塞控制,不管網絡丟包丟成啥樣了,它該怎麼發還怎麼發。

UDP的三大使用場景:

  1. 須要資源少,在網絡狀況比較好的內網,或者對於丟包不敏感的應用
  2. 不須要一對一溝通,創建鏈接,而是能夠廣播的應用。UDP的不面向鏈接的功能,可使得能夠承載廣播或者多播的協議。DHCP就是一種廣播的形式,就是基於UDP協議的。

對於多播,其中D類地址爲多播預留,使用這個地址,能夠將包多播給一批機器。

  1. 須要處理速度快,時延低,能夠容忍少數丟包,可是要求即使網絡擁塞,也絕不退縮,勇往直前的時候。

UDP簡單、處理速度快,不像TCP那樣,操這麼多的心,各類重傳啊,保證順序啊,前面的不收到,後面的無法處理啊。否則等這些事情作完了,時延早就上去了。而TCP在網絡很差出現丟包的時候,擁塞控制策略會主動的退縮,下降發送速度,這就至關於原本環境就差,還自斷臂膀,用戶原本就卡,這下更卡了。

3.2 TCP協議

傳輸控制協議TCP簡介:

  • 面向鏈接的,可靠的,基於字節流的傳輸層通訊協議
  • 將應用層的數據流分割成報文段併發送給目標節點的TCP層
  • 數據報都有序號,對方收到將發送ACK確認,未收到則重傳
  • 使用校驗和校驗數據在傳輸過程當中是否有誤

TCP包頭格式

先來看TCP頭的格式。從這個圖上能夠看出,它比UDP複雜得多。

首先,源端口號和目標端口號是不可少的,這一點和UDP是同樣的。若是沒有這兩個端口號。數據就不知道應該發給哪一個應用。

接下來是包的序號。爲何要給包編號呢?固然是爲了解決亂序的問題。不編好號怎麼確認哪一個應該先來,哪一個應該後到呢。編號是爲了解決亂序問題。

還應該有的就是確認序號。發出去的包應該有確認,要否則我怎麼知道對方有沒有收到呢?若是沒有收到就應該從新發送,直到送達。這個能夠解決不丟包的問題。

TCP是靠譜的協議,可是這不能說明它面臨的網絡環境好。從IP層面來說,若是網絡情況的確那麼差,是沒有任何可靠性保證的,而做爲IP的上一層TCP也無能爲力,惟一能作的就是更加努力,不斷重傳,經過各類算法保證。也就是說,對於TCP來說,IP層你丟不丟包,我管不着,可是我在個人層面上,會努力保證可靠性。

接下來有一些狀態位。例如:URG是緊急指針標誌;SYN是同步序號標誌,用於創建鏈接過程;ACK是回覆,確認序號標誌;PSH是push標誌;RST是從新鏈接;FIN是finish標誌,用於釋放鏈接等。TCP是面向鏈接的,於是雙方要維護鏈接的狀態,這些帶狀態位的包的發送,會引發雙方的狀態變動。

還有一個重要的就是窗口大小。TCP要作流量控制,通訊雙方各聲明一個窗口,標識本身當前可以的處理能力,別發送的太快,撐死我,也別發的太慢,餓死我。

除了作流量控制之外,TCP還會作擁塞控制,對於真正的通路堵車不堵車,它無能爲力,惟一能作的就是控制本身,也即控制發送的速度。不能改變世界,就改變本身嘛。

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

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

TCP的三次握手
握手是爲了創建鏈接,TCP三次握手流程以下:
在TCP/IP協議中,TCP協議提供可靠的鏈接服務,經過三次握手創建鏈接:

  1. 第一次握手:創建鏈接時,服務端發送SYN包(seq=x)到服務器,並進入SYN-SENT狀態,等待服務器確認
  2. 第二次握手:服務器收到SYN包,必須確認客戶的SYN(ack=x+1),同時本身也發送一個SYN包(seq=y),即SYN+ACK包,此時服務器進入SYN-RCVD狀態
  3. 第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=y+1),此包發送完畢,客戶端與服務器進入ESTABLISHED狀態,完成三次握手。


一開始,客戶端和服務端都處於CLOSED狀態。先是服務端主動監聽某個端口,處於LISTEN狀態。而後客戶端主動發起鏈接SYN,以後處於SYN-SENT狀態。服務端收到發起的鏈接,返回SYN,而且ACK客戶端的SYN,以後處於SYN-RCVD狀態。客戶端收到服務端發送的SYN和ACK以後,發送ACK的ACK,以後處於ESTABLISHED狀態,由於它一發一收成功了。服務端收到ACK的ACK以後,處於ESTABLISHED狀態,由於它也一發一收了。

一、爲何要三次,而不是兩次?按說兩我的打招呼,一來一回就能夠了啊?爲了可靠,爲何不是四次?

假設A和B之間通訊,A要發起一個鏈接,當發了第一個請求杳無音信的時候,會有不少的可能性,好比第一個請求包丟了,再如沒有丟,可是繞了彎路,超時了,還有B沒有響應,不想和我鏈接。

A不能確認結果,因而再發,再發。終於,有一個請求包到了B,可是請求包到了B的這個事情,目前A仍是不知道的,A還有可能再發。

B收到了請求包,就知道了A的存在,而且知道A要和它創建鏈接。若是B不樂意創建鏈接,則A會重試一陣後放棄,鏈接創建失敗,沒有問題;若是B是樂意創建鏈接的,則會發送應答包給A。

固然對於B來講,這個應答包也是不知道能不能到達A。這個時候B天然不能認爲鏈接是創建好了,由於應答包仍然會丟,會繞彎路,或者A已經掛了都有可能。於是兩次握手確定不行。

若是A就是不發數據,創建鏈接後空着。在程序設計的時候,能夠要求開啓keepalive機制,即便沒有真實的數據包,也有探活包。

另外,做爲服務端B的程序設計者,對於A這種長時間不發包的客戶端,能夠主動關閉,從而空出資源來給其餘客戶端使用。

三次握手除了雙方創建鏈接外,主要仍是爲了溝通一件事情,就是TCP包的序號的問題。(爲了初始化Sequence Number初始化)

TCP四次揮手

TCP採用四次揮手釋放鏈接:

  1. 第一次揮手:Client發送一個FIN,用來關閉Client到Server的數據傳輸,Client進入FIN_WAIT_1狀態
  2. 第二次揮手:Server收到FIN,發送一個ACK給Client,確認序號爲收到序號+1(與SYN相同,一個FIN佔用一個序號),Server進入CLOSE_WAIT狀態
  3. 第三次揮手:Server發送一個FIN,用來關閉Server到Client的數據傳輸,Server進入LAST_ACK狀態
  4. 第四次揮手:Client收到FIN後,Client進入TIME_WAIT狀態,接着發送一個ACK給Server,確認序號爲收到序號+1,Server進入CLOSE狀態,完成四次揮手。

揮手就是終止鏈接,TCP四次揮手的流程圖:

斷開的時候,咱們能夠看到,當A說「不玩了」,就進入FIN_WAIT_1的狀態,B收到「A不玩」的消息後,發送知道了,就進入CLOSE_WAIT的狀態。

A收到「B說知道了」,就進入FIN_WAIT_2的狀態,若是這個時候B直接跑路,則A將永遠在這個狀態。TCP協議裏面並無對這個狀態的處理,可是Linux有,能夠調整tcp_fin_timeout這個參數,設置一個超時時間。

若是B沒有跑路,發送了「B也不玩了」的請求到達A時,A發送「知道B也不玩了」的ACK後,從FIN_WAIT_2狀態結束,按說A能夠跑路了,可是最後的這個ACK萬一B收不到呢?則B會從新發一個「B不玩了」,這個時候A已經跑路了的話,B就再也收不到ACK了,於是TCP協議要求A最後等待一段時間TIME_WAIT,這個時間要足夠長,長到若是B沒收到ACK的話,「B說不玩了」會重發的,A會從新發一個ACK而且足夠時間到達B。

A直接跑路還有一個問題是,A的端口就直接空出來了,可是B不知道,B原來發過的不少包極可能還在路上,若是A的端口被一個新的應用佔用了,這個新的應用會收到上個鏈接中B發過來的包,雖然序列號是從新生成的,可是這裏要上一個雙保險,防止產生混亂,於是也須要等足夠長的時間,等到原來B發送的全部的包都死翹翹,再空出端口來。

等待的時間設爲2MSL,MSL是Maximum Segment Lifetime,報文最大生存時間,它是任何報文在網絡上存在的最長時間,超過這個時間報文將被丟棄。由於TCP報文基因而IP協議的,而IP頭中有一個TTL域,是IP數據報能夠通過的最大路由數,每通過一個處理他的路由器此值就減1,當此值爲0則數據報將被丟棄,同時發送ICMP報文通知源主機。協議規定MSL爲2分鐘,實際應用中經常使用的是30秒,1分鐘和2分鐘等。

還有一個異常狀況就是,B超過了2MSL的時間,依然沒有收到它發的FIN的ACK,怎麼辦呢?按照TCP的原理,B固然還會重發FIN,這個時候A再收到這個包以後,A就表示,我已經在這裏等了這麼長時間了,已經仁至義盡了,以後的我就都不認了,因而就直接發送RST,B就知道A早就跑了。

一、爲何會有TIME_WAIT狀態?

  • 確保有足夠的時間讓對方收到ACK包
  • 避免新舊鏈接混淆

4 基於TCP和UDP協議的Socket編程

Socket編程進行的是端到端的通訊,每每意識不到中間通過多少局域網,多少路由器,於是可以設置的參數,也只能是端到端協議之上網絡層和傳輸層的。

在網絡層,Socket函數須要指定究竟是IPv4仍是IPv6,分別對應設置爲AF_INET和AF_INET6。另外,還要指定究竟是TCP仍是UDP。還記得我們前面講過的,TCP協議是基於數據流的,因此設置爲SOCK_STREAM,而UDP是基於數據報的,於是設置爲SOCK_DGRAM。

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

TCP的服務端要先監聽一個端口,通常是先調用bind函數,給這個Socket賦予一個IP地址和端口。
爲何須要端口呢?
要知道,你寫的是一個應用程序,當一個網絡包來的時候,內核要經過TCP頭裏面的這個端口,來找到你這個應用程序,把包給你。
爲何要IP地址呢?
有時候,一臺機器會有多個網卡,也就會有多個IP地址,你能夠選擇監聽全部的網卡,也能夠選擇監聽一個網卡,這樣,只有發給這個網卡的包,纔會給你。

當服務端有了IP和端口號,就能夠調用listen函數進行監聽。在TCP的狀態圖裏面,有一個listen狀態,當調用這個函數以後,服務端就進入了這個狀態,這個時候客戶端就能夠發起鏈接了。

在內核中,爲每一個Socket維護兩個隊列。一個是已經創建了鏈接的隊列,這時候鏈接三次握手已經完畢,處於established狀態;一個是尚未徹底創建鏈接的隊列,這個時候三次握手還沒完成,處於syn_rcvd的狀態。

接下來,服務端調用accept函數,拿出一個已經完成的鏈接進行處理。若是尚未完成,就要等着。

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

監聽的Socket和真正用來傳數據的Socket是兩個,一個叫做監聽Socket,一個叫做已鏈接Socket。

鏈接創建成功以後,雙方開始經過read和write函數來讀寫數據,就像往一個文件流裏面寫東西同樣。

這個圖就是基於TCP協議的Socket程序函數調用過程:

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

對於UDP來說,過程有些不同。UDP是沒有鏈接的,因此不須要三次握手,也就不須要調用listen和connect,可是,UDP的的交互仍然須要IP和端口號,於是也須要bind。UDP是沒有維護鏈接狀態的,於是不須要每對鏈接創建一組Socket,而是隻要有一個Socket,就可以和多個客戶端通訊。也正是由於沒有鏈接狀態,每次通訊的時候,都調用sendto和recvfrom,均可以傳入IP地址和端口。

這個圖的內容就是基於UDP協議的Socket程序函數調用過程:

4.3 服務器最大鏈接數

先來算一下理論值,也就是最大鏈接數,系統會用一個四元組來標識一個TCP鏈接。

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

服務器一般固定在某個本地端口上監聽,等待客戶端的鏈接請求。所以,服務端端TCP鏈接四元組中只有對端IP, 也就是客戶端的IP和對端的端口,也即客戶端的端口是可變的,所以,最大TCP鏈接數=客戶端IP數×客戶端端口數。對IPv4,客戶端的IP數最多爲2的32次方,客戶端的端口數最多爲2的16次方,也就是服務端單機最大TCP鏈接數,約爲2的48次方。

固然,服務端最大併發TCP鏈接數遠不能達到理論上限。首先主要是文件描述符限制,Socket都是文件,因此首先要經過ulimit配置文件描述符的數目;另外一個限制是內存,每一個TCP鏈接都要佔用必定內存,操做系統是有限的。

若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)

相關文章
相關標籤/搜索