Go 为开发人员提供了阻塞 I/O 模型,Gopher 只需在 Goroutine 中以最简单、最易用的“阻塞 I/O 模型”的方式,进行 Socket 操作就可以。

但这种方式是 Go 模拟出来,是为了让开发者使用起来更加简单易懂,对应的、真实的底层操作系统 Socket,实际上是非阻塞的

Go 没有使用基于线程的并发模型,而是使用了开销更小的 Goroutine 作为基本执行单元,这让每个 Goroutine 处理一个 TCP 连接成为可能,并且在高并发下依旧表现出色。

虽然目前主流 socket 网络编程模型是 I/O 多路复用模型,但考虑到这个模型在使用时的体验较差,Go 语言将这种复杂性隐藏到运行时层,并结合 Goroutine 的轻量级特性,在用户层提供了基于 I/O 阻塞模型的 Go socket 网络编程模型,这一模型就大大简化了编程难度。

1,Server 端

Go Server 端编程套路模板:

func handleConn(c net.Conn) {
    defer c.Close()
    for {
        // read from the connection
        // ... ...
        // write to the connection
        //... ...
    }
}

func main() {
    l, err := net.Listen("tcp", ":8888")
    if err != nil {
        fmt.Println("listen error:", err)
        return
    }

    for {
        c, err := l.Accept()
        if err != nil {
            fmt.Println("accept error:", err)
            break
        }
        // start a new goroutine to handle
        // the new connection.
        go handleConn(c)
    }
}

2,Client 端

Go Client 端与 Server 端建立连接的两种方式:

conn, err := net.Dial("tcp", "localhost:8888")

// 带有超时机制的建连
conn, err := net.DialTimeout("tcp", "localhost:8888", 2 * time.Second)

net.Dial 函数的第一个参数的可选值,共九个:

  • “tcp”:代表 TCP 协议,其基于的 IP 协议的版本根据参数address的值自适应。
  • “tcp4”:代表基于 IP 协议第四版的 TCP 协议。
  • “tcp6”:代表基于 IP 协议第六版的 TCP 协议。
  • “udp”:代表 UDP 协议,其基于的 IP 协议的版本根据参数address的值自适应。
  • “udp4”:代表基于 IP 协议第四版的 UDP 协议。
  • “udp6”:代表基于 IP 协议第六版的 UDP 协议。
  • “unix”:代表 Unix 通信域下的一种内部 socket 协议,以 SOCK_STREAM 为 socket 类型。
  • “unixgram”:代表 Unix 通信域下的一种内部 socket 协议,以 SOCK_DGRAM 为 socket 类型。
  • “unixpacket”:代表 Unix 通信域下的一种内部 socket 协议,以 SOCK_SEQPACKET 为 socket 类型。

3,Socket 读操作

从 Socket 读数据的三种情况:

  • Socket 中无数据:如果没有设置超时时间,那么读操作会一直阻塞
    • SetReadDeadline 方法可以设置超时时间,SetReadDeadline 方法接受一个绝对时间作为超时的 deadline。
    • 一旦通过这个方法设置了某个 socket 的 Read deadline,当发生超时后,如果我们不重新设置 Deadline,那么后面与这个 socket 有关的所有读操作,都会返回超时失败错误。
    • SetReadDeadline(time.Time{})可用于取消超时时间
  • Socket 中有部分数据:Socket 中有部分数据就绪,且数据数量小于一次读操作期望读出的数据长度
    • 读操作将会成功读出这部分数据,并返回,而不是等待期望长度数据全部读取后,再返回
  • Socket 中有足够数据:成功读出期望的数据,并返回
    • 剩下的数据会在下一次读取时获取

4,Socket 写操作

当 Write 调用的返回值 n 的值,与预期要写入的数据长度相等,且 err = nil 时,我们就执行了一次成功的 Socket 写操作,这是我们在调用 Write 时遇到的最常见的情形。

其它情况还有:

  • 写阻塞
  • 写入部分数据
  • 写入超时
    • SetWriteDeadline 方法用于设置写超时时间

5,关闭 Socket

当客户端主动关闭了 Socket,那么服务端的Read调用将会读到什么呢?这里要分“有数据关闭”和“无数据关闭”两种情况。

  • 有数据关闭:指在客户端关闭连接时,Socket 中还有服务端尚未读取的数据
    • 在这种情况下,服务端的 Read 会成功将剩余数据读取出来,
    • 最后一次 Read 操作将得到io.EOF错误码,表示客户端已经断开了连接。
  • 无数据关闭
    • 服务端调用的 Read 方法将直接返回io.EOF。

(完。)