Go網絡編程

概述

網絡協議

從應用的角度出發,協議可理解爲「規則」,是數據傳輸和數據的解釋的規則。假設,A、B雙方欲傳輸文件。規定:golang

  • 第一次,傳輸文件名,接收方接收到文件名,應答OK給傳輸方;
  • 第二次,發送文件的尺寸,接收方接收到該數據再次應答一個OK;
  • 第三次,傳輸文件內容。一樣,接收方接收數據完成後應答OK表示文件內容接收成功。

由此,不管A、B之間傳遞何種文件,都是經過三次數據傳輸來完成。A、B之間造成了一個最簡單的數據傳輸規則。雙方都按此規則發送、接收數據。A、B之間達成的這個相互遵照的規則即爲協議。面試

這種僅在A、B之間被遵照的協議稱之爲原始協議。編程

當此協議被更多的人採用,不斷的增長、改進、維護、完善。最終造成一個穩定的、完整的文件傳輸協議,被普遍應用於各類文件傳輸過程當中。該協議就成爲一個標準協議。最先的ftp協議就是由此衍生而來。設計模式

典型協議

應用層: 常見的協議有HTTP協議,FTP協議。瀏覽器

傳輸層: 常見協議有TCP/UDP協議。緩存

網絡層: 常見協議有IP協議、ICMP協議、IGMP協議。安全

網絡接口層: 常見協議有ARP協議、RARP協議。服務器

各個協議用途簡述:微信

IP協議是因特網互聯協議(Internet Protocol)網絡

TCP傳輸控制協議(Transmission Control Protocol)是一種面向鏈接的、可靠的、基於字節流的傳輸層通訊協議。

UDP用戶數據報協議(User Datagram Protocol)是OSI參考模型中一種無鏈接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。

ICMP協議是Internet控制報文協議(Internet Control Message Protocol)它是TCP/IP協議族的一個子協議,用於在IP主機路由器之間傳遞控制消息。

IGMP協議是 Internet 組管理協議(Internet Group Management Protocol),是因特網協議家族中的一個組播協議。該協議運行在主機和組播路由器之間。

ARP協議是正向地址解析協議(Address Resolution Protocol),經過已知的IP,尋找對應主機的MAC地址

RARP是反向地址轉換協議,經過MAC地址肯定IP地址。

分層模型

網絡分層架構

爲了減小協議設計的複雜性,大多數網絡模型均採用分層的方式來組織。每一層都有本身的功能,就像建築物同樣,每一層都靠下一層支持。每一層利用下一層提供的服務來爲上一層提供服務,本層服務的實現細節對上層屏蔽。

造車比喻(協議分層)

減小複雜度,解耦

越下面的層,越靠近硬件;越上面的層,越靠近用戶。至於每一層叫什麼名字,對應編程而言不重要,但面試的時候,面試官可能會問每一層的名字。

業內廣泛的分層方式有兩種。OSI七層模型 和TCP/IP四層模型。能夠經過背誦兩個口訣來快速記憶:

OSI七層模型: 應、表、會、傳、網、數、物

TCP/IP四層模型: 應、傳、網、鏈

物理層: 主要定義物理設備標準,如網線的接口類型、光纖的接口類型、各類傳輸介質的傳輸速率等。它的主要做用是傳輸比特流(就是由一、0轉化爲電流強弱來進行傳輸,到達目的地後再轉化爲一、0,也就是咱們常說的數模轉換與模數轉換)。這一層的數據叫作比特。

數據鏈路層: 定義瞭如何讓格式化數據以幀爲單位進行傳輸,以及如何讓控制對物理介質的訪問。這一層一般還提供錯誤檢測和糾正,以確保數據的可靠傳輸。如:串口通訊中使用到的115200、八、N、1

網絡層: 在位於不一樣地理位置的網絡中的兩個主機系統之間提供鏈接和路徑選擇。Internet的發展使得從世界各站點訪問信息的用戶數大大增長,而網絡層正是管理這種鏈接的層。

傳輸層: 定義了一些傳輸數據的協議和端口號(WWW端口80等),如:TCP(傳輸控制協議,傳輸效率低,可靠性強,用於傳輸可靠性要求高,數據量大的數據),UDP(用戶數據報協議,與TCP特性偏偏相反,用於傳輸可靠性要求不高,數據量小的數據,如QQ聊天數據就是經過這種方式傳輸的)。 主要是將從下層接收的數據進行分段和傳輸,到達目的地址後再進行重組。經常把這一層數據叫作段。

