Golang網絡編程-套接字(socket)篇


            Golang網絡編程-套接字(socket)篇

                               作者:尹正傑

版權聲明:原創作品,謝絕轉載!否則將追究法律責任。

 

 

 

一.網絡概述

1>.什么是協議

  從應用的角度出發,協議可理解為“規則”,是數據傳輸和數據的解釋的規則。假設,A、B雙方欲傳輸文件。規定:
    第一次,傳輸文件名,接收方接收到文件名,應答OK給傳輸方;
    第二次,發送文件的尺寸,接收方接收到該數據再次應答一個OK;
    第三次,傳輸文件內容。同樣,接收方接收數據完成后應答OK表示文件內容接收成功。

  由此,無論A、B之間傳遞何種文件,都是通過三次數據傳輸來完成。A、B之間形成了一個最簡單的數據傳輸規則。雙方都按此規則發送、接收數據。A、B之間達成的這個相互遵守的規則即為協議。

  這種僅在A、B之間被遵守的協議稱之為原始協議。

  當此協議被更多的人采用,不斷的增加、改進、維護、完善。最終形成一個穩定的、完整的文件傳輸協議,被廣泛應用於各種文件傳輸過程中。該協議就成為一個標准協議。最早的ftp協議就是由此衍生而來。

  典型協議:
    應用層:
      常見的協議有HTTP協議,FTP協議。
      HTTP超文本傳輸協議(Hyper Text Transfer Protocol)是互聯網上應用最為廣泛的一種網絡協議。
      FTP文件傳輸協議(File Transfer Protocol)
    傳輸層:
      常見協議有TCP/UDP協議。
      TCP傳輸控制協議(Transmission Control Protocol)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。
      UDP用戶數據報協議(User Datagram Protocol)是OSI參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。
    網絡層:
      常見協議有IP協議、ICMP協議、IGMP協議。
      IP協議是因特網互聯協議(Internet Protocol)
      ICMP協議是Internet控制報文協議(Internet Control Message Protocol)它是TCP/IP協議族的一個子協議,用於在IP主機、路由器之間傳遞控制消息。
      IGMP協議是 Internet 組管理協議(Internet Group Management Protocol),是因特網協議家族中的一個組播協議。該協議運行在主機和組播路由器之間。
    鏈路層:
      常見協議有ARP協議、RARP協議。
      ARP協議是正向地址解析協議(Address Resolution Protocol),通過已知的IP,尋找對應主機的MAC地址。
      RARP是反向地址轉換協議,通過MAC地址確定IP地址。

2>.什么是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,針對於面向連接的TCP服務應用;     數據報式Socket(SOCK_DGRAM):       數據報式Socket是一種無連接的Socket,對應於無連接的UDP服務應用。      溫馨提示:          套接字的內核實現較為復雜,不宜在學習初期深入學習,了解到如下結構足矣。

3>.網絡應用程序設計模式

  C/S模式
    傳統的網絡應用設計模式,客戶機(client)/服務器(server)模式。需要在通訊兩端各自部署客戶機和服務器來完成數據通信。
    優點:
      1>.客戶端位於目標主機上可以保證性能,將數據緩存至客戶端本地,從而提高數據傳輸效率。
      2>.一般來說客戶端和服務器程序由一個開發團隊創作,所以他們之間所采用的協議相對靈活。可以在標准協議的基礎上根據需求裁剪及定制。例如,騰訊所采用的通信協議,即為ftp協議的修改剪裁版。
      因此,傳統的網絡應用程序及較大型的網絡應用程序都首選C/S模式進行開發。如知名的網絡游戲魔獸世界。3D畫面,數據量龐大,使用C/S模式可以提前在本地進行大量數據的緩存處理,從而提高觀感。
    缺點:
      1>.由於客戶端和服務器都需要有一個開發團隊來完成開發。工作量將成倍提升,開發周期較長。
      2>.從用戶角度出發,需要將客戶端安裝至用戶主機上,對用戶主機的安全性構成威脅。這也是很多用戶不願使用C/S模式應用程序的重要原因。   B/S模式     瀏覽器(Browser)/服務器(Server)模式。只需在一端部署服務器,而另外一端使用每台PC都默認配置的瀏覽器即可完成數據的傳輸。     優點:       1>.B/S模式相比C/S模式而言,由於它沒有獨立的客戶端,使用標准瀏覽器作為客戶端,其工作開發量較小。只需開發服務器端即可。
      2>.另外由於其采用瀏覽器顯示數據,因此移植性非常好,不受平台限制。如早期的偷菜游戲,在各個平台上都可以完美運行。     缺點:       1>.B
