Introduction
多年的发展,使得IP
和 TCP/UDP
协议基本上就等价于网络协议栈。例如, 蓝牙定义了物理层和协议层,但最重要的是 IP 协议栈,可以在许多蓝牙设备使相同的互联网编程技术。同样, 开发 4G 无线手机技术,如 LTE(Long Term Evolution)
也将使用 IP 协议栈。
IP 提供了第 3 层的 OSI 网络协议栈,TCP 和 UDP 则提供了第 4 层。即使在因特网世界,这些都不是固定不变的:TCP 和 UDP 将面临来自 SCTP(STREAM CONTROL TRANSMISSION PROTOCOL 流控制传输协议)
的挑战,同时在星际空间中提供互联网服务需要新的像正在开发的 DTN 协议。不过,IP, TCP 和 UDP 至少在当前甚至未来相当长的时间内是主要的网络技术。Go 语言提供了对这种编程的全面支持。
本章介绍如何使用 GO 编写 TCP 和 UDP 程序,以及如何使用其他协议的原始套接字。
TCP/IP 协议栈 TCP/IP 协议栈
OSI 模型标准的建立和实施是一个委员会(国际标准化组织 ISO)设计的。OSI 标准中的一些部分是模糊的,有些部件不能很容易地实现,一些地方还没有得到落实。
TCP/IP
协议由长期运行的一个 DARPA(美国国防先进研究项目局)项目设计。该工作其次由 RFC (Request For Comment)实施。TCP/IP
是 Unix
的首要网络协议。TCP/IP
等于传输控制协议/互联网协议。
TCP/IP 协议栈是 OSI 模型的一部分:
TCP
是一个面向连接的协议,UDP
(User Datagram Protocol,用户数据报协议)是一种无连接的协议。
IP 数据包
IP 层
提供了无连接的不可靠的传输系统,任何数据包之间的关联必须依赖更高的层来提供。
IP 层
包头支持数据校验,在包头包括源地址
和目的地址
。
IP 层
通过路由连接到因特网,还负责将大数据包分解为更小的包,并传输到另一端后进行重组。
UDP
UDP
是无连接的,不可靠的。它包括 IP 数据报的内容和端口号的校验
。在后面,我们会用它来构建一些客户端/服务器例子。
TCP
TCP
是构建于 IP 之上的面向链接的协议。它提供了一个虚电路
使得两个应用进程可以通过它来通信。它通过端口号来识别主机上的服务。
互联网地址
要想使用一项服务,你必须先能找到它。互联网使用地址定位设备(例如计算机)。这种寻址方案最初被设计出来只允许极少数的计算机连接上,使用 32 位无符号整形
,拥有高达 2^32个地址。这就是所谓的 IPv4 地址
。近年来,连接(至少可以直接寻址)的设备的数量可能超过这个数字,所以在不久的某一天我们将切换到利用 128 位无符号整数
,拥有高 2^128 个地址的 IPv6
寻址。这种转换最有可能被已经耗尽了所有的 IPv4 地址的新兴发达国家和地区。
IPv4 地址
IP 地址
是一个 32 位整数
构成。每个设备的网络接口都有一个地址。该地址通常使用'.'
符号分割的 4 字节的十进制数,例如:"127.0.0.1" 或 "66.102.11.104"。
所有设备的 IP 地址,通常是由两部分组成:网段地址
和网内地址
。从前,网络地址和网内地址的分辨很简单,使用字节构建 IP 地址。
- 一个
A 类 IP 地址
由 1 字节的网络地址和 3 字节主机地址组成。A 类网络只有 128 个,被很早的互联网成员例如 IBM,通用电气公司(the General Electric Company)和 MIT所拥有。(http://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xml) - 一个
B类 IP 地址
由 2字节的网络地址和 2 字节主机地址组成,最多允许 2^16(65,536)个设备在同一个子网。 - 一个
C 类 IP 地址
由 3 字节的网络地址和 1 字节的主机地址组成。这最多允许 2^8 (其实是 254, 不是 256)个设备。
但是,比如你需要 400 台计算机在同一个网络,该方案是不可行的。254 太小,而 65,536 又太大。根据二进制计算,你大约需要 512(2^9,译者注)。这样就可以通过使用一个 23 位的网络地址和 9 位的设备地址实现。同样,如果您需要高达 1024 台设备,使用一个 22 位网络地址和一个 10 位的设备地址。
知道设备的 IP 地址和多少字节用于网络地址,那么可以比较直接的提取出这个网络中的网络地址和设备地址。例如:“网络掩码”
是一个前面 N 位为 1,其他所有位为 0 的 32 位二进制数。例如,如果使用 16 位的网络地址,掩码为 11111111111111110000000000000000。使用二进制有一点不方便,所以通常使用十进制字节。16 位网络地址的子网掩码是 255.255.0.0,而对于 23 位地址,这将是 255.255.254.0,和 22 位地址,这将是 255.255.252.0。
IPv6 地址
因特网的迅速发展大大超出了原来的预期。最初富余的 32 位地址解决方案已经接近用完。虽然有一些例如 NAT 地址转换这样不是很完美的解决方法,但最终我们将不得不切换到更广阔的地址空间。IPv6
使用 128 位地址
,即使表达同样的地址,字节数变得很麻烦,由':'分隔的 4 位 16 进制组成。一个典型的例子如:2002:c0e8:82e7:0:0:0:c0e8:82e7。
要记住这些地址并不容易!DNS 将变得更加重要。有一些技巧用来介绍一些地址,如省略一些零和重复的数字。例如:"localhost"地址是:0:0:0:0:0:0:0:1,可以缩短到::1
IP 地址类型
IP 类型
"net"包
定义了许多类型, 函数,方法用于 Go 网络编程。IP 类型
被定义为一个字节数组。
type IP []byte
有几个函数来处理一个 IP 类型的变量, 但是在实践中你很可能只用到其中的一些。例如,ParseIP(String)
函数将获取.
分隔的 IPv4 或者冒号分隔的 IPv6 地址, 而 IP
的String方法
将返回一个字符串。请注意,您可能无法取回你期望的: 字符串 0:0:0:0:0:0:0:1 是::1。
下面用一个程序来说明
/* IP
*/
package main
import(
"net"
"os"
"fmt"
)
func main() {
if len(os. len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
os.Exit(1)
}
os.name := os.Args[1]
addr := net.ParseIP(name)
if addr == nil {
fmt.Println("Invalid address")
} else{
fmt.Println("The address is ", addr.String())
}
os.Exit(0)
}
如果编译它为可执行文件 IP
,那么它可以运行如下:
IP 127.0.0.1
得到结果
The address is 127.0.0.1
或
IP 0:0:0:0:0:0:0:1
得到结果
The address is ::1
IP 掩码
为了处理掩码操作,有下面类型:
type IPMask []byte
下面这个函数用一个 4 字节的 IPv4 地址来创建一个掩码
func IPv4Mask(a, b, c, d byte)
另外, 这是一个 IP 的方法返回默认的掩码
func (ip IP) DefaultMask() IPMask
需要注意的是一个掩码的字符串形式是一个十六进制数,如掩码 255.255.0.0 为 ffff0000
一个掩码可以使用一个 IP 地址的方法,找到该 IP 地址的网络
func (ip IP) Mask(mask IPMask) IP
下面的程序是一个使用了这个的例子:
/* Mask
*/
package main
import(
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s dotted-ip-addr\n", os.Args[0])
os.Exit(1)
}
dotAddr := os.Args[1]
addr := net.ParseIP(dotAddr)
if addr == nil {
fmt.Println("Invalid address")
os.Exit(1)
}
mask := addr.DefaultMask()
network := addr.Mask(mask)
ones, bits := mask.Size()
fmt.Println("Address is ", addr.String(),
" Default mask length is ", bits,
"Leading ones count is ", ones,
"Mask is (hex) ", mask.String(),
" Network is ", network.String())
os.Exit(0)
}
编译并运行 Mask
Mask 127.0.0.1
将返回
Address is 127.0.0.1 Default mask length is 8 Network is 127.0.0.0
IPAddr 类型
在 net 包的许多函数和方法会返回一个指向 IPAddr
的指针。这不过只是一个包含 IP
类型的结构体。
type IPAddr struct{
IP IP
}
这种类型的主要用途是通过 IP 主机名执行 DNS 查找。
func ResolveIPAddr(net, addr string) (*IPAddr, os.Error)
其中net
是"ip"
,"ip4"
或者"ip6"
的其中一个. 下面的程序中将会展示。
/*ResolveIP*/
package main
import (
"net"
"os"
"fmt"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage:%s hostname\n", os.Args[0])
fmt.Println("Usage:", os.Args[0], "hostname")
os.Exit(1)
}
name := os.Args[1]
addr,err := net.ResolveIPAddr("ip", name)
if err != nil {
fmt.Println("Resolution error", err.Error())
os.Exit(1)
}
fmt.Println("Resolved address is ", addr.String())
os.Exit(0)
}
运行ResolveIP www.google.com
返回
Resolved address is 66.102.11.104
主机查询
ResolveIPAddr
函数将对某个主机名执行 DNS 查询,并返回一个简单的 IP 地址。然而,通常主机如果有多个网卡,则可以有多个 IP 地址。它们也可能有多个主机名,作为别名。
func LookupHost(name string) (cname string, addrs []string, err os.Error)
这些地址将会被归类为“canonical”主机名。如果你想找到规范名称,使用 func LookupCNAME(name string) (cname string, err os.Error)
/*LookupHost*/
package main
import (
"net"
"os"
"fmt"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage:%s hostname\n", os.Args[0])
os.Exit(1)
}
name := os.Args[1]
addrs,err := net.LookupHost(name)
if err != nil {
fmt.Println("Error: ", err.Error())
os.Exit(2)
}
for _,s := range addrs {
fmt.Println(s)
}
os.Exit(0)
}
注意,这个函数返回字符串,而不是 IPAddress
执行./IPAddress www.baidu.com
结果
115.239.210.27 115.239.211.112
服务
服务
运行在主机。它们通常长期存活,同时被设计成等待请求和响应请求。有许多类型的服务,他们能够通过各种方法向客户提供服务。互联网的世界基于 TCP 和 UDP 这两种通信方法提供许多这些服务,虽然也有其他通信协议如 SCTP伺机取代。许多其他类型的服务,例如点对点, 远过程调用, 通信代理, 和许多其他建立在 TCP 和 UDP 之上的服务之上。
端口
服务存活于主机内。IP 地址可以定位主机。但在每台计算机上可能会提供多种服务,需要一个简单的方法对它们加以区分。TCP,UDP,SCTP 或者其他协议使用端口号
来加以区分。这里使用一个 1~65,535 的无符号整数,每个服务将这些端口号中的一个或多个相关联。
有很多“标准”的端口
。Telnet 服务通常使用端口号 23 的 TCP 协议。DNS 使用端口号 53的 TCP 或 UDP 协议。FTP 使用端口 21 和 20 的命令,进行数据传输。HTTP 通常使用端口80,但经常使用,端口 8000,8080 和 8088,协议为 TCP。X Window 系统往往需要端口 6000-6007,TCP 和 UDP 协议。
在 Unix 系统中, /etc/services
文件列出了常用的端口。Go 语言有一个函数可以获取该文件
。
func LookupPort(network, service string) (port int, err os.Error)
network
是一个字符串例如"tcp"
或"udp"
, service
也是一个字符串,如"telnet"
或"domain"(DNS)
。
示例程序如下
/*LookupPort*/
package main
import (
"net"
"os"
"fmt"
)
func main() {
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "Usage:%s network-type-service\n", os.Args[0])
os.Exit(1)
}
networkType := os.Args[1]
service := os.Args[2]
port,err := net.LookupPort(networkType, service)
if err != nil {
fmt.Println("Error: ", err.Error())
os.Exit(2)
}
fmt.Println("Service port ", port)
os.Exit(0)
}
举个例子, 运行 LookupPort tcp telnet
打印Service port: 23
TCPAddr 类型
TCPAddr
类型包含一个IP
和一个port
的结构:
type TCPAddr struct {
IP IP
Port int
}
函数 ResolveTCPAddr
用来创建一个 TCPAddr
func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error) .Error)
net
是"tcp"
, "tcp4"
或"tcp6"
其中之一,addr
是一个字符串,由主机名或 IP 地址,以及":"后跟随着端口号组成,例如: "www.google.com:80" 或 '127.0.0.1:22"。如果地址是一个 IPv6 地址
,由于已经有冒号,主机部分,必须放在方括号内, 例如:"[::1]:23". 另一种特殊情况是经常用于服务器, 主机地址为 0, 因此,TCP 地址实际上就是端口名称, 例如:":80" 用来表示 HTTP服务器。
TCP 套接字
当你知道如何通过网络和端口 ID 查找一个服务时,然后呢?如果你是一个客户端,你需要一个 API,让您连接到服务,然后将消息发送到该服务,并从服务读取回复。
如果你是一个服务器,你需要能够绑定到一个端口,并监听它。当有消息到来,你需要能够读取它并回复客户端。
net.TCPConn
是允许在客户端和服务器之间的全双工通信的 Go 类型。两种主要方法是
func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)
TCPConn
被客户端和服务器用来读写消息
TCP 客户端
一旦客户端已经建立 TCP 服务, 就可以和对方设备"通话"了. 如果成功,该调用返回一个用于通信的 TCPConn。客户端和服务器通过它交换消息。通常情况下,客户端使用 TCPConn写入请求到服务器, 并从 TCPConn 的读取响应。持续如此,直到任一(或两者)的两侧关闭连接。客户端使用该函数建立一个 TCP 连接
。
func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error)
其中 laddr
是本地地址,通常设置为 nil 和 raddr
是一个服务的远程地址, net
是一个字符串,根据您是否希望是一个 TCPv4 连接,TCPv6 连接来设置为"tcp4"
, "tcp6"
或"tcp"
中的一个,当然你也可以不关心链接形式。
一个简单的例子,展示个客户端连接到一个网页(HTTP)服务器。在后面的章节,我们将处理大量的 HTTP 客户端和服务器细节,现在我们先从简单的看看。
客户端可能发送的消息之一就是“HEAD”消息
。这用来查询服务器的信息和文档信息。 服务器返回的信息,不返回文档本身。发送到服务器的请求可能是
"HEAD / HTTP/1.0\r\n\r\n"
这是在请求服务器的根文件信息。 一个典型的响应可能是
HTTP/1.0 200 OK
ETag:"-9985996"
Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT
Content- Content-Length: 18074
Connection: close
Date: Sat, 28 Aug 2010 00:43:48 GMT
Server: lighttpd/1.4.23
我们首先通过(GetHeadInfo.go)程序来建立 TCP 连接,发送请求字符串,读取并打印响应。编译后就可以调用,例如:
GetHeadInfo www.google.com:80
程序
/*LookupPort*/
package main
import (
"net"
"os"
"fmt"
"io/ioutil"
)
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error:%s", err.Error)
os.Exit(1)
}
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage:%s host:port\n", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
tcpAddr,err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
conn,err := net.DialTCP("tcp", nil, tcpAddr)
checkError(err)
_,err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
result,err := ioutil.ReadAll(conn)
checkError(err)
fmt.Println(string(result))
os.Exit(0)
}
结果
[root@localhost testcode]# ./GetHeadInfo www.baidu.com:80
HTTP/1.0 302 Found
Content-Length: 17931
Content-Type: text/html
Date: Wed, 12 Jun 2019 02:20:37 GMT
Etag: "54d9748e-460b"
Server: bfe/1.0.8.18
第一个要注意的点是近乎多余的错误检查。因为正常情况下,网络程序失败的机会大大超过单机的程序。在客户端,服务器端或任何路由和中间交换上,硬件可能失败;通信可能会被防火墙阻塞;因网络负载可能会出现超时;当客户端联系服务器,服务器可能会崩溃,下列检查是必须的:
- 指定的地址中可能存在语法错误
- 尝试连接到远程服务可能会失败。例如, 所请求的服务可能没有运行, 或者有可能是主机没有连接到网络
- 虽然连接已经建立,如果连接突然丢失也可能导致写失败,或网络超时
- 同样,读操作也可能会失败
值得一提的是,如何从服务端读取数据
。在这种情况下,读本质上是一个单一的来自服务器的响应,这将终止文件结束的连接。但是,它可能包括多个 TCP 数据包,所以我们需要不断地读,直到文件的末尾。在io/ioutil
下的ReadAll
函数考虑这些问题,并返回完整响应。
有一些涉及语言的问题,首先, 大多数函数返回两个值, 第二个值是可能出现的错误。如果没有错误发生, 那么它的值为 nil。在 C 中, 如果需要的话,同样的行为通过定义特殊值例如NULL, 或 -1, 或 0 来返回。在 Java 中, 同样的错误检查通过抛出和捕获异常来管理,它会使代码看起来很凌乱。
在这个程序的早期版本, 我在返回结果中返回 buf 数组, 它的类型是[512]byte。我试图强迫类型为一个字符串但失败了- 只有字节数组类型[]byte 可以强制转换。这确实有点困扰。
一个时间(Daytime)
我们可以建立最简单的服务--时间(Daytime)服务。这是一个标准的互联网服务, 由 RFC 867定义, 默认的端口 13
,协议是 TCP 和 UDP
。很遗憾, 对安全的偏执,几乎没有任何站点运行着时间(Daytime)服务器。不过没关系,我们可以建立我们自己的。 (对于那些有兴趣, 你可以在你的系统安装 inetd
, 你通常可以得到一个时间(Daytime)服务器。)
在一个服务器上注册并监听一个端口。然后它阻塞在一个"accept"
操作,并等待客户端连接。当一个客户端连接, accept 调用返回一个连接(connection)对象。时间(Daytime)服务非常简单,只是将当前时间写入到客户端, 关闭该连接,并继续等待下一个客户端。
有关调用
func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)
net
参数可以设置为字符串"tcp"
, "tcp4"
或者"tcp6"
中的一个。如果你想监听所有网络接口,IP地址应设置为 0,或如果你只是想监听一个简单网络接口,IP 地址可以设置为该网络的地址。如果端口设置为 0,O/S 会为你选择一个端口。否则,你可以选择你自己的。需要注意的是,在 Unix 系统上,除非你是监控系统,否则不能监听低于 1024 的端口,小于 128 的端口是由IETF 标准化。该示例程序选择端口 1200 没有特别的原因。TCP 地址如下":1200" - 所有网络接口, 端口 1200。
程序
/*DaytimeServer*/
package main
import (
"net"
"os"
"fmt"
"time"
)
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error:%s", err.Error)
os.Exit(1)
}
}
func main() {
service := ":1200"
tcpAddr,err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
listener,err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
daytime := time.Now().String()
conn.Write([]byte(daytime))
conn.Close()
}
}
如果你运行该服务器, 它会在那里等待, 没有做任何事。当一个客户端连接到该服务器, 它会响应发送时间(Daytime)字符串,然后继续等待下一个客户端。
相比客户端服务器更要注意对错误的处理。服务器应该永远运行,所以,如果出现任何与客户端有关的错误,服务器只是忽略客户端继续运行。否则,客户端可以尝试搞砸了与服务器的连接,并导致服务器宕机。
我们还没有建立一个客户端。这很简单,只是改变以前的客户端省略的初始写入。另外, 只需打开一个 telnet
连接到该主机:
telnet localhost 1200
输出如下:
[root@localhost protocol]# telnet localhost 1200
Trying ::1...
Connected to localhost.
Escape character is '^]'.
2019-06-11 16:29:51.081306713 -0400 EDT m=+57.103802696Connection closed by foreign host.
"2019-06-11 16:29:51.081306713 -0400 EDT m=+57.103802696"为服务器返回内容
多线程服务器
"echo"是另一种简单的 IETF 服务。只是读取客户端数据,并将其发送回去:
/*SimpleEchoServer*/
package main
import (
"net"
"os"
"fmt"
)
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error:%s", err.Error)
os.Exit(1)
}
}
func main() {
service := ":1201"
tcpAddr,err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
listener,err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
handleClient(conn)
conn.Close()
}
}
func handleClient(conn net.Conn) {
var buf [512]byte
for {
n,err := conn.Read(buf[0:])
if err != nil {
return
}
fmt.Println(string(buf[0:]))
_,err2 := conn.Write(buf[0:n])
if err2 != nil {
return
}
}
}
工作时,此服务器有一个明显的问题: 它是单线程的。当有一个客户端连接到它,就没有其他的客户端可以连接上。其他客户端将被阻塞,可能会超时。幸好客户端很容易使用go-routine 扩展。我们仅仅需要把连接关闭移到处理程序结束后,示例代码如下:
/*ThreadEchoServer*/
package main
import (
"net"
"os"
"fmt"
)
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error:%s", err.Error)
os.Exit(1)
}
}
func main() {
service := ":1201"
tcpAddr,err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
listener,err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
defer conn.Close()
var buf [512]byte
for {
n,err := conn.Read(buf[0:])
if err != nil {
return
}
fmt.Println(string(buf[0:]))
_,err2 := conn.Write(buf[0:n])
if err2 != nil {
return
}
}
}
控制 TCP 连接
超时
服务端会断开那些超时的客户端,如果他们响应不够快,比如没有及时往服务端写一个请求。这应该是长时间(几分钟)的,因为用户可能花费了时间。相反, 客户端可能希望超时服务器(一个更短的时间后)。通过下面的来实现这两种:
func (c *TCPConn) SetTimeout(nsec int64) os.Error
套接字读写前。
存活状态
即使没有任何通信,一个客户端可能希望保持连接到服务器的状态。可以使用
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error
还有几个其他的连接控制方法, 可以查看"net"包。
UDP 数据报
在一个无连接的协议中,每个消息都包含了关于它的来源和目的地的信息。没有"session"建立在使用长寿命的套接字。UDP 客户端和服务器使用的数据包,单独包含来源和目的地的信息。除非客户端或服务器这样做,否则消息的状态不会保持。这些消息不能保证一定到达,也可能保证按顺序到达。
客户端最常见的情况,发送消息,并希望响应正常到达。服务器最常见的情况为将收到一条消息,然后发送一个或多个回复给客户端。而在peer-to-peer的情况下, 服务器可能仅仅是把消息转发到其他点。
Go 处理 TCP 和 UDP 之间的主要区别
是如何处理多个客户端可能同时有数据包到达,没有一个管理 TCP 会话的缓冲。主要需要调用的是
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) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error)
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)
UDP 时间服务的客户端并不需要做很多的变化,仅仅改变...TCP...调用为...UDP...调用:
/*UDPDaytimeClient*/
package main
import (
"net"
"os"
"fmt"
)
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error:%s", err.Error)
os.Exit(1)
}
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr,"Usage:%s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
udpAddr,err := net.ResolveUDPAddr("udp4", service)
checkError(err)
conn,err := net.DialUDP("udp", nil, udpAddr)
_,err := conn.Write([]byte("anything"))
checkError(err)
var buf [512]byte
n,err := conn.Read(buf[0:])
checkError(err)
fmt.Println(string(buf[0:n]))
os.Exit(0)
}
服务器也有很少的改动:
/*UDPDaytimeServer*/
package main
import (
"net"
"os"
"fmt"
"time"
)
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error:%s", err.Error)
os.Exit(1)
}
}
func main() {
service := ":1200"
udpAddr,err := net.ResolveUDPAddr("udp4", service)
checkError(err)
conn,err := net.ListenUDP("udp", udpAddr)
checkError(err)
for {
handleClient(conn)
}
_,err := conn.Write([]byte("anything"))
checkError(err)
var buf [512]byte
n,err := conn.Read(buf[0:])
checkError(err)
fmt.Println(string(buf[0:n]))
os.Exit(0)
}
func handleClient(conn net.Conn) {
var buf [512]byte
_,addr,err := conn.ReadFromUDP(buf[0:])
if err != nil {
return
}
daytime := time.Now().String()
conn.WriteToUDP([]byte(daytime), addr)
}
服务器侦听多个套接字
一个服务器可能不止在一个端口监听多个客户端,或是更多端口,在这种情况下,它在端口之间使用某种轮询机制。
在 C 中, 调用的内核 select()可以完成这项工作。 调用需要一个文件描述符的数字。该进程被暂停。当 I/O 准备好其中一个,一个唤醒被完成,并且该过程可以继续。This is cheaper than busy polling. 在 Go 中, 完成相同的功能,通过为每个端口使用一个不同的 goroutine。低级别的 select()时发现,I/O 已经准备好该线程,一个线程将运行。
Conn,PacketConn 和 Listener类型
迄今为止我们已经区分 TCP 和 UDP API 的不同,使用例子 DialTCP
和 DialUDP
分别返回一个 TCPConn
和 UDPConn
。Conn 类型是一个接口,TCPConn 和 UDPConn 实现了该接口。在很大程度上,你可以通过该接口处理而不是用这两种类型。
你可以使用一个简单的函数,而不是单独使用 TCP 和 UDP 的 dial 函数
func Dial(net, laddr, raddr string) (c Conn, err os.Error)
net
可以是"tcp"
, "tcp4"
(IPv4-only),"tcp6"
(IPv6-only), "udp"
,"udp4"
(IPv4-only), "udp6"
(IPv6-only), "ip"
, "ip4"
(IPv4-only)和"ip6"
(IPv6-only)任何一种。它将返回一个实现了 Conn 接口的类型。注意此函数接受一个字符串而不是 raddr 地址参数,因此,使用此程序可避免的地址类型。
使用该函数需要对程序轻微的调整。例如, 前面的程序从一个 Web 页面获取 HEAD 信息可以被重新写为
/*IPGetHeadInfo*/
package main
import (
"bytes"
"net"
"os"
"fmt"
"io"
)
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error:%s", err.Error)
os.Exit(1)
}
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage:%s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
conn,err := net.Dial("tcp", service)
checkError(err)
_,err := conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
result,err := readFully(conn)
checkError(err)
fmt.Println(string(result))
os.Exit(0)
}
func readFully(conn net.Conn)([]byte, error) {
defer conn.Close()
result := bytes.NewBuffer(nil)
var buf [512]byte
for {
n,err := conn.Read(buf[0:])
result.Write(buf[0:])
if err != nil {
if err == io.EOF {
break
}
return nil,err
}
}
return result.Bytes(),nil
}
使用该函数同样可以简化一个服务器的编写
func Listen(net, laddr string) (l Listener, err os.Error)
返回一个实现Listener
接口的对象. 该接口有一个方法
func (l Listener) Accept() (c Conn, err os.Error)
这将允许构建一个服务器。使用它, 将使前面给出的多线程 Echo 服务器改变
/*ThreadIPEchoServer*/
package main
import (
"net"
"os"
"fmt"
)
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error:%s", err.Error)
os.Exit(1)
}
}
func main() {
service := ":1200"
listener,err := net.Listen("tcp", service)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
defer conn.Close()
var buf [512]byte
for {
n,err := conn.Read(buf[0:])
if err != nil {
return
}
fmt.Println(string(buf[0:]))
_,err2 := conn.Write(buf[0:n])
if err2 != nil {
return
}
}
}
如果你想写一个 UDP 服务器, 这里有一个 PacketConn
的接口,和一个实现了该接口的方法:
func ListenPacket(net, laddr string) (c PacketConn, err os.Error)
这个接口的主要方法ReadFrom
和 WriteTo
用来处理数据包的读取和写入
Go 的 net 包建议使用接口类型而不是具体的实现类型。但是,通过使用它们,你失去了具体的方法,比如 SetKeepAlive
或 TCPConn
和 UDPConn
的SetReadBuffer
,除非你做一个类型转换。如何选择在于你。
原始套接字和 IPConn 类型
本节涵盖了大多数程序员可能需要的高级资料。它涉及 raw sockets
,允许程序员建立自己的 IP 协议,或使用 TCP 或 UDP 协议。
TCP 和 UDP 并不是建立在 IP 层之上唯一的协议。该网站:http://www.iana.org/assignments/protocol-numbers 列表上大约有 140 关于它们(该列表往往在Unix 系统的/etc/protocols
文件上)。TCP 和 UDP 在这个名单上分别为 6 和 17。
Go 允许你建立所谓的原始套接字,使您可以使用这些于其它协议通信,甚至建立你自己的。但它提供了最低限度的支持: 它会连接主机, 写入和读取和主机之间的数据包。在接下来的章节中,我们将着眼于设计和实现自己的基于 TCP 之上的协议; 这部分认为同样的问题存在于 IP 层。
为了简单起见,我们将使用几乎最简单的例子: 如何发送一个 ping 消息给主机。Ping 使用"echo"命令的ICMP 协议
。这是一个面向字节协议, 客户端发送一个字节流到另一个主机, 并等待主机的答复。格式如下:
- 首字节是 8, 表示 echo 消息
- 第二个字节是 0
- 第三和第四字节是整个消息的校验和
- 第五和第六字节是一个任意标识
- 第七和第八字节是一个任意的序列号
- 该数据包的其余部分是用户数据
下面的程序将准备一个 IP 连接,发送一个 ping 请求到主机,并得到答复。您可能需要 root权限才能运行成功。
/*Ping*/
package main
import (
"bytes"
"fmt"
"io"
"net"
"os"
)
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error:%s", err.Error)
os.Exit(1)
}
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage:", os.Args[0], "host")
os.Exit(1)
}
addr,err := net.ResolveIPAddr("ip", os.Args[1])
if err != nil {
fmt.Println("Resolution error", err.Error())
os.Exit(1)
}
conn,err := net.DialIP("ip4:icmp", addr, addr)
checkError(err)
var msg [512]byte
msg[0] = 8 //echo
msg[1] = 0 //code 0
msg[2] = 0 //checksum, fix later
msg[3] = 0 //checksum, fix later
msg[4] = 0 //identifier[0]
msg[5] = 13 //identifier[1]
msg[6] = 0 //sequence[0]
msg[7] = 37 //sequence[1]
len := 8
check := checkSum(msg[0:len])
msg[2] = byte(check >> 8)
msg[3] = byte(check &255)
_,err = conn.Write(msg[0:len])
checkError(err)
_,err = conn.Read(msg[0:])
checkError(err)
fmt.Println("Got response")
if msg[5] == 13 {
fmt.Println("identifier matches")
}
if msg[7] == 37 {
fmt.Println("Sequence matches")
}
os.Exit(0)
}
func checkSum(msg []byte) uint16 {
sum := 0
//assume even for now
for n := 1; n < len(msg) -1; n += 2 {
sum += int(msg[n])*256 + int(msg[n+1])
}
sum = (sum>>16)+(sum&0xffff)
sum += (sum>>16)
var answer uint16 = uint16(^sum)
return answer
}
结论
本章着重 IP, TCP 和 UDP 级别的编程。如果你想实现自己的协议,或用现有的协议建立一个客户端或服务器,这些内容往往很重要。