會話層: 經過傳輸層(端口號:傳輸端口與接收端口)創建數據傳輸的通路。主要在你的系統之間發起會話或者接受會話請求(設備之間須要互相認識能夠是IP也能夠是MAC或者是主機名)。

應用層: 是最靠近用戶的OSI層。這一層爲用戶的應用程序(例如電子郵件、文件傳輸和終端仿真)提供網絡服務。

層與協議

每一層都是爲了完成一種功能,爲了實現這些功能,就須要你們都遵照共同的規則。你們都遵照這規則,就叫作「協議」(protocol)。

網絡的每一層,都定義了不少協議。這些協議的總稱,叫「TCP/IP協議」。TCP/IP協議是一個你們族,不只僅只有TCP和IP協議,它還包括其它的協議,以下圖:

協議功能

鏈路層

以太網規定,連入網絡的全部設備,都必須具備「網卡」接口。數據包必須是從一塊網卡,傳送到另外一塊網卡。經過網卡可以使不一樣的計算機之間鏈接,從而完成數據通訊等功能。網卡的地址——MAC 地址,就是數據包的物理髮送地址和物理接收地址。

網卡對應到協議裏面就是與鏈路層ARP協議相關的

每一個網卡有本身惟一的Mac地址

ARP能夠幫助藉助IP獲取Mac地址

RARP能夠藉助Mac地址獲取IP。

網絡層

網絡層的做用是引進一套新的地址,使得咱們可以區分不一樣的計算機是否屬於同一個子網絡。這套地址就叫作「網絡地址」,這是咱們平時所說的IP地址。這個IP地址比如咱們的手機號碼,經過手機號碼能夠獲得用戶所在的歸屬地。

網絡地址幫助咱們肯定計算機所在的子網絡,MAC 地址則將數據包送到該子網絡中的目標網卡。網絡層協議包含的主要信息是源IP和目的IP。

因而,「網絡層」出現之後,每臺計算機有了兩種地址,一種是 MAC 地址,另外一種是網絡地址兩種地址之間沒有任何聯繫,MAC 地址是綁定在網卡上的,網絡地址則是管理員分配的,它們只是隨機組合在一塊兒。

網絡地址幫助咱們肯定計算機所在的子網絡,MAC 地址則將數據包送到該子網絡中的目標網卡。所以,從邏輯上能夠推斷,一定是先處理網絡地址,而後再處理 MAC 地址。

IP地址本質:2進制數。----點分十進制IP地址(string)

傳輸層

當咱們一邊聊QQ,一邊聊微信,當一個數據包從互聯網上發來的時候,咱們怎麼知道,它是來自QQ的內容,仍是來自微信的內容?

也就是說,咱們還須要一個參數,表示這個數據包到底供哪一個程序(進程)使用。這個參數就叫作「端口」(port),它實際上是每個使用網卡的程序的編號。每一個數據包都發到主機的特定端口,因此不一樣的程序就能取到本身所須要的數據。

端口就是在傳輸層指定的。

port -- 在一臺主機上惟一標識一個進程

端口特色:

  • 對於同一個端口,在不一樣系統中對應着不一樣的進程
  • 對於同一個系統,一個端口只能被一個進程擁有

經常使用協議:TCP、UDP

應用層

應用程序收到「傳輸層」的數據,接下來就要進行解讀。因爲互聯網是開放架構,數據來源五花八門,必須事先規定好格式,不然根本沒法解讀。「應用層」的做用,就是規定應用程序的數據格式。

FTP、HTTP、或自定義協議
對數據進行封裝、解封裝

通訊過程

數據通訊過程

封裝: 應用層 ----------------- 傳輸層 ---------------- 網絡層 ----------- 鏈路層

解封裝: 鏈路層 ------------- 網路層 ------------- 傳輸層 ------------ 應用層

Socket編程

什麼是Socket

Socket,英文含義是【插座、插孔】,通常稱之爲套接字,用於描述IP地址和端口。能夠實現不一樣程序間的數據通訊。

Socket起源於Unix,而Unix基本哲學之一就是「一切皆文件」,均可以用「打開open –> 讀寫write/read –> 關閉close」模式來操做。Socket就是該模式的一個實現,網絡的Socket數據傳輸是一種特殊的I/O,Socket也是一種文件描述符。Socket也具備一個相似於打開文件的函數調用:Socket(),該函數返回一個整型的Socket描述符,隨後的鏈接創建、數據傳輸等操做都是經過該Socket實現的。

套接字的內核實現較爲複雜,不宜在學習初期深刻學習,瞭解到以下結構足矣。

