《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);

 
	