《Go语言编程》
第5 章
网 络 编 程
本章我们将全面介绍如何使用Go语言开发网络程序。Go语言标准库里提供的net包,支持基于IP层、TCP/UDP层及更高层面(如HTTP、FTP、SMTP)的网络操作,其中用于IP层的称为RawSocket。
5.1 Socket 编程
在Go语言中编写网络程序时,我们将看不到传统的编码形式。以前我们使用Socket编程时, 会按照如下步骤展开。
- 建立Socket:使用socket()函数。
- 绑定Socket:使用bind()函数。
- 监听:使用listen()函数。或者连接:使用connect()函数。
- 接受连接:使用accept()函数。
- 接收:使用receive()函数。或者发送:使用send()函数。
Go语言标准库对此过程进行了抽象和封装。无论我们期望使用什么协议建立什么形式的连接,都只需要调用net.Dial()即可。
- 5.1.1 Dial()函数
Dial()函数的原型如下:
func Dial(net, addr string) (Conn, error)
其中net参数是网络协议的名字,addr参数是IP地址或域名,而端口号以“:”的形式跟随在地址或域名的后面,端口号可选。如果连接成功,返回连接对象,否则返回error。
我们来看一下几种常见协议的调用方式。
TCP链接:
conn, err := net.Dial("tcp", "192.168.0.10:2100")
UDP链接:
conn, err := net.Dial("udp", "192.168.0.12:975")
ICMP链接(使用协议名称):
conn, err := net.Dial("ip4:icmp", "www.tianqiweiqi.com")
ICMP链接(使用协议编号):
conn, err := net.Dial("ip4:1", "10.0.0.3")
这里我们可以通过以下链接查看协议编号的含义:http://www.iana.org/assignments/protocol-num- bers/protocol-numbers.xml。
目前,Dial()函数支持如下几种网络协议:”tcp”、”tcp4″(仅限IPv4)、”tcp6″(仅限IPv6)、”udp”、”udp4″(仅限IPv4)、”udp6″(仅限IPv6)、”ip”、”ip4″(仅限IPv4)和”ip6″(仅限IPv6)。
在成功建立连接后,我们就可以进行数据的发送和接收。发送数据时,使用conn的Write() 成员方法,接收数据时使用Read()方法。
5.1.2 ICMP示例程序
下面我们实现这样一个例子:我们使用ICMP协议向在线的主机发送一个问候,并等待主机返回,具体代码如代码清单5-1所示。
代码清单5-1 icmptest.go
package main import ( “net” “os” “bytes” “fmt” ) func main() { if len(os.Args) != 2 { fmt.Println(“Usage: “, os.Args[0], “host”) os.Exit(1) } service := os.Args[1] conn, err := net.Dial(“ip4:icmp”, service) checkError(err) var msg [512]byte msg[0] = 8 // echo msg[1] = 0 // code 0 msg[2] = 0 // checksum msg[3] = 0 // checksum 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 // 先假设为偶数 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 } func checkError(err error) { if err != nil { fmt.Fprintf(os.Stderr, “Fatal error: %s”, err.Error()) os.Exit(1) } } 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:n]) if err != nil { if err == io.EOF { break } return nil, err } } return result.Bytes(), nil }
执行结果如下:
$ go build icmptest.go $ ./icmptest www.tianqiweiqi.com Got response Identifier matches Sequence matches
5.1.3 TCP示例程序
下面我们建立TCP链接来实现初步的HTTP协议,通过向网络主机发送HTTP Head请求,读取网络主机返回的信息,具体代码如代码清单5-2所示。
代码清单5-2 simplehttp.go
package main import ( “net” “os” “bytes” “fmt” ) 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 checkError(err error) { if err != nil { fmt.Fprintf(os.Stderr, “Fatal error: %s”, err.Error()) os.Exit(1) } } 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:n]) if err != nil { if err == io.EOF { break } return nil, err } } return result.Bytes(), nil }
执行这段程序并查看执行结果:
$ go build simplehttp.go $ ./simplehttp qbox.me:80 HTTP/1.1 301 Moved Permanently Server: nginx/1.0.14 Date: Mon, 21 May 2012 03:15:08 GMT Content-Type: text/html Content-Length: 184 Connection: close Location: https://qbox.me/
5.1.4 更丰富的网络通信
实际上,Dial()函数是对DialTCP()、DialUDP()、DialIP()和DialUnix()的封装。我们也可以直接调用这些函数,它们的功能是一致的。这些函数的原型如下:
func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err error) func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err error) func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error) func DialUnix(net string, laddr, raddr *UnixAddr) (c *UnixConn, err error)
之前基于TCP发送HTTP请求,读取服务器返回的HTTP Head的整个流程也可以使用代码清单5-3所示的实现方式。
代码清单5-3 simplehttp2.go
package main import ( “net” “os” “fmt” “io/ioutil” ) 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] 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) } func checkError(err error) { if err != nil { fmt.Fprintf(os.Stderr, “Fatal error: %s”, err.Error()) os.Exit(1) } }
与之前使用Dail()的例子相比,这里有两个不同:
- net.ResolveTCPAddr(),用于解析地址和端口号;
- net.DialTCP(),用于建立链接。这两个函数在Dial()中都得到了封装。
此外,net包中还包含了一系列的工具函数,合理地使用这些函数可以更好地保障程序的质量。
验证IP地址有效性的代码如下:
func net.ParseIP()
创建子网掩码的代码如下:
func IPv4Mask(a, b, c, d byte) IPMask
获取默认子网掩码的代码如下:
func (ip IP) DefaultMask() IPMask
根据域名查找IP的代码如下:
func ResolveIPAddr(net, addr string) (*IPAddr, error) func LookupHost(name string) (cname string, addrs []string, err error);