在TCP/IP協議中,「IP地址+TCP或UDP端口號」惟一標識網絡通信中的一個進程。「IP地址+端口號」就對應一個socket。欲創建鏈接的兩個進程各自有一個socket來標識,那麼這兩個socket組成的socket pair就惟一標識一個鏈接。所以能夠用Socket來描述網絡鏈接的一對一關係。

經常使用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數據報式Socket(SOCK_DGRAM)。流式是一種面向鏈接的Socket,針對於面向鏈接的TCP服務應用;數據報式Socket是一種無鏈接的Socket,對應於無鏈接的UDP服務應用。

Socket是典型的雙向全雙工

網絡應用程序設計模式

C/S模式

傳統的網絡應用設計模式,客戶機(client)/服務器(server)模式。須要在通信兩端各自部署客戶機和服務器來完成數據通訊。

B/S模式

瀏覽器(Browser)/服務器(Server)模式。只需在一端部署服務器,而另一端使用每臺PC都默認配置的瀏覽器便可完成數據的傳輸。

優缺點

對於C/S模式來講,其優勢明顯。客戶端位於目標主機上能夠保證性能,將數據緩存至客戶端本地,從而提升數據傳輸效率。且,通常來講客戶端和服務器程序由一個開發團隊創做,因此他們之間所採用的協議相對靈活。能夠在標準協議的基礎上根據需求裁剪及定製。例如,騰訊所採用的通訊協議,即爲ftp協議的修改剪裁版。

​ 所以,傳統的網絡應用程序及較大型的網絡應用程序都首選C/S模式進行開發。如,知名的網絡遊戲魔獸世界。3D畫面,數據量龐大,使用C/S模式能夠提早在本地進行大量數據的緩存處理,從而提升觀感。

​ C/S模式的缺點也較突出。因爲客戶端和服務器都須要有一個開發團隊來完成開發。工做量將成倍提高,開發週期較長。另外,從用戶角度出發,須要將客戶端安插至用戶主機上,對用戶主機的安全性構成威脅。這也是不少用戶不肯使用C/S模式應用程序的重要緣由。

​ B/S模式相比C/S模式而言,因爲它沒有獨立的客戶端,使用標準瀏覽器做爲客戶端,其工做開發量較小。只需開發服務器端便可。另外因爲其採用瀏覽器顯示數據,所以移植性很是好,不受平臺限制。如早期的偷菜遊戲,在各個平臺上均可以完美運行。

​ B/S模式的缺點也較明顯。因爲使用第三方瀏覽器,所以網絡應用支持受限。另外,沒有客戶端放到對方主機上,緩存數據不盡如人意,從而傳輸數據量受到限制。應用的觀感大打折扣。第三,必須與瀏覽器同樣,採用標準http協議進行通訊,協議選擇不靈活

​ 所以在開發過程當中,模式的選擇由上述各自的特色決定。根據實際需求選擇應用程序設計模式。

TCP的C/S架構

服務器首先啓動一個net.Listen(),這個net.Listen()從名字上看好像是啓動一個監聽,實際上這是因爲套接字socket最先期設計的緣由,在Go語言設計的時候仍是沿用了Unix當初設計的思想,直接把函數名拿過來了,這個函數初學的同窗都會有一個誤解,認爲它是監聽,實際上它不是,這個listen()函數不是真正的監聽客戶端,要監聽的話監聽什麼?要監聽客戶端和個人鏈接,可是這個Listen不是監聽客戶端,而是我設置服務器監聽的資源(IP、端口),Accept()纔是真正監聽的,那言外之意,監聽嘛,我等着你對我進行訪問吧,那就是說,你沒訪問我以前是否是應該一直處於等待狀態,一下子咱們寫程序看一下,是在Listen()的時候等着仍是在Accept的時候等着,因此Accept是表示接受的意思,當它Accpet調用起來之後,它就等着客戶端和我創建鏈接,比方說,圖示上已經說了,它會阻塞等待用戶創建鏈接,那言外之意,我沒有用戶創建鏈接以前它就一直阻塞在那裏等待着,實際上監聽是在Accept的時候才發起的,固然Accept不是無源之水,它必要Listen設置好了鏈接方式(tcp仍是udp)、IP地址以及端口之後才能阻塞去監聽,當有一個客戶端和服務器發起請求以後,我調Accept()函數完成了,那就說明我服務器和客戶端之間的鏈接創建好了,接來下幹什麼呢?進行數據傳輸,我創建好鏈接的目的就是爲了進行數據傳遞,咱們這裏假定那通常實際上也是這樣,客戶端主動找服務器創建鏈接,鏈接創建好了,客戶端先發送數據給服務器,服務器被動的接受客戶端發來的請求,被動接受客戶端請求數據,接受到了請求之後,服務器進行相應的分析處理,處理完之後把你要請求的數據回寫回去,服務端Read()是讀取客戶端發送過來的請求,Write()是我把你的請求處理完以後再給你寫回去,當這些都作完了,說明咱們跟客戶端的一次通訊就完成了,那這個時候咱們就能夠關閉鏈接。固然若是你還想後續繼續通訊的話,這個close()關閉就要延遲。

