Go socket编程实践: UDP服务器和客户端实现

在上一篇文章Go socket编程实践: TCP服务器和客户端实现, 我们实现了一个ECHO服务器, 并且也实现了一个客户端来访问服务器。
这篇文章讲解如何实现一个UDP服务器和客户端。
这次我们使用的协议是 RFC 868
此协议提供了一个独立于站点的,机器可读的日期和时间信息。时间服务返回的提供了一个32位的数字,是从1900年1月1日午夜到现在的秒数。
RFC 868定义时间协议使用端口37, TCP和UDP协议都可以。

另外还有两个关于时间/日期的RFC协议。
NTP (RFC 1305)是网络时间协议,提供了精确的时间同步。
daytime (RFC 867)在TCP端口13侦听,返回ACSII格式的日期和时间。

TCP/IP模型中,UDP为网络层以上和应用层以下提供了一个简单的接口。UDP只提供数据的不可靠传递,它一旦把应用程序发给网络层的数据发送出去,就不保留数据备份(所以UDP有时候也被认为是不可靠的数据报协议)。UDP在IP数据报的头部仅仅加入了复用和数据校验(字段)。

UDP首部字段由4个部分组成,其中两个是可选的。各16bit的来源端口和目的端口用来标记发送和接受的应用进程。因为UDP不需要应答,所以来源端口是可选的,如果来源端口不用,那么置为零。在目的端口后面是长度固定的以字节为单位的长度域,用来指定UDP数据报包括数据部分的长度,长度最小值为8byte。首部剩下地16bit是用来对首部和数据部分一起做校验和(Checksum)的,这部分是可选的,但在实际应用中一般都使用这一功能。

由于缺乏可靠性且属于非连接导向协定,UDP应用一般必须允许一定量的丢包、出错和复制贴上。但有些应用,比如TFTP,如果需要则必须在应用层增加根本的可靠机制。但是绝大多数UDP应用都不需要可靠机制,甚至可能因为引入可靠机制而降低性能。流媒体(串流技术)、即时多媒体游戏和IP电话(VoIP)一定就是典型的UDP应用。如果某个应用需要很高的可靠性,那么可以用传输控制协议(TCP协议)来代替UDP。

由于缺乏拥塞控制(congestion control),需要基于网络的机制来减少因失控和高速UDP流量负荷而导致的拥塞崩溃效应。换句话说,因为UDP发送者不能够检测拥塞,所以像使用包队列和丢弃技术的路由器这样的网络基本设备往往就成为降低UDP过大通信量的有效工具。数据报拥塞控制协议(DCCP)设计成通过在诸如流媒体类型的高速率UDP流中,增加主机拥塞控制,来减小这个潜在的问题。

典型网络上的众多使用UDP协议的关键应用一定程度上是相似的。这些应用包括域名系统(DNS)、简单网络管理协议(SNMP)、动态主机配置协议(DHCP)、路由信息协议(RIP)和某些影音串流服务等等。、

服务器

Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数。其他基本几乎一模一样,只有TCP换成了UDP而已。UDP的几个主要函数如下所示:

1
2
3
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)

net.ListenUDP返回UDPConn对象, 可以读取数据,以及往相应的client发送数据 (指定UDPAddr)。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main
import (
"encoding/binary"
"flag"
"fmt"
"net"
"os"
"time"
)
var host = flag.String("host", "", "host")
var port = flag.String("port", "37", "port")
func main() {
flag.Parse()
addr, err := net.ResolveUDPAddr("udp", *host+":"+*port)
if err != nil {
fmt.Println("Can't resolve address: ", err)
os.Exit(1)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Println("Error listening:", err)
os.Exit(1)
}
defer conn.Close()
for {
handleClient(conn)
}
}
func handleClient(conn *net.UDPConn) {
data := make([]byte, 1024)
n, remoteAddr, err := conn.ReadFromUDP(data)
if err != nil {
fmt.Println("failed to read UDP msg because of ", err.Error())
return
}
daytime := time.Now().Unix()
fmt.Println(n, remoteAddr)
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(daytime))
conn.WriteToUDP(b, remoteAddr)
}

时间数是64位的,需要将其转换成一个32位的字节。

执行 go run timeserver.go启动服务器

客户端

客户端代码和TCP类似。

1
2
3
4
5
func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) Read(b []byte) (int, error)
func (c *UDPConn) Write(b []byte) (int, error)
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main
import (
"encoding/binary"
"flag"
"fmt"
"net"
"os"
"time"
)
var host = flag.String("host", "localhost", "host")
var port = flag.String("port", "37", "port")
//go run timeclient.go -host time.nist.gov
func main() {
flag.Parse()
addr, err := net.ResolveUDPAddr("udp", *host+":"+*port)
if err != nil {
fmt.Println("Can't resolve address: ", err)
os.Exit(1)
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
fmt.Println("Can't dial: ", err)
os.Exit(1)
}
defer conn.Close()
_, err = conn.Write([]byte(""))
if err != nil {
fmt.Println("failed:", err)
os.Exit(1)
}
data := make([]byte, 4)
_, err = conn.Read(data)
if err != nil {
fmt.Println("failed to read UDP msg because of ", err)
os.Exit(1)
}
t := binary.BigEndian.Uint32(data)
fmt.Println(time.Unix(int64(t), 0).String())
os.Exit(0)
}

执行go run timeclient.go测试UDP。
也可以用公网的时间服务器测试: go run timeclient.go -host time.nist.gov

参考

  1. https://tools.ietf.org/html/rfc868