[golang] socket 编程

972 阅读4分钟

Socket 起源于 Unix,而 Unix 基本哲学之一就是“一切皆文件”,都可以用“打开 open –> 读写 write/read –> 关闭 close”模式来操作。Socket 就是该模式的一个实现,网络的 Socket 数据传输是一种特殊的 I/O,Socket 也是一种文件描述符。Socket 也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的 Socket 描述符,随后的连接建立、数据传输等操作都是通过该 Socket 实现的。

常用的 Socket 类型有两种:流式 Socket(SOCK_STREAM)和数据报式 Socket(SOCK_DGRAM)。流式是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用;数据报式 Socket 是一种无连接的 Socket,对应于无连接的 UDP 服务应用。

使用 TCP/IP 协议的应用程序通常采用应用编程接口:UNIX BSD 的套接字(socket)和 UNIX System V 的 TLI(已经被淘汰),来实现网络进程之间的通信。

Socket 基础知识

Socket 有两种:TCP Socket 和 UDP Socket,而要确定一个进程的需要三元组:IP 地址、端口和协议(TCP 和 UDP)。
image.png

IPv4 地址

全球因特网所采用的协议族是 TCP/IP 协议,IP 是 TCP/IP 协议中网络层的协议(核心协议)。IPv4 的地址位数为 32 位,地址格式类似这样:127.0.0.1172.122.121.111

IPv6 地址

IPv6 的地址位数为 128 位,在 IPv6 的设计过程中除了一劳永逸地解决了地址短缺问题以外,还考虑了在 IPv4 中解决不好的其它问题,主要有端到端 IP 连接、服务质量(QoS)、安全性、多播、移动性、即插即用等。地址格式类似这样:2002:c0e8:82e7:0:0:0:c0e8:82e7

go 支持的 ip 类型

type IP []byte
name := os.Args[1]
// 把一个 IPv4 或者 IPv6 的地址转化成 IP 类型
addr := net.ParseIP(name)
if addr == nil {
    fmt.Println("Invalid address")
} else {
    fmt.Println("The address is ", addr.String())
}

TCP Socket

TCPConn 可以用在客户端和服务器端来读写数据,可以通过 DialTCP 获取,当连接建立时服务器端也创建一个同类型的对象。

func (c *TCPConn) Write(b []byte) (int, error)
func (c *TCPConn) Read(b []byte) (int, error)

// network 参数是 tcp4、tcp6、tcp 中的任意一个
// laddr 表示本机地址,一般设置为 nil
// raddr 表示远程的服务地址
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)

TCPAddr 表示一个 TCP 的地址信息,可以通过 ResolveTCPAddr 获取。

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

// net 参数是 tcp4、tcp6、tcp 中的任意一个
// addr 表示域名或者 IP 地址,例如 www.google.com:80 或者 127.0.0.1:22
func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)

TCP client

模拟一个基于 HTTP 协议的客户端请求去连接一个 Web 服务端。

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("timestamp"))
    checkError(err)
    // result, err := ioutil.ReadAll(conn)
    result := make([]byte, 256)
    _, err = conn.Read(result)
    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)
    }
}

TCP server

可以通过 net 包来创建一个服务器端程序,在服务器端需要绑定服务到指定的非激活端口,并监听此端口。

func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
func (l *TCPListener) Accept() (Conn, error)
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
        }
        go handleClient(conn)
    }
}

func handleClient(conn net.Conn) {
    // 一定时间内客户端无请求发送,conn 便会自动关闭
    conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout
    request := make([]byte, 128)                          // set maxium request length to 128B to prevent flood attack
    defer conn.Close()                                    // close connection before exit
    // 保持与客户端的长连接
    for {
        // 读取客户端发来的请求
        read_len, err := conn.Read(request)

        if err != nil {
            fmt.Println(err)
            break
        }

        if read_len == 0 {
            break // connection already closed by client
        } else if strings.TrimSpace(string(request[:read_len])) == "timestamp" {
            daytime := strconv.FormatInt(time.Now().Unix(), 10)
            conn.Write([]byte(daytime))
        } else {
            daytime := time.Now().String()
            conn.Write([]byte(daytime))
        }
        
        // 每次读取到请求处理完毕后,需要清理 request,因为 conn.Read() 会将新读取到的内容 append 到原内容之后
        request = make([]byte, 128) // clear last read content
    }
}

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

UDP Socket

UDP Socket 不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP 缺少对客户端连接请求的 Accept 方法。

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 client

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)
    checkError(err)
    _, 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 checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error())
        os.Exit(1)
    }
}

UDP server

func main() {
    service := ":1200"
    udpAddr, err := net.ResolveUDPAddr("udp4", service)
    checkError(err)
    conn, err := net.ListenUDP("udp", udpAddr)
    checkError(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 checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error())
        os.Exit(1)
    }
}