客戶端這個流程很簡單,由於服務器先要站出來在那兒等着客戶端和我創建鏈接,因此說,服務器就得先啓動,客戶端至關因而我得等你服務器啓動起來之後你都準備好了,我在給你發送訪問請求,客戶端發送訪問請求,也是調用一個函數,叫作net.Dail()函數,這個Dail()函數會對阻塞的Accept()發送一個請求,若是服務器準備好,Accept()返回的時候,Dail也返回,我們就說客戶端和服務器創建好了鏈接,客戶端先發送數據,因此客戶端先是一個寫操做,發送完數據,服務器那邊讀到客戶端請求進行處理,處理完以後寫回來,客戶端再Read()讀取服務器寫回來的數據,讀完之後客戶端也能夠作簡單處理,比方說我讀到了之後,打印顯示,完成了寫,完成了讀,一次跟網絡端的通訊也就完成了,客戶端能夠關閉鏈接,大體的流程就是這樣。

前面說過,socket通訊,既然要通訊,至少得是一對,如上圖所示,Accpet()和Dail()成功後都會返回一個socket。

其實Listen()的時候也會建立一個socket,可是這個socket不是用於通訊的,只是建立用於通訊的socket,綁定IP地址和端口設置監聽的

簡單的C/S模型通訊

Server端

Listen函數:

network: 選用的協議:TCP、UDP 如: "tcp"或"udp"

address:IP地址+端口號 如: "127.0.0.1:8000"或":8000"

Listener接口:

Conn接口:

參看 https://studygolang.com/pkgdoc 中文幫助文檔中的demo:

TCP服務器端代碼:

package main

import (
    "fmt"
    "net"
)

func main() {
    //指定服務器 通訊協議,IP地址,port. 建立一個用戶監聽的socket
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("服務器設置監聽失敗,緣由是:%v\n", err)
        return
    }
    defer listener.Close()

    fmt.Println("服務器等待客戶端創建鏈接...")
    //阻塞監聽客戶端鏈接請求,成功創建鏈接,返回用於通訊的socket
    conn, err := listener.Accept()
    if err != nil {
        fmt.Printf("服務器監聽失敗,緣由是:%v\n", err)
    }
    defer conn.Close()

    fmt.Println("服務器與客戶端成功創建鏈接!!!")
    //讀取客戶端發送的數據
    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Printf("Conn Read()錯誤,緣由是:%v\n", err)
    }

    //處理數據 -- 打印
    fmt.Println("服務器讀到數據:", string(buf[:n]))
}

運行代碼

利用nc模式客戶端請求

再次查看運行的終端,能夠發現已經成功創建了鏈接

咱們用nc鏈接後,還能夠發送數據

再次查看運行的終端

如圖,在整個通訊過程當中,服務器端有兩個socket參與進來,但用於通訊的只有 conn 這個socket。它是由 listener建立的。隸屬於服務器端。

Client端

Dial函數:

network: 選用的協議:TCP、UDP 如: "tcp"或"udp"

address:IP地址+端口號 如: "127.0.0.1:8000"或":8000"

Conn接口:

客戶端代碼:

package main

import (
    "fmt"
    "net"
)

func main() {

    //指定服務器IP+port建立 通訊套接字
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Dial err:%v\n", err)
        return
    }
    defer conn.Close()

    //主動寫數據給服務器
    _, err = conn.Write([]byte("Are you ready?"))
    if err != nil {
        fmt.Printf("conn.Write err:%v\n", err)
        return
    }

    buf := make([]byte, 1024)
    //接受服務器回發的數據
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Printf("conn.Read err:%v\n", err)
        return
    }

    fmt.Printf("服務器回發的數據爲:%v\n", string(buf[:n]))
}

因爲咱們想要服務器端回寫內容,因此須要修改一下以前的服務端代碼

更新服務器端代碼:

package main

import (
    "fmt"
    "net"
)

