深入Go UDP編程

nhuc7871 8年前發布 | 16K 次閱讀 UDP Socket 操作系統 Google Go/Golang開發

用戶數據報協議(User Datagram Protocol,縮寫為UDP),又稱用戶數據報文協議,是一個簡單的面向數據報(package-oriented)的傳輸層協議,正式規范為 RFC 768 。UDP只提供數據的不可靠傳遞,它一旦把應用程序發給網絡層的數據發送出去,就不保留數據備份(所以UDP有時候也被認為是不可靠的數據報協議)。UDP在IP數據報的頭部僅僅加入了復用和數據校驗。

由于缺乏可靠性且屬于非連接導向協議,UDP應用一般必須允許一定量的丟包、出錯和復制粘貼。但有些應用,比如TFTP,如果需要則必須在應用層增加根本的可靠機制。但是絕大多數UDP應用都不需要可靠機制,甚至可能因為引入可靠機制而降低性能。流媒體(流技術)、即時多媒體游戲和IP電話(VoIP)一定就是典型的UDP應用。如果某個應用需要很高的可靠性,那么可以用傳輸控制協議(TCP協議)來代替UDP。

由于缺乏擁塞控制(congestion control),需要基于網絡的機制來減少因失控和高速UDP流量負荷而導致的擁塞崩潰效應。換句話說,因為UDP發送者不能夠檢測擁塞,所以像使用包隊列和丟棄技術的路由器這樣的網絡基本設備往往就成為降低UDP過大通信量的有效工具。數據報擁塞控制協議(DCCP)設計成通過在諸如流媒體類型的高速率UDP流中,增加主機擁塞控制,來減小這個潛在的問題。

典型網絡上的眾多使用UDP協議的關鍵應用一定程度上是相似的。這些應用包括域名系統(DNS)、簡單網絡管理協議(SNMP)、動態主機配置協議(DHCP)、路由信息協議(RIP)和某些影音流服務等等。

UDP報頭

偏移 字節 0 1 2 3
字節  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 0 來源連接端口 目的連接端口
4 32 報長 檢查碼

IPv4偽頭部

0 – 7 8 – 15 16 – 23 24 – 31
0 來源地址
32 目的地址
64 全零 協議名 UDP報文長度
96 來源連接端口 目的連接端口
128 報文長度 檢驗和
160+ 數據

IPv6偽頭部

0 – 7 8 – 15 16 – 23 24 – 31
0 來源地址
32
64
96
128 目的地址
160
192
224
256 UDP報文長
288 全零 下一個指針位置
320 來源連接端口 目的連接端口
352 報文長 校驗和
384+ 數據

以上大段的背景介紹引自維基百科。

而TCP是面向連接(connection-oriented)的協議,可以提供可靠的數據傳輸。

本文講介紹Go語言的UDP庫及其使用方法,以及了解使用過程中的細節和陷阱。

一個簡單的例子

首先看一個簡單的UDP的例子,這個例子演示了Go UDP通過 Dial 方式發送數據報的例子。

package main

import (
"fmt"
"net"
)

func main() {
 listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9981})
 if err != nil {
 fmt.Println(err)
 return
 }

 fmt.Printf("Local: <%s> \n", listener.LocalAddr().String())

 data := make([]byte, 1024)
 for {
 n, remoteAddr, err := listener.ReadFromUDP(data)
 if err != nil {
 fmt.Printf("error during read: %s", err)
 }

 fmt.Printf("<%s> %s\n", remoteAddr, data[:n])

 _, err = listener.WriteToUDP([]byte("world"), remoteAddr)

 if err != nil {
 fmt.Printf(err.Error())
 }
 }
}
packagemain

import(
"fmt"
"net"
)

funcmain() {
 sip := net.ParseIP("127.0.0.1")

 srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port:0}
 dstAddr := &net.UDPAddr{IP: ip, Port:9981}

 conn, err := net.DialUDP("udp", srcAddr, dstAddr)
