用户数据报协议(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库及其使用方法,以及了解使用过程中的细节和陷阱。

一个简单的例子

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())
		}
	}
}
package main
import (
	"fmt"
	"net"
)
func main() {
	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)
	if err != nil {
		fmt.Println(err)
	}
	defer conn.Close()
	conn.Write([]byte("hello"))
	fmt.Printf("<%s>\n", conn.RemoteAddr())
}
ListenTCPAcceptListenUDPReadFromUDPWriteToUDP

我们稍微修改一下client1.go,让它保持UDP Socket文件一直打开:

func main() {
	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)
	if err != nil {
		fmt.Println(err)
	}
	defer conn.Close()
	b := make([]byte, 1)
	os.Stdin.Read(b)
	conn.Write([]byte("hello"))
	fmt.Printf("<%s>\n", conn.RemoteAddr())
}
netstat
udp4       0      0  localhost.54676        localhost.9981
udp4       0      0  localhost.9981         *.*
lsof
server1   59312 smallnest    3u  IPv4 0xad793a9a54467f61      0t0  UDP localhost:9981
client1   59323 smallnest    3u  IPv4 0xad793a9a544681c1      0t0  UDP localhost:54676->localhost:9981

更复杂的例子

我们还可以将上面的例子演化一下,实现双向的读写。

listener.ReadFromUDPlistener.WriteToUDP

客户端修改为读写:

package main
import (
	"fmt"
	"net"
)
func main() {
	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)
	if err != nil {
		fmt.Println(err)
	}
	defer conn.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())
}
WriteRead

等价的客户端和服务器

下面这个是两个服务器通信的例子,互为客户端和服务器,在发送数据报的时候,我们可以将发送的一方称之为源地址,发送的目的地一方称之为目标地址。

package main
import (
	"fmt"
	"net"
	"os"
	"time"
)
func read(conn *net.UDPConn) {
	for {
		data := make([]byte, 1024)
		n, remoteAddr, err := conn.ReadFromUDP(data)
		if err != nil {
			fmt.Printf("error during read: %s", err)
		}
		fmt.Printf("receive %s from <%s>\n", data[:n], remoteAddr)
	}
}
func main() {
	addr1 := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9981}
	addr2 := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9982}
	go func() {
		listener1, err := net.ListenUDP("udp", addr1)
		if err != nil {
			fmt.Println(err)
			return
		}
		go read(listener1)
		time.Sleep(5 * time.Second)
		listener1.WriteToUDP([]byte("ping to #2: "+addr2.String()), addr2)
	}()
	go func() {
		listener1, err := net.ListenUDP("udp", addr2)
		if err != nil {
			fmt.Println(err)
			return
		}
		go read(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方法集的比较

DialUDPconnectconnected*UDPConn
ListenUDP*UDPConnDialUDP*UDPConnconnectedunconnected

你必须清楚知道你的UDP是连接的(connected)还是未连接(unconnected)的,这样你才能正确的选择的读写方法。

*UDPConnconnectedReadWrite*UDPConnunconnectedReadFromUDPWriteToUDPReadFromWriteTo

事实上Go的这种设计和Unix/Linux设计一致,下面是Linux关于UDP的文档:

sendtosendmsgconnectsendwritesendtosendmsgbind/proc/sys/net/ipv4/ip_local_port_range
ReadFromWriteToPacketConnReadFromUDPWriteToUDPAddrUDPAddr
unconnected*UDPConnWriteToUDPunconnected*UDPConnRead

答案是可以,但是在这种情况下,客户端的地址信息就被忽略了。

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, err := listener.Read(data)
		if err != nil {
			fmt.Printf("error during read: %s", err)
		}
		fmt.Printf("<%s>\n", data[:n])
	}
}
unconnected*UDPConnWrite
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())
	_, err = listener.Write([]byte("hello"))
	if err != nil {
		fmt.Printf(err.Error())
	}
}

出错:

	
write udp 127.0.0.1:9981: write: destination address requiredsmallnestMBP:udp smallnest
connected*UDPConnWriteToUDP
func main() {
	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)
	if err != nil {
		fmt.Println(err)
	}
	defer conn.Close()
	_, err = conn.WriteToUDP([]byte("hello"), dstAddr)
	if err != nil {
		fmt.Println(err)
	}
}

报错:

write udp 127.0.0.1:50141->127.0.0.1:9981: use of WriteTo with pre-connected connection
connected*UDPConnClosedWriteToUDP
func main() {
	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)
	if err != nil {
		fmt.Println(err)
	}
	err = conn.Close()
	if err != nil {
		fmt.Println(err)
	}
	_, err = conn.WriteToUDP([]byte("hello"), dstAddr)
	if err != nil {
		fmt.Println(err)
	}
}

同样的报错:

write udp 127.0.0.1:59074->127.0.0.1:9981: use of WriteTo with pre-connected connection
connected*UDPConnReadFromUDPReadconnected

看下面的client的例子:

func main() {
	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)
	if err != nil {
		fmt.Println(err)
	}
	defer conn.Close()
	go func() {
		data := make([]byte, 1024)
		for {
			n, remoteAddr, err := conn.ReadFromUDP(data)
			if err != 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)
}
*UDPConnWriteMsgUDP(b, oob []byte, addr *UDPAddr)connectedunconnected
UDPConnUDPConn

通用多播编程

golang.org/x/net/ipv4golang.org/x/net/ipv6

首先找到要进行多播所使用的网卡,然后监听本机合适的地址和服务端口。 将这个应用加入到多播组中,它就可以从组中监听包信息,当然你还可以对包传输进行更多的控制设置。 应用收到包后还可以检查包是否来自这个组的包。

完整的代码如下:

package main
import (
	"fmt"
	"net"
	"golang.org/x/net/ipv4"
)
func main() {
	//1. 得到一个interface
	en4, err := net.InterfaceByName("en4")
	if err != nil {
		fmt.Println(err)
	}
	group := net.IPv4(224, 0, 0, 250)
	//2. bind一个本地地址
	c, err := net.ListenPacket("udp4", "0.0.0.0:1024")
	if err != nil {
		fmt.Println(err)
	}
	defer c.Close()
	//3.
	p := ipv4.NewPacketConn(c)
	if err := p.JoinGroup(en4, &net.UDPAddr{IP: group}); err != nil {
		fmt.Println(err)
	}
	//4.更多的控制
	if err := p.SetControlMessage(ipv4.FlagDst, true); err != nil {
		fmt.Println(err)
	}
	//5.接收消息
	b := make([]byte, 1500)
	for {
		n, cm, src, err := p.ReadFrom(b)
		if err != nil {
			fmt.Println(err)
		}
		if cm.Dst.IsMulticast() {
			if cm.Dst.Equal(group) {
				fmt.Printf("received: %s from <%s>\n", b[:n], src)
				n, err = p.WriteTo([]byte("world"), cm, src)
				if err != 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")
if err != nil {
	// error handling
}
defer c1.Close()
c2, err := net.ListenPacket("udp4", "224.0.0.0:1024")
if err != nil {
	// error handling
}
defer c2.Close()
p1 := ipv4.NewPacketConn(c1)
if err := p1.JoinGroup(en0, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 248)}); err != nil {
	// error handling
}
p2 := ipv4.NewPacketConn(c2)
if err := p2.JoinGroup(en0, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 248)}); err != nil {
	// error handling
}
Source-specific multicasting

标准库多播编程

标准库的多播编程简化了上面的操作,当然也减少了更多的控制。如果想实现一个简单的多播程序,可以使用这样的方法。

服务器端的代码:

func main() {
	//如果第二参数为nil,它会使用系统指定多播接口,但是不推荐这样使用
	addr, err := net.ResolveUDPAddr("udp", "224.0.0.250:9981")
	if err != nil {
		fmt.Println(err)
	}
	listener, err := net.ListenMulticastUDP("udp", nil, addr)
	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])
	}
}

写个客户端测试一下:

func main() {
	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)
	if err != nil {
		fmt.Println(err)
	}
	defer conn.Close()
	conn.Write([]byte("hello"))
	fmt.Printf("<%s>\n", conn.RemoteAddr())}

广播

关于单播、多播、广播和任播可以参考我以前写的一篇文章: 单播,组播(多播),广播以及任播。

广播的编程方式和多播的编程方式有所不同。简单说,广播意味着你吼一嗓子,局域网内的所有的机器都会收到。

服务器端代码:

func main() {
	listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, 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())
		}
	}
}
DialUDPListenUDPunconnected*UDPConnWriteToUDP
func main() {
	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)
	if err != nil {
		fmt.Println(err)
	}
	n, err := conn.WriteToUDP([]byte("hello"), dstAddr)
	if err != nil {
		fmt.Println(err)
	}
	data := make([]byte, 1024)
	n, _, err = conn.ReadFrom(data)
	if err != 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