42. Go tcp客户端、服务端编程

内容参考net包,net包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和Unix域socket。

虽然本包提供了对网络原语的访问,大部分使用者只需要Dial、Listen和Accept函数提供的基本接口;以及相关的Conn和Listener接口。crypto/tls包提供了相同的接口和类似的Dial和Listen函数。

服务端

在服务器端我们需要绑定服务到指定的非激活端口, 并监听此端口;当有客户端请求到达的时候可以接收到来自客户端连接的请求。

package main

import (
    "fmt"
    "net"
    "os"
    "strings"
    "time"
)

func main() {
    // 监听的服务器端口
    service := "127.0.0.1:1200"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
    // 在服务器端我们需要绑定服务到指定的非激活端口, 并监听此端口,
    // 当有客户端请求到达的时候可以接收到来自客户端连接的请求
    //
    // func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error)
    // ListenTCP在本地TCP地址laddr上声明并返回一个*TCPListener, net参数必须是"tcp", "tcp4", "tcp6",
    // 如果laddr的端口字段为0, 函数将选择一个当前可用的端口, 可以用Listener的Addr方法获得该端口
    listener, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
    for {
        // 接受每一种请求
        // func (l *TCPListener) Accept() (Conn, error)
        // Accept用于实现Listener接口的Accept方法
        // 他会等待下一个呼叫, 并返回一个该呼叫的Conn接口
        conn, err := listener.Accept()
        // 当有错误发生的情况下最好是由服务端记录错误, 然后当前连接的客户端直接报错而退出, 从而不会
        // 影响到当前服务端运行的整个服务
        if err != nil {
            continue
        }
        // 处理客户端的连接
        go handleClient(conn)
    }
}

func handleClient(conn net.Conn) {
    // 超时时间, 当一定时间内客户端无请求发送, conn便会自动关闭, 下面的for循环即会因为连接已关闭而跳出
    conn.SetReadDeadline(time.Now().Add(1 * time.Minute))
    // request在创建时需要指定一个最大长度以防止flood attack; 每次读取到请求处理完毕后,
    // 需要清理request, 因为conn.Read()会将新读取到的内容append到原内容之后
    // request 为提供的读取的最长缓冲区, 缓冲区满后会自动回写到客户端接收, 因此客户端需要注意接收到数据的完整性后在处理
    // 但是也并非都是写满后才返回给客户端, 有可能提前返回数据, 需要一个协议
    // 如果该缓冲区给的不够, 可能造成客户端传递过来的信息被截断, 无法得到预期的结果
    request := make([]byte, 9)
    defer conn.Close()
    for {
        // 不断读取客户端发来的请求, 由于我们需要保持与客户端的长连接, 所以不能在读取完一次请求后就关闭连接
        // 将读取到的数据追加保存到request中
        readLen, err := conn.Read(request)
        if err != nil {
            fmt.Println(err)
            break
        }

        // 如果没有读取到客户端任何信息, 则默认客户端已经关闭
        if readLen == 0 {
            break
        } else if strings.TrimSpace(string(request[:readLen])) == "timestamp" {
            date := time.Now().Format("2006-01-02 15:04:05")
            conn.Write([]byte(date))
        } else {
            conn.Write([]byte(request[:readLen]))
        }

        // 每次发送写完毕后都清除缓存空间, 防止追加
        request = make([]byte, 8)
    }
}

客户端

首先程序将用户的输入作为参数service传入net.ResolveTCPAddr获取一个tcpAddr,然后把tcpAddr传入DialTCP后创建了一个TCP连接conn,通过conn来发送请求信息,最后通过ioutil.ReadAll从conn中读取全部的文本,也就是服务端响应反馈的信息。

package main

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

func main() {
    // 获取提供服务的服务器ip地址
    service := os.Args[1]
    // func ResolveTCPAddr(net, addr string) (*TCPAddr, error)
    // ResolveTCPAddr获取一个TCPAddr, 一个TCPAddr类型, 他表示一个TCP的地址信息
    // ResolveTCPAddr将addr作为TCP地址解析并返回
    // 参数addr格式为"host:port"或"[ipv6-host%zone]:port", 解析得到网络名和端口名
    // net必须是"tcp", "tcp4"或"tcp6"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
    // 通过net包中的DialTCP函数来建立一个TCP连接, 并返回一个TCPConn类型的对象
    // 当连接建立时服务器端也创建一个同类型的对象, 此时客户端和服务器端通过各自拥
    // 有的TCPConn对象来进行数据交换
    //
    // 一般而言, 客户端通过TCPConn对象将请求信息, 发送到服务器端, 读取服务器端响应的信息
    // 服务器端读取并解析来自客户端的请求并返回应答信息, 这个连接只有当任一端关闭了连接之后才失效,
    // 不然这连接可以一直在使用
    //
    // func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error)
    // DialTCP在网络协议net上连接本地地址laddr和远端地址raddr
    // net必须是"tcp", "tcp4", "tcp6"
    // 如果laddr不是nil, 将使用它作为本地地址, 否则自动选择一个本地地址
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
    // 向连接的服务器端写入信息
    // net包中有一个类型TCPConn, 这个类型可以用来作为客户端和服务器端交互的通道, 他有两个主要的函数:
    // func (c *TCPConn) Write(b []byte) (n int, err os.Error)
    // func (c *TCPConn) Read(b []byte) (n int, err os.Error)
    // TCPConn可以用在客户端和服务器端来读写数据
    //_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
    _, err = conn.Write([]byte("timestamp"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
    _, err = conn.Write([]byte("a~~~"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
    _, err = conn.Write([]byte("b~~~~~"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
    _, err = conn.Write([]byte("c~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
    // 当客户端停止写入时, 需要告诉服务端, 信息发送终止, 服务端就返回全部的数据
    if err = conn.CloseWrite(); err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
    // 读取服务器端响应的全部内容
    // ReadAll从r读取数据直到EOF或遇到error, 返回读取的数据和遇到的错误
    // 成功的调用返回的err为nil而非EOF, 因为本函数定义为读取r直到EOF, 它不会将读取返回的EOF视为应报告的错误
    result, err := ioutil.ReadAll(conn)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
    // 输出获取到的内容
    fmt.Println(string(result))
    os.Exit(0)
}

服务端启动

服务端每次请求结束后都会以文件结束符标志输出。

zhgxun-pro:go zhgxun$ go run server.go 
EOF
EOF
EOF
EOF
EOF
EOF

客户端输出

当客户端停止写入时,服务端将全部数据返回。

zhgxun-pro:go zhgxun$ 
zhgxun-pro:go zhgxun$ go run client.go 127.0.0.1:1200
2017-09-17 21:07:44a~~~b~~~~~c~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
zhgxun-pro:go zhgxun$ go run client.go 127.0.0.1:1200
2017-09-17 21:07:46a~~~b~~~~~c~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
zhgxun-pro:go zhgxun$ go run client.go 127.0.0.1:1200
2017-09-17 21:07:47a~~~b~~~~~c~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
zhgxun-pro:go zhgxun$ go run client.go 127.0.0.1:1200
2017-09-17 21:07:48a~~~b~~~~~c~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
zhgxun-pro:go zhgxun$ go run client.go 127.0.0.1:1200
2017-09-17 21:07:49a~~~b~~~~~c~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
zhgxun-pro:go zhgxun$ go run client.go 127.0.0.1:1200
2017-09-17 21:07:50a~~~b~~~~~c~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
zhgxun-pro:go zhgxun$