iferr !=nil{
 fmt.Println(err)
 }
deferconn.Close()

 conn.Write([]byte("hello"))

 fmt.Printf("<%s>\n", conn.RemoteAddr())
}

可以看到, Go UDP的處理類似TCP的處理,雖然不像TCP面向連接的方式 ListenTCP 和 Accept 的方式建立連接,但是它通過 ListenUDP 和 ReadFromUDP 可以接收各個客戶端發送的數據報,并通過 WriteToUDP 寫數據給特定的客戶端。

我們稍微修改一下client1.go,讓它保持UDP Socket文件一直打開:

funcmain() {
 ip := net.ParseIP("127.0.0.1")

 srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port:0}
 dstAddr := &net.UDPAddr{IP: ip, Port:9981}

 conn, err := net.DialUDP("udp", srcAddr, dstAddr)
iferr !=nil{
 fmt.Println(err)
 }
deferconn.Close()

 b := make([]byte,1)
 os.Stdin.Read(b)

 conn.Write([]byte("hello"))

 fmt.Printf("<%s>\n", conn.RemoteAddr())
}

使用 netstat 可以看到這個網絡文件描述符(因為我在同一臺機器上運行服務器,所以你會看到兩條記錄,一個是服務器打開的,一個是客戶端打開的)。

udp4 00localhost.54676localhost.9981
udp4 00localhost.9981*.*

或者使用 lsof 命令查看:

server1 59312smallnest3u IPv40xad793a9a54467f610t0 UDP localhost:9981
client1 59323smallnest3u IPv40xad793a9a544681c10t0 UDP localhost:54676->localhost:9981

更復雜的例子

我們還可以將上面的例子演化一下,實現雙向的讀寫。

服務器端代碼不用修改,因為它已經實現了讀寫,讀是通過 listener.ReadFromUDP ,寫通過 listener.WriteToUDP 。

客戶端修改為讀寫:

packagemain

import(
"fmt"
"net"
)

funcmain() {
 ip := net.ParseIP("127.0.0.1")

 srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port:0}
 dstAddr := &net.UDPAddr{IP: ip, Port:9981}

 conn, err := net.DialUDP("udp", srcAddr, dstAddr)
iferr !=nil{
 fmt.Println(err)
 }
deferconn.Close()

 conn.Write([]byte("hello"))

 data := make([]byte,1024)
 n, err := conn.Read(data)

 fmt.Printf("read %s from <%s>\n", data[:n], conn.RemoteAddr())
}

這里client的寫是 Write ,讀是 Read 。

等價的客戶端和服務器

下面這個是兩個服務器通信的例子,互為客戶端和服務器,在發送數據報的時候,我們可以將發送的一方稱之為源地址,發送的目的地一方稱之為目標地址。

packagemain

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

funcread(conn *net.UDPConn) {
for{
 data := make([]byte,1024)
 n, remoteAddr, err := conn.ReadFromUDP(data)
iferr !=nil{
 fmt.Printf("error during read: %s", err)
 }

 fmt.Printf("receive %s from <%s>\n", data[:n], remoteAddr)
 }
}

funcmain() {
 addr1 := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port:9981}
 addr2 := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port:9982}

gofunc() {
 listener1, err := net.ListenUDP("udp", addr1)
iferr !=nil{
 fmt.Println(err)
return
 }

goread(listener1)

 time.Sleep(5* time.Second)

 listener1.WriteToUDP([]byte("ping to #2: "+addr2.String()), addr2)
 }()

gofunc() {
 listener1, err := net.ListenUDP("udp", addr2)
iferr !=nil{
 fmt.Println(err)
return
 }
goread(listener1)
 time.Sleep(5* time.Second)

 listener1.WriteToUDP([]byte("ping to #1: "+addr1.String()), addr1)

 }()

 b := make([]byte,1)
 os.Stdin.Read(b)
}