func main() {
    //指定服務器 通訊協議,IP地址,port. 建立一個用戶監聽的socket
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("服務器設置監聽失敗,緣由是:%v\n", err)
        return
    }
    defer listener.Close()

    fmt.Println("服務器等待客戶端創建鏈接...")
    //阻塞監聽客戶端鏈接請求,成功創建鏈接,返回用於通訊的socket
    conn, err := listener.Accept()
    if err != nil {
        fmt.Printf("服務器監聽失敗,緣由是:%v\n", err)
    }
    defer conn.Close()

    fmt.Println("服務器與客戶端成功創建鏈接!!!")
    //讀取客戶端發送的數據
    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Printf("Conn Read()錯誤,緣由是:%v\n", err)
    }

    //處理數據 -- 回寫給客戶端
    if string(buf[:n]) == "Are you ready?" {
        conn.Write([]byte("I am ready!!!"))
    } else {
        conn.Write([]byte("I don`t know what you say!!!"))
    }
}

併發的C/S模型通訊

併發Server

如今已經完成了客戶端與服務端的通訊,可是服務端只能接收一個用戶發送過來的數據,怎樣接收多個客戶端發送過來的數據,實現一個高效的併發服務器呢?

Accept()函數的做用是等待客戶端的連接,若是客戶端沒有連接,該方法會阻塞。若是有客戶端連接,那麼該方法返回一個Socket負責與客戶端進行通訊。因此,每來一個客戶端,該方法就應該返回一個Socket與其通訊,所以,可使用一個死循環,將Accept()調用過程包裹起來。

須要注意的是,實現併發處理多個客戶端數據的服務器,就須要針對每個客戶端鏈接,單獨產生一個Socket,並建立一個單獨的goroutine與之完成通訊。

func main() {

    //建立監聽套接字
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Listen() err:%v\n", err)
        return
    }
    defer listener.Close()

    //監聽客戶端鏈接請求
    for {
        fmt.Println("服務器等待客戶端鏈接...")
        conn, err := listener.Accept()
        if err != nil {
            fmt.Printf("listener.Accept() err:%v\n", err)
            return
        }
        //具體完成服務器和客戶端的數據通訊
        go HandleConnect(conn)
    }
}

將客戶端的數據處理工做封裝到HandleConn方法中,需將Accept()返回的Socket傳遞給該方法,變量conn的類型爲:net.Conn。可使用conn.RemoteAddr()來獲取成功與服務器創建鏈接的客戶端IP地址和端口號:

客戶端可能持續不斷的發送數據,所以接收數據的過程能夠放在for循環中,服務端也持續不斷的向客戶端返回處理後的數據。

func HandleConnect(conn net.Conn) {
    defer conn.Close()
    //獲取鏈接的客戶端 Addr

    addr := conn.RemoteAddr()
    fmt.Println(addr, "客戶端成功鏈接!")
    //循環讀取客戶端發送數據
    buf := make([]byte, 1024)
    for {
        //注意,read讀取時,會將命令行裏的換行符也給讀取了,在*Unix上換行符是\n,在Windows上時\r\n
        n, err := conn.Read(buf)
        if err != nil {
            if err == io.EOF {
                fmt.Println("客戶端退出了!!!")
                break
            } else {
                fmt.Printf("conn.Read() err:%v\n", err)
                return
            }
        }
        fmt.Printf("服務器讀到數據:%v", string(buf[:n]))
        //小寫轉大寫,回發給客戶端
        conn.Write(bytes.ToUpper(buf[:n]))
    }
}

併發Client

客戶端不只須要持續的向服務端發送數據,同時也要接收從服務端返回的數據。所以可將發送和接收放到不一樣的協程中。

主協程循環接收服務器回發的數據(該數據應已轉換爲大寫),並打印至屏幕;子協程循環從鍵盤讀取用戶輸入數據,寫給服務器。讀取鍵盤輸入可以使用 os.Stdin.Read(str)。定義切片str,將讀到的數據保存至str中。

這樣,客戶端也實現了多任務。

客戶端代碼:

package main

import (
    "fmt"
    "io"
    "net"
    "os"
)

func main() {

    //發起鏈接請求
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Dial() err:%v\n", err)
        return
    }
    defer conn.Close()

    //獲取用戶鍵盤輸入(os.Stdin),並將輸入數據發送給服務器
    go func() {
        str := make([]byte, 1024)
        for {
            n, err := os.Stdin.Read(str)
            if err != nil {
                fmt.Printf("os.Stdin.Read() err:%v\n", err)
                continue
            }
            //寫給服務器,讀多少,寫多少
            conn.Write(str[:n])
        }
    }()

    for {
        buf := make([]byte, 1024)
        n, err := conn.Read(buf)
        if err != nil {
            if err == io.EOF {
                fmt.Println("服務端退出了!!!")
                return
            } else {
                fmt.Printf("conn.Read() err:%v\n", err)
                continue
            }
        }
        fmt.Printf("客戶端讀到服務器回發數據:%s",buf[:n])
    }
}

