Go語言UDP網路程式設計

2020-07-16 10:05:02
UDP 和上一節《TCP網路程式設計》中的 TCP 一樣,也工作在網路傳輸層,但和 TCP 不同的是,它提供不可靠的通訊服務。UDP 網路程式設計也為 C-S 模式,要設計一個伺服器,一個或多個客戶機。

另外,UDP 是不保證可靠性的通訊協定,所以客戶機和伺服器之間只要建立連線,就可以直接通訊,而不用呼叫 Aceept() 進行連線確認。本節將詳細講解 UDP 網路程式設計伺服器、客戶機的設計原理和設計過程。

UDPAddr 地址結構體

在進行 UDP 網路程式設計時,伺服器或客戶機的地址使用 UDPAddr 地址結構體表示,UDPAddr 包含兩個欄位:IP 和 Port,形式如下:

type UDPAddr struct {
    IP IP
    Port int
}

函數 ResolveUDPAddr() 可以把網路地址轉換為 UDPAddr 地址結構,該函數原型定義如下:

func ResolveUDPAddr(net, addr string) (*UDPAddr, error)

在呼叫函數 ResolveUDPAddr() 時,引數 net 是網路協定名,可以是“udp”、“udp4”或“udp6”。引數 addr 是 IP 地址或域名,如果是 IPv6 地址則必須使用“[]”括起來。另外,埠號以“:”的形式跟隨在 IP 地址或域名的後面,埠是可選的。

函數 ResolveUDPAddr() 呼叫成功後返回一個指向 UDPAddr 結構體的指標,否則返回一個錯誤型別。

另外,UDPAddr 地址物件還有兩個方法:Network() 和 String(),Network() 方法用於返回 UDPAddr 地址物件的網路協定名,比如“udp”;String() 方法可以將 UDPAddr 地址轉換成字串形式。這兩個方法原型定義如下:

func (a *UDPAddr) Network() string
func (a *UDPAddr) String() string

UDPConn 物件

在進行 UDP 網路程式設計時,客戶機和伺服器之間是通過 UDPConn 物件實現連線的,UDPConn 是 Conn 介面的實現。UDPConn 物件系結了伺服器的網路協定和地址資訊。UDPConn 物件定義如下:

type UDPConn struct {
    //空結構
}

通過 UDPConn 連線物件在客戶機和伺服器之間進行通訊,UDP 並不能保證通訊的可靠性和有序性,這些都要由程式設計師來處理。為此,TCPConn 物件提供了 ReadFromUDP() 方法和 WriteToUDP() 方法,這兩個方法直接使用遠端主機地址進行資料傳送和接收,即便在鏈路失效的情況下,通訊操作都能正常進行。

ReadFromUDP() 方法和 WriteToUDP() 方法的原型定義如下:

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

ReadFromUDP() 方法呼叫成功後返回接收位元組數和傳送方地址,否則返回一個錯誤型別;WriteToUDP() 方法呼叫成功後返回傳送位元組數,否則返回一個錯誤型別。

UDP 伺服器設計

在 UDP 網路程式設計中,伺服器工作過程如下:

1) UDP 伺服器首先註冊一個公知埠,然後呼叫 ListenUDP() 函數在這個埠上建立一個 UDPConn 連線物件,並在該物件上和客戶機建立不可靠連線。

2) 如果伺服器和某個客戶機建立了 UDPConn 連線,就可以使用該物件的 ReadFromUDP() 方法和 WriteToUDP() 方法相互通訊了。

3) 不管上一次通訊是否完成或正常,UDP 伺服器依然會接受下一次連線請求。

函數 ListenUDP() 原型定義如下:

func ListenUDP(net sting, laddr *UDPAddr) (*UDPConn, error)

在呼叫函數 ListenUDP() 時,引數 net 是網路協定名,可以是“udp”、“udp4”或“udp6”。引數 laddr 是伺服器本地地址,可以是任意活動的主機地址,或者是內部測試地址“127.0.0.1”。該函數呼叫成功,返回一個 UDPConn 物件;呼叫失敗,返回一個錯誤型別。