Read和Write方法集的比較

前面的例子中客戶端有時使用 DialUDP 建立數據報的源對象和目標對象(地址和端口), 它會創建UDP Socket文件描述符,然后調用內部的 connect 為這個文件描述符設置源地址和目標地址,這時Go將它稱之為 connected ,盡管我們知道UDP是無連接的協議,Go這種叫法我想根源來自Unix/Linux的UDP的實現。這個方法返回 *UDPConn 。

有的時候卻可以通過 ListenUDP 返回的 *UDPConn 直接往某個目標地址發送數據報,而不是通過 DialUDP 方式發送,原因在于兩者返回的 *UDPConn 是不同的。前者是 connected ,后者是 unconnected 。

你必須清楚知道你的UDP是連接的(connected)還是未連接(unconnected)的,這樣你才能正確的選擇的讀寫方法。

如果 *UDPConn 是 connected ,讀寫方法是 Read 和 Write 。

如果 *UDPConn 是 unconnected ,讀寫方法是 ReadFromUDP 和 WriteToUDP (以及 ReadFrom 和 WriteTo )。

事實上Go的這種設計和Unix/Linux設計一致,下面是Linux關于UDP的文檔:

When a UDP socket is created, its local and remote addresses are unspecified. Datagrams can be sent immediately using sendto or sendmsg with a valid destination address as an argument. When connect is called on the socket, the default destination address is set and datagrams can now be sent using send or write without specifying a destination address. It is still possible to send to other destinations by passing an address to sendto or sendmsg . In order to receive packets, the socket can be bound to a local address first by using bind . Otherwise, the socket layer will automatically assign a free local port out of the range defined by /proc/sys/net/ipv4/ip_local_port_range and bind the socket to INADDR_ANY.

ReadFrom 和 WriteTo 是為了實現 PacketConn 接口而實現的方法,它們的實現基本上和 ReadFromUDP 和 WriteToUDP 一樣,只不過地址換成了更通用的 Addr ,而不是具體化的 UDPAddr 。

還有幾種情況需要弄清楚:

1、因為 unconnected 的 *UDPConn 還沒有目標地址,所以需要把目標地址當作參數傳入到 WriteToUDP 的方法中,但是 unconnected 的 *UDPConn 可以調用 Read 方法嗎?

答案是 可以 ,但是在這種情況下,客戶端的地址信息就被忽略了。

funcmain() {
 listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port:9981})
iferr !=nil{
 fmt.Println(err)
return
 }

 fmt.Printf("Local: <%s> \n", listener.LocalAddr().String())

 data := make([]byte,1024)
for{
 n, err := listener.Read(data)
iferr !=nil{
 fmt.Printf("error during read: %s", err)
 }

 fmt.Printf("<%s>\n", data[:n])

 }
}

2、unconnected 的 *UDPConn 可以調用 Write 方法嗎?

答案是 不可以 , 因為不知道目標地址。

funcmain() {
 listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port:9981})
iferr !=nil{
 fmt.Println(err)
return
 }

 fmt.Printf("Local: <%s> \n", listener.LocalAddr().String())

 _, err = listener.Write([]byte("hello"))
iferr !=nil{
 fmt.Printf(err.Error())
 }
}

出錯:

writeudp127.0.0.1:9981:write:destinationaddressrequiredsmallnestMBP:udpsmallnest

3、connected 的 *UDPConn 可以調用 WriteToUDP 方法嗎?

答案是 不可以 , 因為目標地址已經設置。

即使是相同的目標地址也 不可以

funcmain() {
 ip := net.ParseIP("127.0.0.1")

 srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port:0}
 dstAddr := &net.UDPAddr{IP: ip, Port:9981}

 conn, err := net.DialUDP("udp", srcAddr, dstAddr)
iferr !=nil{
 fmt.Println(err)
 }
