以前主要使用的是Linux C网络编程,现在学习了Golang,就总结一下Go中的socket编程,本文基于Go 1.7.4,和socket相关的代码在go/src/net/net.go文件中。

网络协议模型回顾

我们先来简单回顾一下网络写协模型。说到网络协议一般就两个模型:概念意义上的OSI(Open Systems Interconnect)的七层协议和实际中使用的TCP/IP四层协议。

OSI七层网络模型

iso.gif

从第一层到第七层分别是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

TCP/IP四层网络模型:

tcp_stack.gif

从第一层到第四层分别是:网络接口层、网间层、传输层、应用层

最后用一张图来结束回顾:

network-protocol.gif

完整版可下载:http://www.colasoft.com.cn/download/protocols_map.php

Go socket编程

首先需要明确一点,这里介绍socket编程的主要是面向三层和四层的协议,再具体点就是主要是针对IP协议、TCP、UDP的,像HTTP这种基于TCP的七层协议不在讨论范围内。

通用socket编程API

Go中socket编程的API都在net包中。其实和C中类似,Go提供了Dial函数来连接服务器,使用Listen监听,Accept接受连接,所以如果写过其他语言(尤其是POSIX标准的)的网络编程的话,其实Go的网络编程也主要就是熟悉一下API而已,所以我们就直入主题了。

Go socket编程中最常用的类型Conn

type Conn interface {
        // Read reads data from the connection.
        // Read can be made to time out and return an Error with Timeout() == true
        // after a fixed time limit; see SetDeadline and SetReadDeadline.
        Read(b []byte) (n int, err error)

        // Write writes data to the connection.
        // Write can be made to time out and return an Error with Timeout() == true
        // after a fixed time limit; see SetDeadline and SetWriteDeadline.
        Write(b []byte) (n int, err error)

        // Close closes the connection.
        // Any blocked Read or Write operations will be unblocked and return errors.
        Close() error

        // LocalAddr returns the local network address.
        LocalAddr() Addr

        // RemoteAddr returns the remote network address.
        RemoteAddr() Addr

        // SetDeadline sets the read and write deadlines associated
        // with the connection. It is equivalent to calling both
        // SetReadDeadline and SetWriteDeadline.
        //
        // A deadline is an absolute time after which I/O operations
        // fail with a timeout (see type Error) instead of
        // blocking. The deadline applies to all future and pending
        // I/O, not just the immediately following call to Read or
        // Write. After a deadline has been exceeded, the connection
        // can be refreshed by setting a deadline in the future.
        //
        // An idle timeout can be implemented by repeatedly extending
        // the deadline after successful Read or Write calls.
        //
        // A zero value for t means I/O operations will not time out.
        SetDeadline(t time.Time) error

        // SetReadDeadline sets the deadline for future Read calls
        // and any currently-blocked Read call.
        // A zero value for t means Read will not time out.
        SetReadDeadline(t time.Time) error

        // SetWriteDeadline sets the deadline for future Write calls
        // and any currently-blocked Write call.
        // Even if write times out, it may return n > 0, indicating that
        // some of the data was successfully written.
        // A zero value for t means Write will not time out.
        SetWriteDeadline(t time.Time) error
}

Conn是一个接口,在Go中代表一个普通的面向流的网络连接。该接口定义了8个方法,基本上都是最基础的网络操作函数,后面我们会看到TCP和UDP分别实现了该接口,并且加入了一些自己的特性。需要注意的是,不同的goroutine是可以并发的调用同一个Conn的方法的。

再看Dial函数:

func Dial(network, address string) (Conn, error)

如前面所述,该函数用于连接服务器。

  • network:用于指定网络类型,目前支持的值有:tcptcp4(IPv4-only)、tcp6(IPv6-only)、udpudp4(IPv4-only)、udp6(IPv6-only)、ipip4(IPv4-only)、ip6(IPv6-only)、unixunixgramunixpacket
  • address:指定要连接的地址,对于TCP和UDP来说,地址格式为host:port。对于IPv6因为地址中已经有冒号了,所以需要用中括号将IP地址括起来,比如[::1]:80。如果省略掉host的话,比如:80,就认为是本地系统。

一些例子:

Dial("tcp", "192.0.2.1:80")
Dial("tcp", "golang.org:http")
Dial("tcp", "[2001:db8::1]:http")
Dial("tcp", "[fe80::1%lo0]:80")
Dial("tcp", ":80")

如果是IP网络的话,network必须是ipip4ip6,且必须在后面加上冒号说明协议号或者名字,比如:

Dial("ip4:1", "192.0.2.1")
Dial("ip6:ipv6-icmp", "2001:db8::1")

如果address对应多个地址(比如可能是一个域名对用多个IP),那么Dial会挨个尝试直到成功连上某一个。