【範例 1】UDP Server 端設計,伺服器使用本地地址,伺服器端口號為 5001。伺服器設計工作模式採用迴圈伺服器,對每一個連線請求呼叫執行緒 handleClient 來處理。
//UDP Server 端設計
package main

import(
    "fmt"
    "net"
    "os"
)
func main() {
    service := ":5001"
    udpAddr, err := net.ResolveUDPAddr("udp", service)
    checkError(err)
    conn, err := net.ListenUDP("udp", udpAddr)
    checkError(err)
    for {
        handleClient(conn)
    }
}
func handleClient(conn *net.UDPConn) {
    var buf [512]byte
    n, addr, err := conn.ReadFromUDP(buf[0:])
    if err != nil {
        return
    }
    fmt.Println("Receive from client", addr.String(), string(buf[0:n]))
    conn.WriteToUDP([]byte("Welcome Client!"), addr)
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

UDP 客戶機設計

在 UDP 網路程式設計中,客戶機工作過程如下:

1) UDP 客戶機在獲取了伺服器的伺服器端口號和服務地址之後,可以呼叫 DialUDP() 函數向伺服器發出連線請求,如果請求成功會返回 UDPConn 物件。

2) 客戶機可以直接呼叫 UDPConn 物件的 ReadFromUDP() 方法或 WriteToUDP() 方法,與伺服器進行通訊活動。

3) 通訊完成後,客戶機呼叫 Close() 方法關閉 UDPConn 連線,斷開通訊鏈路。

函數 DialUDP() 原型定義如下:

func DialUDP(net string, laddr, raddr *UDPAddr)(*UDPConn, error)

在呼叫函數 DialUDP() 時,引數 net 是網路協定名,可以是“udp”、“udp4”或“udp6”。引數 laddr 是本地主機地址,可以設為 nil。引數 raddr 是對方主機地址,必須指定不能省略。函數呼叫成功後,返回 UDPConn 物件;呼叫失敗,返回一個錯誤型別。

方法 Close() 的原型定義如下:

func (c *UDPConn) Close() error

該方法呼叫成功後,關閉 UDPConn 連線;呼叫失敗,返回一個錯誤型別。

【範例 2】UDP Client 端設計,客戶機通過內部測試地址“127.0.0.1”和埠 5001 和伺服器建立通訊連線。
// UDP Client端設計
package main

import(
    "fmt"
    "net"
    "os"
)
func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
    }
    service := os.Args[1]
    udpAddr, err := net.ResolveUDPAddr("udp", service)
    checkError(err)
    conn, err := net.DialUDP("udp", nil, udpAddr)
    checkError(err)
    _, err = conn.Write([]byte("Hello server!"))
    checkError(err)
    var buf [512]byte
    n, addr, err := conn.ReadFromUDP(buf[0:])
    checkError(err)
    fmt.Println("Reply form server", addr.String(), string(buf[0:n]))
    conn.Close()
    os.Exit(0)
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}
編譯並執行伺服器端和用戶端,測試過程如下:

啟動伺服器:go run .main.go
客戶機連線:go run .client.go 127.0.0.1:5001
伺服器響應:Receive from client 127.0.0.1:53825 Hello server!
客戶機接收:Reply form server 127.0.0.1:5001 Welcome Client!

通過測試結果會發現,採用 TCP 時必須先啟動伺服器,然後才能正常啟動客戶機,如果伺服器中斷,則客戶機也會異常退出。而採用 UDP 時,客戶機和伺服器啟動沒有先後次序,而且即便是伺服器異常退出,客戶機也能正常工作。

總之,TCP 可以保證客戶機、伺服器雙方按照可靠有序的方式進行通訊,但通訊效率低;而 UDP 雖然不能保證通訊的可靠性,但通訊效率要高得多,在有些場合還是非常有用的。