deferconn.Close()

 _, err = conn.WriteToUDP([]byte("hello"), dstAddr)
iferr !=nil{
 fmt.Println(err)
 }
}

報錯:

write udp 127.0.0.1:50141->127.0.0.1:9981:useofWriteTowithpre-connected connection

4、connected 的 *UDPConn 如果調用 Closed 以后可以調用 WriteToUDP 方法嗎?

答案是 不可以

funcmain() {
 ip := net.ParseIP("127.0.0.1")

 srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port:0}
 dstAddr := &net.UDPAddr{IP: ip, Port:9981}

 conn, err := net.DialUDP("udp", srcAddr, dstAddr)
iferr !=nil{
 fmt.Println(err)
 }
 err = conn.Close()
iferr !=nil{
 fmt.Println(err)
 }

 _, err = conn.WriteToUDP([]byte("hello"), dstAddr)
iferr !=nil{
 fmt.Println(err)
 }
}

同樣的報錯:

write udp 127.0.0.1:59074->127.0.0.1:9981:useofWriteTowithpre-connected connection

5、connected 的 *UDPConn 可以調用 ReadFromUDP 方法嗎?

答案是 可以 ,但是它的功能基本和 Read 一樣,只能和它 connected 的對端通信。

看下面的client的例子:

funcmain() {
 ip := net.ParseIP("127.0.0.1")

 srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port:0}
 dstAddr := &net.UDPAddr{IP: ip, Port:9981}

 conn, err := net.DialUDP("udp", srcAddr, dstAddr)
iferr !=nil{
 fmt.Println(err)
 }
deferconn.Close()

gofunc() {
 data := make([]byte,1024)
for{
 n, remoteAddr, err := conn.ReadFromUDP(data)
iferr !=nil{
 fmt.Printf("error during read: %s", err)
 }

 fmt.Printf("<%s> %s\n", remoteAddr, data[:n])
 }
 }()

 conn.Write([]byte("hello"))

 b := make([]byte,1)
 os.Stdin.Read(b)
}

6、*UDPConn 還有一個通用的 WriteMsgUDP(b, oob []byte, addr *UDPAddr) ,同時支持 connected 和 unconnected 的UDPConn:

  1. 如果 UDPConn 還未連接,那么它會發送數據報給addr
  2. 如果 UDPConn 已連接,那么它會發送數據報給連接的對端,這種情況下addr應該為nil

通用多播編程

Go標準庫也支持多播,但是我們首先我們看通用的多播是如何實現的,它使用 golang.org/x/net/ipv4 或者 golang.org/x/net/ipv6 進行控制。

首先找到要進行多播所使用的網卡,然后監聽本機合適的地址和服務端口。

將這個應用加入到多播組中,它就可以從組中監聽包信息,當然你還可以對包傳輸進行更多的控制設置。

應用收到包后還可以檢查包是否來自這個組的包。

完整的代碼如下:

packagemain

import(
"fmt"
"net"

"golang.org/x/net/ipv4"
)

funcmain() {
//1. 得到一個interface
 en4, err := net.InterfaceByName("en4")
iferr !=nil{
 fmt.Println(err)
 }
 group := net.IPv4(224,0,0,250)

//2. bind一個本地地址
 c, err := net.ListenPacket("udp4","0.0.0.0:1024")
iferr !=nil{
 fmt.Println(err)
 }
deferc.Close()

//3.
 p := ipv4.NewPacketConn(c)
iferr := p.JoinGroup(en4, &net.UDPAddr{IP: group}); err !=nil{
 fmt.Println(err)
 }

//4.更多的控制
iferr := p.SetControlMessage(ipv4.FlagDst,true); err !=nil{
 fmt.Println(err)
 }

//5.接收消息
 b := make([]byte,1500)
for{
 n, cm, src, err := p.ReadFrom(b)
iferr !=nil{
 fmt.Println(err)
 }
ifcm.Dst.IsMulticast() {
ifcm.Dst.Equal(group) {
 fmt.Printf("received: %s from <%s>\n", b[:n], src)
 n, err = p.WriteTo([]byte("world"), cm, src)
iferr !=nil{
 fmt.Println(err)
 }
 } else{
 fmt.Println("Unknown group")
continue
 }
 }
 }
}