除了Dial,还有一个DialTimeout多提供了一个超时的功能:

func DialTimeout(network, address string, timeout time.Duration) (Conn, error)

OK,了解了ConnDial我们就可以写一个小的程序来模拟HTTP中的HEAD方法了。

Example 1

main.go:

package main

import (
    "fmt"
    "io/ioutil"
    "net"
)

func main() {
    conn, err := net.Dial("tcp", "time-track.cn:80")
    checkErr(err)

    _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
    checkErr(err)

    result, err := ioutil.ReadAll(conn)
    checkErr(err)

    fmt.Println(string(result))
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

运行go run main.go

HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 1452
Content-Type: text/html
Server: Microsoft-IIS/8.5
Set-Cookie: CookieZJWFANGDAOLIAN=113.200.50.42#2017-03-07-14#111.67.196.107; expires=07-03-2017 23:59:59; path=/
Set-Cookie: ASPSESSIONIDQCADSRQC=GAOJKJCDPPGPDFNIABBDGCNI; path=/
X-Powered-By: ASP.NET

接下来我们看ListenAccept

type Listener interface {
        // Accept waits for and returns the next connection to the listener.
        Accept() (Conn, error)

        // Close closes the listener.
        // Any blocked Accept operations will be unblocked and return errors.
        Close() error

        // Addr returns the listener's network address.
        Addr() Addr
}

func Listen(net, laddr string) (Listener, error)

Listener接口定义了普通的面向流的监听器,不同的goroutine可以并发的调用同一个监听器的方法。

Listen函数创建一个监听,

  • net:网络类型必须是面向流的网络,目前可选值为tcptcp4tcp6unixunixpacket
  • laddr:指定监听本地哪些网络接口,对于TCP和UDP,格式为host:port。如果省略host,表示监听所有本地地址。

现在我们来写一个简单的echo服务器程序:

Example 2:

package main

import (
    "io"
    "log"
    "net"
)

func main() {
    l, err := net.Listen("tcp", ":2000")
    checkErr(err)

    defer l.Close()

    for {
        conn, err := l.Accept()
        if err != nil {
            log.Fatal(err)
            continue
        }

        go func(c net.Conn) {
            io.Copy(c, c)
            c.Close()
        }(conn)
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

这里我们对于每个客户端的请求,都分配一个goroutine去处理,这样服务端就不会阻塞在某一个客户端上了,这和C中的fork机制是一样的。

所以,掌握了ConnDialListenAccept等API,我们就可以在简单的Go socket编程了。上面我们也看到,这些API一般都有一个参数用来指定具体的网络类型,比如是IP还是TCP,亦或是UDP、UNIX套接字等,也就是说它将多种不同的协议都整合到了同一些API之中,记忆和使用起来也比较方便,。当然,针对每种具体的协议,也有特定的API,其实质也就是实现了诸如Conn等接口,当然通用的socket API也实现了这些接口(比如conn类型就是Conn接口的通用实现),只不过只是实现了一些比较通用的方法等,但是特定协议的socket API会针对本协议多提供一些方法等。Go推荐使用通用的API进行socket编程,而不是具体某种协议的socket API,除非我们需要使用某些特定的方法。当然,即使要使用也可以使用类型转换等。下面我们分别对各种协议特定的socket API进行简单的介绍,主要目的是熟悉一下类型定义和API。

IP协议的API

Go用IP类型表示一个IP地址(IPv4和IPv6):

type IP []byte

func ParseIP(s string) IP

一般我们通过上面列的ParseIP函数将一个合法的string类型(比如"192.0.2.1""2001:db8::68"等)的IP转换为IP类型的IP。该类型也提供了许多方法用来操作对IP地址进行操作。

Go用IPAddr类型来表示一个IP端:

type IPAddr struct {
        IP   IP
        Zone string // IPv6 scoped addressing zone
}

func ResolveIPAddr(net, addr string) (*IPAddr, error)

func (a *IPAddr) Network() string
func (a *IPAddr) String() string

一般我们使用ResolveIPAddr来获取一个IPAddr类型的指针:

  • net:指明IP网络,可选值为"ip", "ip4", "ip6"。
  • addr:指定网络地址,可以是域名、IP等。

后面我们会看到TCP会有一个ResolveTCPAddr,UDP会有一个ResolveUDPAddr,UNIX套接字会有一个ResolveUnixAddr,其用法和作用其实都是类似的。IPAddr只有两个方法。

IP协议中IPConn实现了Conn接口(同时也实现了PacketConn接口),用来表示一个IP连接:

type IPConn struct {
        // contains filtered or unexported fields
}

func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error)
func ListenIP(netProto string, laddr *IPAddr) (*IPConn, error)

// 方法省略

最后再介绍两个和IP相关的类型IPMaskIPNet,分别代表IP掩码(实质也是IP)和一个IP网络,其实就是我们日常说的子网掩码和子网:

type IPMask []byte

type IPNet struct {
        IP   IP     // network number
        Mask IPMask // network mask
}

总结,其实如前面所说,每种具体的协议其实都是在通用的协议实现的基础上又增加了一些本协议特有的东西,在API层面很多时候就是增加了一些前后缀,然后新增了一些特有的API而已。所以后面的几种协议的介绍会更加精简。最后以一个简单实现的ping作为例子,ping使用的是ICMP协议,协议格式为:

  • 第1个字节是8,表名是echo消息
  • 第2个字节是0
  • 第3、4个字节是整个消息的校验和
  • 第5、6个字节是任意标识符
  • 第7、8个字节是任意的序列号
  • 剩余部分是用户数据。
package main

import (
    "os"
    "fmt"
    "net"
)

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)
    checkErr(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])
    checkErr(err)

    _, err = conn.Read(msg[0:])
    checkErr(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
}

func checkErr(err error)  {
    if err != nil {
        //fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        //os.Exit(1)
        panic(err)
    }
}

NB:运行这个程序需要root权限。

TCP协议的API

TCPAddr类型表示一个TCP连接端:

type TCPAddr struct {
        IP   IP
        Port int
        Zone string // IPv6 scoped addressing zone
}

// 使用该函数获取一个*TCPAddr
func ResolveTCPAddr(net, addr string) (*TCPAddr, error)

TCPConn类型表示一个TCP连接:

type TCPConn struct {
        // contains filtered or unexported fields
}

func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error)

TCPListener类型表示一个TCP监听:

type TCPListener struct {
        // contains filtered or unexported fields
}

func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error)

最后我们实现一个简单的时间服务器作为示例:

package main

import (
    "net"
    "time"
)

func main()  {
    service := ":2000"
    tcpAddr, err := net.ResolveTCPAddr("tcp", service)
    checkErr(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkErr(err)

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }

        daytime := time.Now().String()
        conn.Write([]byte(daytime))
        conn.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

UDP协议的API

UDPAddr类型表示一个UDP端:

type UDPAddr struct {
        IP   IP
        Port int
        Zone string // IPv6 scoped addressing zone
}

// 获取一个*UDPAddr
func ResolveUDPAddr(net, addr string) (*UDPAddr, error)

UDPConn类型表示一个UDP连接:

type UDPConn struct {
        // contains filtered or unexported fields
}

func DialUDP(net string, laddr, raddr *UDPAddr) (*UDPConn, error)
func ListenMulticastUDP(network string, ifi *Interface, gaddr *UDPAddr) (*UDPConn, error)
func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error)

最后我们再用UDP实现一下上一节的时间服务器:

package main

import (
    "net"
    "time"
)

func main() {
    service := ":2000"
    udpAddr, err := net.ResolveUDPAddr("udp4", service)
    checkErr(err)

    conn, err := net.ListenUDP("udp", udpAddr)
    checkErr(err)

    for {
        handleClient(conn)
    }
}

func handleClient(conn *net.UDPConn) {
    var buf [512]byte

    _, addr, err := conn.ReadFromUDP(buf[0:])
    if err != nil {
        return
    }

    daytime := time.Now().String()

    conn.WriteToUDP([]byte(daytime), addr)
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

UNIX套接字的API

UnixAddr类型代表一个UNIX套接字端:

type UnixAddr struct {
        Name string
        Net  string
}

func ResolveUnixAddr(net, addr string) (*UnixAddr, error)

UnixConn类型代表一个UNIX套接字连接:

type UnixConn struct {
        // contains filtered or unexported fields
}

func DialUnix(net string, laddr, raddr *UnixAddr) (*UnixConn, error)
func ListenUnixgram(net string, laddr *UnixAddr) (*UnixConn, error)

UnixListener类型代表一个UNIX监听:

type UnixListener struct {
        // contains filtered or unexported fields
}

func ListenUnix(net string, laddr *UnixAddr) (*UnixListener, error)

最后再用Unix socket实现一个echo servier:

package main

import (
    "net"
    "io"
)

func main()  {
    unixAddr, err := net.ResolveUnixAddr("unix", "/tmp/echo.sock")
    checkErr(err)

    l, err := net.ListenUnix("unix", unixAddr)
    checkErr(err)

    for {
        conn, err := l.Accept()
        if err != nil {
            continue
        }

        go func(c net.Conn) {
            io.Copy(c, c)
            c.Close()
        }(conn)
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

感觉用Go写网络程序比C简单一些,不需要记很多结构体等。最重要的是跨平台...感觉已经不想再写C了...

参考:

  1. Go官方文档及源码。
  2. Network Programming with Go.