/S模式的缺點也較明顯。由於使用第三方瀏覽器,因此網絡應用支持受限。
      2>.沒有客戶端放到對方主機上,緩存數據不盡如人意,從而傳輸數據量受到限制。應用的觀感大打折扣。
      3>.必須與瀏覽器一樣,采用標准http協議進行通信,協議選擇不靈活。
  綜上所述,在開發過程中,模式的選擇由上述各自的特點決定。根據實際需求選擇應用程序設計模式。

4>.博主推薦閱讀

  計算機網絡基礎之網絡拓撲介紹:
    https://www.cnblogs.com/yinzhengjie/p/11846279.html

  計算機網絡基礎之OSI參考模型(理論上的標准):
    https://www.cnblogs.com/yinzhengjie/p/11846473.html

  計算機網絡基礎之網絡設備:
    https://www.cnblogs.com/yinzhengjie/p/11853809.html

  計算機網絡基礎之TCP/IP 協議棧(事實上的標准):
    https://www.cnblogs.com/yinzhengjie/p/11854107.html

  計算機網絡基礎之IP地址詳解:
    https://www.cnblogs.com/yinzhengjie/p/11854562.html

 

二.TCP的socket編程實戰案例

1>.簡單C/S模型通信

 

package main

import (
    "fmt"
    "net"
)

func main() {

    /**
    使用Listen函數創建監聽socket,其函數簽名如下:
        func Listen(network, address string) (Listener, error)
    以下是對函數簽名的參數說明:
        network:
            指定服務端socket的協議,如tcp/udp,注意是小寫字母喲~
        address:
            指定服務端監聽的IP地址和端口號,如果不指定地址默認監聽當前服務器所有IP地址喲~
    */
    socket, err := net.Listen("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println("開啟監聽失敗,錯誤原因: ", err)
        return
    }
    defer socket.Close()
    fmt.Println("開啟監聽...")
    for {
        /**
        等待客戶端連接請求
        */
        conn, err := socket.Accept()
        if err != nil {
            fmt.Println("建立鏈接失敗,錯誤原因: ", err)
            return
        }
        defer conn.Close()
        fmt.Println("建立鏈接成功,客戶端地址是: ", conn.RemoteAddr())

        /**
        接收客戶端數據
        */
        buf := make([]byte, 1024)
        conn.Read(buf)
        fmt.Printf("讀取到客戶端的數據為: %s\n", string(buf))

        /**
        發送數據給客戶端
        */
        tmp := "Blog地址:[https://www.cnblogs.com/yinzhengjie/]"
        conn.Write([]byte(tmp))
    }
}
簡單版本服務端代碼
package main

import (
    "fmt"
    "net"
)

func main() {

    /**
    使用Dial函數鏈接服務端,其函數簽名如下所示:
        func Dial(network, address string) (Conn, error)
    以下是對函數簽名的各參數說明:
        network:
            指定客戶端socket的協議,如tcp/udp,該協議應該和需要鏈接服務端的協議一致喲~
        address:
            指定客戶端需要鏈接服務端的socket信息,即指定服務端的IP地址和端口
    */
    conn, err := net.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println("連接服務端出錯,錯誤原因: ", err)
        return
    }
    defer conn.Close()
    fmt.Println("與服務端連接建立成功...")
    /**
    給服務端發送數據
    */
    conn.Write([]byte("服務端,請問博客地址的URL是多少呢?"))

    /**
    獲取服務器的應答
    */
    var buf = make([]byte, 1024)
    conn.Read(buf)
    fmt.Printf("從服務端獲取到的數據為:%s\n", string(buf))
}
簡單版本客戶端代碼
package main

import (
    "fmt"
    "net"
    "strconv"
)