同一個應用可以加入到多個組中,多個應用也可以加入到同一個組中。

多個UDP listener可以監聽同樣的端口,加入到同一個group中。

It is possible for multiple UDP listeners that listen on the same UDP port to join the same multicast group. The net package will provide a socket that listens to a wildcard address with reusable UDP port when an appropriate multicast address prefix is passed to the net.ListenPacket or net.ListenUDP.

c1, err := net.ListenPacket("udp4","224.0.0.0:1024")
iferr !=nil{
// error handling
}
deferc1.Close()
c2, err := net.ListenPacket("udp4","224.0.0.0:1024")
iferr !=nil{
// error handling
}
deferc2.Close()
p1 := ipv4.NewPacketConn(c1)
iferr := p1.JoinGroup(en0, &net.UDPAddr{IP: net.IPv4(224,0,0,248)}); err !=nil{
// error handling
}
p2 := ipv4.NewPacketConn(c2)
iferr := p2.JoinGroup(en0, &net.UDPAddr{IP: net.IPv4(224,0,0,248)}); err !=nil{
// error handling
}

還支持 Source-specific multicasting 特性。

標準庫多播編程

標準庫的多播編程簡化了上面的操作,當然也減少了更多的控制。如果想實現一個簡單的多播程序,可以使用這樣的方法。

服務器端的代碼:

funcmain() {
//如果第二參數為nil,它會使用系統指定多播接口,但是不推薦這樣使用
 addr, err := net.ResolveUDPAddr("udp","224.0.0.250:9981")
iferr !=nil{
 fmt.Println(err)
 }

 listener, err := net.ListenMulticastUDP("udp",nil, addr)
iferr !=nil{
 fmt.Println(err)
return
 }

 fmt.Printf("Local: <%s> \n", listener.LocalAddr().String())

 data := make([]byte,1024)
for{
 n, remoteAddr, err := listener.ReadFromUDP(data)
iferr !=nil{
 fmt.Printf("error during read: %s", err)
 }

 fmt.Printf("<%s> %s\n", remoteAddr, data[:n])
 }
}

寫個客戶端測試一下:

funcmain() {
 ip := net.ParseIP("224.0.0.250")

 srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port:0}
 dstAddr := &net.UDPAddr{IP: ip, Port:9981}

 conn, err := net.DialUDP("udp", srcAddr, dstAddr)
iferr !=nil{
 fmt.Println(err)
 }
deferconn.Close()

 conn.Write([]byte("hello"))

 fmt.Printf("<%s>\n", conn.RemoteAddr())}

廣播

關于單播、多播、廣播和任播可以參考我以前寫的一篇文章: 單播,組播(多播),廣播以及任播 。

廣播的編程方式和多播的編程方式有所不同。簡單說,廣播意味著你吼一嗓子,局域網內的所有的機器都會收到。

服務器端代碼:

funcmain() {
 listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port:9981})
iferr !=nil{
 fmt.Println(err)
return
 }

 fmt.Printf("Local: <%s> \n", listener.LocalAddr().String())

 data := make([]byte,1024)
for{
 n, remoteAddr, err := listener.ReadFromUDP(data)
iferr !=nil{
 fmt.Printf("error during read: %s", err)
 }

 fmt.Printf("<%s> %s\n", remoteAddr, data[:n])

 _, err = listener.WriteToUDP([]byte("world"), remoteAddr)

iferr !=nil{
 fmt.Printf(err.Error())
 }
 }
}

客戶端代碼有所不同,它不是通過 DialUDP “連接” 廣播地址,而是通過 ListenUDP 創建一個 unconnected 的 *UDPConn ,然后通過 WriteToUDP 發送數據報,這和你腦海中的客戶端不太一致:

funcmain() {
 ip := net.ParseIP("172.24.14.255")

 srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port:0}
 dstAddr := &net.UDPAddr{IP: ip, Port:9981}

 conn, err := net.ListenUDP("udp", srcAddr)
iferr !=nil{
 fmt.Println(err)
 }

 n, err := conn.WriteToUDP([]byte("hello"), dstAddr)
iferr !=nil{
 fmt.Println(err)
 }

 data := make([]byte,1024)
 n, _, err = conn.ReadFrom(data)
iferr !=nil{
 fmt.Println(err)
 }

 fmt.Printf("read %s from <%s>\n", data[:n], conn.RemoteAddr())

 b := make([]byte,1)
 os.Stdin.Read(b)
}

你局域網內的廣播地址可能和例子中的不同,你可以通過 ifconfig 查看。

廣播地址(Broadcast Address)是專門用于同時向網絡中所有工作站進行發送的一個地址。在使用TCP/IP 協議的網絡中,主機標識段host ID 為全1 的IP 地址為廣播地址,廣播的分組傳送給host ID段所涉及的所有計算機。例如,對于10.1.1.0 (255.255.255.0 )網段,其廣播地址為10.1.1.255 (255 即為2 進制的11111111 ),當發出一個目的地址為10.1.1.255 的分組(封包)時,它將被分發給該網段上的所有計算機。

任播

在互聯網中,通常使用邊界網關協議來實現任播。比如域名根服務器就是通過任播的方式提供。13臺根服務器使用13個任播地址,但是有500多臺實際服務器。

你可以通過單播的方式發送數據包,只有最快的(最近的)的一個UDP服務器接收到。

Anycasting最初是在RFC1546中提出并定義的,它的最初語義是,在IP網絡上通過一個Anycast地址標識一組提供特定服務的主機,同時服務訪問方并不關心提供服務的具體是哪一臺主機(比如DNS或者鏡像服務),訪問該地址的報文可以被IP網絡路由到這一組目標中的任何一臺主機上,它提供的是一種無狀態的、盡力而為的服務。

RFC2373(IP Version 6 Addressing Architecture, July 1998)提供了較新的說明和動機:任播地址的一個期望應用是標識屬于某個提供互聯網服務的機構的路由器集合。這種地址可以用作IPv6路由標題的中間地址,使數據分組通過某一聚合或聚合序列傳遞。其他可能的用途是標識屬于某一子網的路由器組或提供進入某一路由范圍入口的路由器組。

RFC2373標準對任播的定義是,當一個單播地址被分配到多于一個的接口上時,發到該接口的報文被網絡路由到由路由協議度量的“最近”的目標接口上。與Unicast和Multicast類似,Anycast也是IP網絡的一種通信模式。Unicast允許源結點向單一目標結點發送數據報,Multicast允許源結點向一組目標結點發送數據報,而Anycast則允許源結點向一組目標結點中的一個結點發送數據報,而這個結點由路由系統選擇,對源結點透明;同時,路由系統選擇“最近”的結點為源結點提供服務,從而在一定程序上為源結點提供了更好的服務也減輕了網絡負載。

參考文檔

  1. https://zh.wikipedia.org/wiki/用戶數據報協議
  2. https://golang.org/pkg/net/
  3. http://man7.org/linux/man-pages/man7/udp.7.html
  4. https://godoc.org/golang.org/x/net/ipv4
  5. https://github.com/golang/go/issues/13391
  6. http://baike.baidu.com/view/473043.htm
  7. http://baike.baidu.com/view/2032315.htm

 

來自:http://colobu.com/2016/10/19/Go-UDP-Programming/

 

 本文由用戶 nhuc7871 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!