TCP通訊

下圖是一次TCP通信的時序圖。TCP鏈接創建斷開。包含你們熟知的三次握手和四次揮手。

在這個例子中,首先客戶端主動發起鏈接、發送請求,而後服務器端響應請求,而後客戶端主動關閉鏈接。兩條豎線表示通信的兩端,從上到下表示時間的前後順序。注意,數據從一端傳到網絡的另外一端也須要時間,因此圖中的箭頭都是斜的。

三次握手

所謂三次握手(Three-Way Handshake)即創建TCP鏈接,就是指創建一個TCP鏈接時,須要客戶端和服務端總共發送3個包以確認鏈接的創建。比如兩我的在打電話:

Client: "喂,你能聽獲得嗎?"

Server: "我聽獲得,你聽獲得我嗎?"

Client: "我能聽到你,今天balabala..."

創建鏈接(三次握手)的過程:

  1. 客戶端發送一個帶SYN標誌的TCP報文到服務器。這是上圖中三次握手過程當中的段1。客戶端發出SYN位表示鏈接請求。序號是1000,這個序號在網絡通信中用做臨時的地址,每發一個數據字節,這個序號要加1,這樣在接收端能夠根據序號排出數據包的正確順序,也能夠發現丟包的狀況。
    另外,規定SYN位和FIN位也要佔一個序號,此次雖然沒發數據,可是因爲發了SYN位,所以下次再發送應該用序號1001。
    mss表示最大段尺寸,若是一個段太大,封裝成幀後超過了鏈路層的最大長度,就必須在IP層分片,爲了不這種狀況,客戶端聲明本身的最大段尺寸,建議服務器端發來的段不要超過這個長度。
  2. 服務器端迴應客戶端,是三次握手中的第2個報文段,同時帶ACK標誌和SYN標誌。表示對剛纔客戶端SYN的迴應;同時又發送SYN給客戶端,詢問客戶端是否準備好進行數據通信。
    服務器發出段2,也帶有SYN位,同時置ACK位表示確認,確認序號是1001,表示「我接收到序號1000及其之前全部的段,請你下次發送序號爲1001的段」,也就是應答了客戶端的鏈接請求,同時也給客戶端發出一個鏈接請求,同時聲明最大尺寸爲1024。
  3. 客戶必須再次迴應服務器端一個ACK報文,這是報文段3。
    客戶端發出段3,對服務器的鏈接請求進行應答,確認序號是8001。在這個過程當中,客戶端和服務器分別給對方發了鏈接請求,也應答了對方的鏈接請求,其中服務器的請求和應答在一個段中發出。

所以一共有三個段用於創建鏈接,稱爲「三方握手」。在創建鏈接的同時,雙方協商了一些信息,例如,雙方發送序號的初始值、最大段尺寸等。

數據傳輸的過程:

  1. 客戶端發出段4,包含從序號1001開始的20個字節數據。
  2. 服務器發出段5,確認序號爲1021,對序號爲1001-1020的數據表示確認收到,同時請求發送序號1021開始的數據,服務器在應答的同時也向客戶端發送從序號8001開始的10個字節數據。
  3. 客戶端發出段6,對服務器發來的序號爲8001-8010的數據表示確認收到,請求發送序號8011開始的數據。

在數據傳輸過程當中,ACK和確認序號是很是重要的,應用程序交給TCP協議發送的數據會暫存在TCP層的發送緩衝區中,發出數據包給對方以後,只有收到對方應答的ACK段才知道該數據包確實發到了對方,能夠從發送緩衝區中釋放掉了,若是由於網絡故障丟失了數據包或者丟失了對方發回的ACK段,通過等待超時後TCP協議自動將發送緩衝區中的數據包重發。

四次揮手

所謂四次揮手(Four-Way-Wavehand)即終止TCP鏈接,就是指斷開一個TCP鏈接時,須要客戶端和服務端總共發送4個包以確認鏈接的斷開。在socket編程中,這一過程由客戶端或服務器任一方執行close來觸發。比如兩我的打完電話要掛斷:

Client: "我要說的事情都說完了,我沒事了。掛啦?"

Server: "等下,我還有一個事兒。Balabala…"

Server: "好了,我沒事兒了。掛了啊。"

Client: "ok!拜拜"

關閉鏈接(四次握手)的過程:

因爲TCP鏈接是全雙工的,所以每一個方向都必須單獨進行關閉。這原則是當一方完成它的數據發送任務後就能發送一個FIN來終止這個方向的鏈接。收到一個 FIN只意味着這一方向上沒有數據流動,一個TCP鏈接在收到一個FIN後仍能發送數據。首先進行關閉的一方將執行主動關閉,而另外一方執行被動關閉。

  1. 客戶端發出段7,FIN位表示關閉鏈接的請求。
  2. 服務器發出段8,應答客戶端的關閉鏈接請求。
  3. 服務器發出段9,其中也包含FIN位,向客戶端發送關閉鏈接請求。
  4. 客戶端發出段10,應答服務器的關閉鏈接請求。

創建鏈接的過程是三次握手,而關閉鏈接一般須要4個段,服務器的應答和關閉鏈接請求一般不合並在一個段中,由於有鏈接半關閉的狀況,這種狀況下客戶端關閉鏈接以後就不能再發送數據給服務器了,可是服務器還能夠發送數據給客戶端,直到服務器也關閉鏈接爲止。

下圖是TCP狀態轉換圖

UDP通訊

在以前的案例中,咱們一直使用的是TCP協議來編寫Socket的客戶端與服務端。其實也可使用UDP協議來編寫Socket的客戶端與服務端。

UDP服務器

因爲UDP是「無鏈接」的,因此,服務器端不須要額外建立監聽套接字,只須要指定好IP和port,而後監聽該地址,等待客戶端與之創建鏈接,便可通訊。

  1. 建立監聽地址

    func ResolveUDPAddr(network, address string) (*UDPAddr, error)

    ResolveUDPAddr將addr做爲UDP地址解析並返回。參數addr格式爲"host:port"或"[ipv6-host%zone]:port",解析獲得網絡名和端口名;net必須是"udp"、"udp4"或"udp6"。
    IPv6地址字面值/名稱必須用方括號包起來,如"[::1]:80"、"[ipv6-host]:http"或"[ipv6-host%zone]:80"。

  2. 建立用於通訊的socket

    func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)

    ListenUDP建立一個接收目的地是本地地址laddr的UDP數據包的網絡鏈接。net必須是"udp"、"udp4"、"udp6";若是laddr端口爲0,函數將選擇一個當前可用的端口,能夠用Listener的Addr方法得到該端口。返回的*UDPConn的ReadFrom和WriteTo方法能夠用來發送和接收UDP數據包(每一個包均可得到來源地址或設置目標地址)。

  3. 接受UDP數據

    func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)

    ReadFromUDP從c讀取一個UDP數據包,將有效負載拷貝到b,返回拷貝字節數和數據包來源地址。

    ReadFromUDP方法會在超過一個固定的時間點以後超時,並返回一個錯誤。

  4. 寫出數據到UDP

    func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)

    WriteToUDP經過c向地址addr發送一個數據包,b爲包的有效負載,返回寫入的字節。

    WriteToUDP方法會在超過一個固定的時間點以後超時,並返回一個錯誤。在面向數據包的鏈接上,寫入超時是十分罕見的。

服務器端代碼:

package main

import (
    "fmt"
    "net"
)

func main() {

    //0.本應從步驟1開始,可是在寫步驟1的時候發現,步驟1還須要*UDPAddr類型的參數,因此須要先建立一個*DUPAddr
    //組織一個udp地址結構,指定服務器的IP+port
    udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.ResolveUDPAddr()函數執行出錯,錯誤爲:%v\n", err)
        return
    }
    fmt.Printf("UDP服務器地址結構建立完成!!!\n")

    //1.建立用戶通訊的socket
    //因爲ListenUDP須要一個*UDPAddr類型的參數,因此咱們還須要先建立一個監聽地址
    udpConn, err := net.ListenUDP("udp", udpAddr)
    if err != nil {
        fmt.Printf("net.ListenUDP()函數執行出錯,錯誤爲:%v\n", err)
        return
    }
    defer udpConn.Close()
    fmt.Printf("UDP服務器通訊socket建立完成!!!\n")

    //2.讀取客戶端發送的數據(阻塞發生在ReadFromUDP()方法中)
    buf := make([]byte, 4096)
    //ReadFromUDP()方法返回三個值,分別是讀取到的字節數,客戶端的地址,error
    n, clientUDPAddr, err := udpConn.ReadFromUDP(buf)
    if err != nil {
        fmt.Printf("*UDPAddr.ReadFromUDP()方法執行出錯,錯誤爲:%v\n", err)
        return
    }
    //3.模擬處理數據
    fmt.Printf("服務器讀到%v的數據:%s",clientUDPAddr, buf[:n])

    //4.回寫數據給客戶端
    _, err = udpConn.WriteToUDP([]byte("I am OK!"), clientUDPAddr)
    if err != nil {
        fmt.Printf("*UDPAddr.WriteToUDP()方法執行出錯,錯誤爲:%v\n", err)
        return
    }
}

運行代碼

經過nc請求測試

服務端讀取請求數據,並回寫"I am OK!"

UDP客戶端

udp客戶端的編寫與TCP客戶端的編寫,基本上是同樣的,只是將協議換成udp.代碼以下:

package main

import (
    "fmt"
    "net"
)

func main() {
    conn, err := net.Dial("udp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Dial()函數執行出錯,錯誤爲:%v\n", err)
        return
    }
    defer conn.Close()

    conn.Write([]byte("hello, I`m a client in UDP!"))

    buf := make([]byte, 4096)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Printf("Conn.Read()方法執行出錯,錯誤爲:%v\n", err)
        return
    }
    fmt.Printf("服務器發來數據:%s\n", buf[:n])
}

併發

其實對於UDP而言,服務器不須要併發,只要循環處理客戶端數據便可。客戶端也等同於TCP通訊併發的客戶端。

服務器:

package main

import (
    "fmt"
    "net"
)

func main() {

    //0.本應從步驟1開始,可是在寫步驟1的時候發現,步驟1還須要*UDPAddr類型的參數,因此須要先建立一個*DUPAddr
    //組織一個udp地址結構,指定服務器的IP+port
    udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.ResolveUDPAddr()函數執行出錯,錯誤爲:%v\n", err)
        return
    }
    fmt.Printf("UDP服務器地址結構建立完成!!!\n")

    //1.建立用戶通訊的socket
    //因爲ListenUDP須要一個*UDPAddr類型的參數,因此咱們還須要先建立一個監聽地址
    udpConn, err := net.ListenUDP("udp", udpAddr)
    if err != nil {
        fmt.Printf("net.ListenUDP()函數執行出錯,錯誤爲:%v\n", err)
        return
    }
    defer udpConn.Close()
    fmt.Printf("UDP服務器通訊socket建立完成!!!\n")

    for {
        //2.讀取客戶端發送的數據(阻塞發生在ReadFromUDP()方法中)
        buf := make([]byte, 4096)
        //ReadFromUDP()方法返回三個值,分別是讀取到的字節數,客戶端的地址,error
        n, clientUDPAddr, err := udpConn.ReadFromUDP(buf)
        if err != nil {
            fmt.Printf("*UDPAddr.ReadFromUDP()方法執行出錯,錯誤爲:%v\n", err)
            continue
        }
        //3.模擬處理數據
        fmt.Printf("服務器讀到%v的數據:%s\n",clientUDPAddr, buf[:n])

        //4.回寫數據給客戶端
        _, err = udpConn.WriteToUDP([]byte("I am OK!"), clientUDPAddr)
        if err != nil {
            fmt.Printf("*UDPAddr.WriteToUDP()方法執行出錯,錯誤爲:%v\n", err)
            continue
        }
    }
}

客戶端:

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    conn, err := net.Dial("udp", "127.0.0.1:8000")
    if err != nil {
        fmt.Printf("net.Dial()函數執行出錯,錯誤爲:%v\n", err)
        return
    }
    defer conn.Close()

    go func() {
        buf := make([]byte, 4096)
        for {
            //從鍵盤讀取內容,放入buf
            n, err := os.Stdin.Read(buf)
            if err != nil {
                fmt.Printf("os.Stdin.Read()執行出錯,錯誤爲:%v\n", err)
                return
            }
            //給服務器發送
            conn.Write(buf[:n])
        }
    }()
    for {
        buf := make([]byte, 4096)
        n, err := conn.Read(buf)
        if err != nil {
            fmt.Printf("Conn.Read()方法執行出錯,錯誤爲:%v\n", err)
            return
        }
        fmt.Printf("服務器發來數據:%s\n", buf[:n])
    }
}

UDP與TCP的差別

TCP UDP
面向鏈接 面向無鏈接
要求系統資源較多 要求系統資源較少
TCP程序結構複雜 UDP程序結構比較簡單
使用流式 使用數據包式
保證數據準確性 不保證數據準確性
保證數據順序 不保證數據順序
通信速度較慢 通信速度較快

使用場景

TCP: 對數據傳輸安全性、穩定性要求高的場合。網絡文件傳輸。下載、上傳。

UDP: 對數據實時傳輸要求較高的場合。視頻直播、在線電話會議。遊戲

相關文章:

http請求是如何先創建的三次握手?

相關文章
相關標籤/搜索