Go語言IP網路程式設計

2020-07-16 10:05:03
IP 是 Internet 網路層的核心協定,它是一種不可靠的、無連線的通訊協定。TCP、UDP 都是在 IP 的基礎上實現的通訊協定,所以 IP 屬於一種底層協定,它可以直接對網路封包 (Package) 進行處理。另外,通過 IP 使用者還可以實現自己的網路服務協定。本節將詳細講解 IP 網路程式設計伺服器、客戶機的設計原理和設計過程。

IPAddr 地址結構體

在進行 IP 網路程式設計時,伺服器或客戶機的地址使用 IPAddr 地址結構體表示,IPAddr 結構體只有一個欄位 IP,形式如下:

type IPAddr struct {
    IP IP
}

通過了解 IPAddr 地址結構可以發現,IP 網路程式設計屬於一種底層網路程式設計,它可以直接對 IP 包進行處理,所以 IPAddr 地址中沒有埠地址,這個和 TCPAddr 地址結構、UDPAddr 地址結構都不同,在應用時要特別注意。

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

func ResolveIPAddr(net, addr string) (*IPAddr, error)

在呼叫 ResolveIPAddr() 函數時,引數 net 表示網路型別,可以是“ip”、“ip4”或“ip6”,引數 addr 是 IP 地址或域名,如果是 IPv6 地址則必須使用“[]”括起來。

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

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

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

IPConn 物件

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

type IPConn struct {
    //空結構
}

由於 IPConn 是一個無連線的通訊物件,所以 IPConn 物件提供了 ReadFromIP() 方法和 WriteToIP() 方法用於在客戶機和伺服器之間進行資料收發操作。ReadFromIP() 和 WriteToIP() 的原型定義如下:

func (c *IPConn) ReadFromIP(b []byte) (int, *IPAddr, error)
func (c *IPConn) WriteToIP(b []bytez addr *IPAddr) (int, error)

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

IP 伺服器設計

由於工作在網路層,ip 伺服器並不需要在一個指定的埠上和客戶機進行通訊連線,IP 伺服器的工作過程如下:

1) IP 伺服器使用指定的協定簇和協定,呼叫 ListenIP() 函數建立一個 IPConn 連線物件,並在該物件和客戶機間建立不可靠連線。

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

3) 如果通訊結束,伺服器還可以呼叫 Close() 方法關閉 IPConn 連線。

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

func ListenIP(netProto string, laddr *IPAddr) (*IPConn, error)

在呼叫函數 ListenIP() 時,引數 netProto 是“網路型別+協定名”或“網路型別+協定號”,中間用“:”隔開,比如“IP4:IP”或“IP4:4”。引數 laddr 是伺服器本地地址,可以是任意活動的主機地址,或者是內部測試地址“127.0.0.1”。該函數呼叫成功,返回一個 IPConn 物件;呼叫失敗,返回一個錯誤型別。

【範例 1】IP Server 端設計,伺服器使用本地主機地址,呼叫 Hostname() 函數獲取。伺服器設計工作模式採用迴圈伺服器,對每一個連線請求呼叫執行緒 handleClient 來處理。
// IP Server 端設計
package main

import(
    "fmt"
    "net"
    "os"
)
func main() {
    name, err := os.Hostname()
    checkError(err)
    ipAddr, err := net.ResolveIPAddr("ip4", name)
    checkError(err)
    fmt.Println(ipAddr)
    conn, err := net.ListenIP("ip4:ip", ipAddr)
    checkError(err)
    for {
        handleClient(conn)
    }
}
func handleClient(conn *net.IPConn) {
    var buf [512]byte
    n, addr, err := conn.ReadFromIP(buf[0:])
    if err != nil {
        return
    }
    fmt.Println("Receive from client", addr.String(), string(buf[0:n]))
    conn.WriteToIP([]byte("Welcome Client!"), addr)
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

IP 客戶機設計

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

1) IP 客戶機在獲取了伺服器的網路地址之後,可以呼叫 DialIP() 函數向伺服器發出連線請求,如果請求成功會返回 IPConn 物件。

2) 如果連線成功,客戶機可以直接呼叫 IPConn 物件的 ReadFromIP() 方法或 WriteToIP() 方法,與伺服器進行通訊活動。

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

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

func DialIP (netProto string, laddr, raddr *IPAddr) (*IPConn, error)

在呼叫函數 DialIP() 時,引數 netProto 是“網路型別+協定名”或“網路型別+協定號”,中間用“:”隔開,比如“IP4:IP”或“IP4:4”。引數 laddr 是本地主機地址,可以設為 nil。引數 raddr 是對方主機地址,必須指定不能省略。函數呼叫成功後,返回 IPConn 物件;呼叫失敗,返回一個錯誤型別。

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

func (c *IPConn) Close() error

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

【範例 2】IP Client 端設計,客戶機通過內部測試地址“127.0.0.1”和伺服器建立通訊連線,伺服器主機地址可以使用 Hostname() 函數獲取。
// IP 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]
    lAddr, err := net.ResolveIPAddr("ip4", service)
    checkError(err)
    name, err := os.Hostname()
    checkError(err)
    rAddr, err := net.ResolveIPAddr("ip4", name)
    checkError(err)
    conn, err := net.DialIP("ip4:ip", lAddr, rAddr)
    checkError(err)
    _, err = conn.WriteToIP([]byte("Hello Server!"), rAddr)
    checkError(err)
    var buf [512]byte
    n, addr, err := conn.ReadFromIP(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
伺服器響應:Receive from client 127.0.0.1 Hello Server!
客戶機接收:Reply form server 192.168.1.104 Welcome Client!

通過測試結果可以看出,TCP、UDP 的伺服器和客戶機通訊時必須使用埠號,而 IP 伺服器和客戶機之間通訊不需要埠號。另外,如果在同一台計算機上,伺服器、客戶機要使用不同的地址進行測試,比如本例伺服器地址是“192.168.1.104”,客戶機使用內部測試地址“127.0.0.1”。

如果使用相同的地址,會發生自發自收的現象,原因是 IP 是底層通訊,並沒有像 TCP、UDP 那樣使用埠號來區分不同的通訊進程。

Ping 程式設計

不管是 UNIX 還是 Windows 系統中都有一個 Ping 命令,利用它可以檢查網路是否連通,分析判斷網路故障。Ping 會向目標主機傳送測試封包,看對方是否有響應並統計響應時間,以此測試網路。

Ping 命令的這些功能是使用 IP 層的 ICMP 實現的,在測試過程中,源主機向目標主機傳送回顯請求報文(ICMP_ECHO_REQUEST,type = 8, code = 0),目的主機返回回顯響應報文(ICMP_ECHO_REPLY,type = 0, code = 0),相關的封包格式如下圖所示。

ICMP 回顯請求和響應數據包格式
圖:ICMP 回顯請求和響應封包格式