func main() {

    /**
    使用Listen函數創建監聽socket,其函數簽名如下:
        func Listen(network, address string) (Listener, error)
    以下是對函數簽名的參數說明:
        network:
            指定服務端socket的協議,如tcp/udp,注意是小寫字母喲~
        address:
            指定服務端監聽的IP地址和端口號,如果不指定地址默認監聽當前服務器所有IP地址喲~
    */
    socket, err := net.Listen("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println("開啟監聽失敗,錯誤原因: ", err)
        return
    }
    defer socket.Close()
    fmt.Println("開啟監聽...")

    for {
        /**
        等待客戶端連接請求
        */
        conn, err := socket.Accept()
        if err != nil {
            fmt.Println("建立鏈接失敗,錯誤原因: ", err)
            return
        }
        defer conn.Close()
        fmt.Println("建立鏈接成功,客戶端地址是: ", conn.RemoteAddr())

        /**
        分兩次接收客戶端數據:
            第一次最終接收數據的長度;
            第二次根據第一次接受的長度,創建容量大小;
        */
        tmp := make([]byte, 2)

        conn.Read(tmp)
        dataLength, err := strconv.Atoi(string(tmp)) //把字節切片轉換成整型
        if err != nil {
            fmt.Println("獲取數據長度失敗: ", err)
            return
        }
        fmt.Println("獲取到的數據長度是: ", dataLength)

        conn.Write([]byte("已獲取到數據長度"))

        /**
        開始讀取數據
        */
        buf := make([]byte, dataLength)
        conn.Read(buf)
        fmt.Printf("讀取到客戶端的數據為: %s\n", string(buf))

        /**
        發送數據給客戶端
        */
        data := "Blog地址:[https://www.cnblogs.com/yinzhengjie/]"
        conn.Write([]byte(data))
    }
}
簡單版本服務端代碼(優化版)
package main

import (
    "fmt"
    "net"
    "strconv"
)

func main() {

    /**
    使用Dial函數鏈接服務端,其函數簽名如下所示:
        func Dial(network, address string) (Conn, error)
    以下是對函數簽名的各參數說明:
        network:
            指定客戶端socket的協議,如tcp/udp,該協議應該和需要鏈接服務端的協議一致喲~
        address:
            指定客戶端需要鏈接服務端的socket信息,即指定服務端的IP地址和端口
    */
    conn, err := net.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println("連接服務端出錯,錯誤原因: ", err)
        return
    }
    defer conn.Close()
    fmt.Println("與服務端連接建立成功...")

    /**
    定義需要發送的數據,第一次給服務端發送要發的長度
    */
    data := []byte("服務端,請問博客地址的URL是多少呢?")
    lenData := len(data)

    /**
    給服務端發送數據的長度
    */
    conn.Write([]byte(strconv.Itoa(lenData)))

    /**
    獲取服務器的應答
    */
    var buf = make([]byte, 1024)
    conn.Read(buf)
    fmt.Printf("從服務端獲取到的數據為:%s\n", string(buf))

    /**
    第二次給服務器發送數據
    */
    conn.Write(data)
    conn.Read(buf)
    fmt.Printf("獲取到的數據為:%s\n", string(buf))
}
簡單版本客戶端代碼(優化版)

2>.並發C/S模型通信

package main

import (
    "fmt"
    "net"
    "strings"
)

func HandleConn(conn net.Conn) {
    //函數調用完畢,自動關閉conn
    defer conn.Close()

    //獲取客戶端的網絡地址信息
    addr := conn.RemoteAddr().String()
    fmt.Println(addr, " conncet sucessful")

    buf := make([]byte, 2048)

    for {
        //讀取用戶數據
        n, err := conn.Read(buf)
        if err != nil {
            fmt.Println("err = ", err)
            return
        }
        fmt.Printf("[%s]: %s\n", addr, string(buf[:n]))
        fmt.Println("len = ", len(string(buf[:n])))

        //if "exit" == string(buf[:n-1]) {     // nc測試,發送時,只有 \n
        if "exit" == string(buf[:n-2]) { // 自己寫的客戶端測試, 發送時,多了2個字符, "\r\n"
            fmt.Println(addr, " exit")
            return
        }

        //把數據轉換為大寫,再給用戶發送
        conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
    }
}

func main() {

    /**
      使用Listen函數創建監聽socket,其函數簽名如下:
          func Listen(network, address string) (Listener, error)
      以下是對函數簽名的參數說明:
          network:
              指定服務端socket的協議,如tcp/udp,注意是小寫字母喲~
          address:
              指定服務端監聽的IP地址和端口號,如果不指定地址默認監聽當前服務器所有IP地址喲~
    */
    socket, err := net.Listen("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println("開啟監聽失敗,錯誤原因: ", err)
        return
    }
    defer socket.Close()
    fmt.Println("開啟監聽...")

    //接收多個用戶
    for {
        /**
          等待客戶端連接請求
        */
        conn, err := socket.Accept()
        if err != nil {
            fmt.Println("建立鏈接失敗,錯誤原因: ", err)
            return
        }

        //處理用戶請求, 新建一個go程
        go HandleConn(conn)
    }
}
服務端代碼
package main

import (
    "fmt"
    "net"
    "strconv"
)

func main() {

    /**
    使用Dial函數鏈接服務端,其函數簽名如下所示:
        func Dial(network, address string) (Conn, error)
    以下是對函數簽名的各參數說明:
        network:
            指定客戶端socket的協議,如tcp/udp,該協議應該和需要鏈接服務端的協議一致喲~
        address:
            指定客戶端需要鏈接服務端的socket信息,即指定服務端的IP地址和端口
    */
    conn, err := net.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println("連接服務端出錯,錯誤原因: ", err)
        return
    }
    defer conn.Close()
    fmt.Println("與服務端連接建立成功...")

    /**
    定義需要發送的數據,第一次給服務端發送要發的長度
    */
    data := []byte("服務端,請問博客地址的URL是多少呢?")
    lenData := len(data)

    /**
    給服務端發送數據的長度
    */
    conn.Write([]byte(strconv.Itoa(lenData)))

    /**
    獲取服務器的應答
    */
    var buf = make([]byte, 1024)
    conn.Read(buf)
    fmt.Printf("從服務端獲取到的數據為:%s\n", string(buf))

    /**
    第二次給服務器發送數據
    */
    conn.Write(data)
    conn.Read(buf)
    fmt.Printf("獲取到的數據為:%s\n", string(buf))
}
客戶端代碼

 

三.UDP的socket編程實戰案例

1>.UDPTCP的差異概述

TCP和UDP的主要區別如下:
  1>.TCP是面向連接,UDP是面向無連接
    TCP在建立/端口連接時分別要進行三次握手/四次斷開,所以我們說TCP是可靠的連接,而說UDP是不可靠的連接;
  2>.TCP是流式傳輸,可能會出現"粘包"問題,UDP是數據報傳輸,UDP可能會出現"丟包"問題
    "粘包"問題可以通過發送數據包的長度解決
    "丟包"問題可以通過每一個數據報添加標識位
  3>.TCP要求系統資源較多,UDP要求系統資源較少
    TCP需要創建連接再進行通信,所以效率要比UDP慢
  4>.TCP程序結構較復雜,UDP程序結構較簡單
  5>.TCP可以保證數據的准確性,而UDP則不保證數據的准確性

應用場景:
  TCP的應用場景:
    比如文件傳輸,重要數據傳輸等。
  UDP的應用常見:
    比如打電話,直播等.

2>.簡單C/S模型通信

package main

import (
    "fmt"
    "net"
)

func main() {
    /**
    創建監聽的地址,並且指定udp協議
    */
    udp_addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:9999")
    if err != nil {
        fmt.Println("獲取監聽地址失敗,錯誤原因: ", err)
        return
    }

    /**
    創建數據通信socket
    */
    conn, err := net.ListenUDP("udp", udp_addr)
    if err != nil {
        fmt.Println("開啟UDP監聽失敗,錯誤原因: ", err)
        return
    }
    defer conn.Close()

    fmt.Println("開啟監聽...")

    buf := make([]byte, 1024)

    /**
    通過ReadFromUDP可以讀取數據,可以返回如下三個參數:
        dataLength:
            數據的長度
        raddr:
            遠程的客戶端地址
        err:
            錯誤信息
    */
    dataLength, raddr, err := conn.ReadFromUDP(buf)
    if err != nil {
        fmt.Println("獲取客戶端傳遞數據失敗,錯誤原因: ", err)
        return
    }
    fmt.Println("獲取到客戶端的數據為: ", string(buf[:dataLength]))

    /**
    寫回數據
    */
    conn.WriteToUDP([]byte("服務端已經收到數據啦~"), raddr)
}
簡單版本服務端代碼
package main

import (
    "fmt"
    "net"
)

func main() {
    /**
    使用Dial函數鏈接服務端,其函數簽名如下所示:
        func Dial(network, address string) (Conn, error)
    以下是對函數簽名的各參數說明:
        network:
            指定客戶端socket的協議,如tcp/udp,該協議應該和需要鏈接服務端的協議一致喲~
        address:
            指定客戶端需要鏈接服務端的socket信息,即指定服務端的IP地址和端口
    */
    conn, err := net.Dial("udp", "127.0.0.1:9999")
    if err != nil {
        fmt.Println("連接服務端出錯,錯誤原因: ", err)
        return
    }
    defer conn.Close()
    fmt.Println("與服務端連接建立成功...")

    /**
    給服務端發送數據
    */
    conn.Write([]byte("Hi,My name is Jason Yin."))

    /**
    讀取服務端返回的數據
    */
    tmp := make([]byte, 1024)
    n, _ := conn.Read(tmp)
    fmt.Println("獲取到服務器返回的數據為: ", string(tmp[:n]))
}
簡單版本客戶端代碼

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM