typora/note/Go/socket.md
2024-12-12 10:48:55 +08:00

4.8 KiB
Raw Blame History

Socket模型

  • 提供阻塞 I/O 模型,只需在 Goroutine 中以最简单、最易用的“阻塞 I/O 模型”的方式,进行 Socket 操作
  • 每个 Goroutine 处理一个 TCP 连接成为可能,并且在高并发下依旧表现出色
  • 在运行时中实现了网络轮询器netpoller)netpoller 的作用,就是只阻塞执行网络 I/O 操作的 Goroutine但不阻塞执行 Goroutine 的线程(也就是 M
  • Go 程序的用户层(相对于 Go 运行时层)来说,它眼中看到的 goroutine 采用了“阻塞 I/O 模型”进行网络 I/O 操作Socket 都是“阻塞”的

netpoller 网络轮询器

  • I/O 多路复用机制
  • 真实的底层操作系统 Socket是非阻塞的
  • 运行时拦截了针对底层 Socket 的系统调用返回的错误码,并通过 netpoller 和 Goroutine 调度,让 Goroutine“阻塞”在用户层所看到的 Socket 描述符上

netpoller流程

  • 用户层针对某个 Socket 描述符发起read操作时如果这个 Socket 对应的连接上还没有数据,运行时将这个 Socket 描述符加入到 netpoller 中监听,同时发起此次读操作的 Goroutine 会被挂起
  • Go 运行时收到 Socket 数据可读的通知Go 运行时重新唤醒等待在这个 Socket 上准备读数据的 Goroutine
  • 从 Goroutine 的视角来看,就像是 read 操作一直阻塞在 Socket 描述符上

不同系统io多路复用模型

  • Linux epoll
  • Windows iocp
  • FreeBSD/MacOS kqueue
  • Solaris event port

socket 服务端监听listen与接收Accept

  • 服务端程序通常采用一个 Goroutine 处理一个连接
 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)
     }
 }

socket客户端

conn, err := net.Dial("tcp", "localhost:8888")
conn, err := net.DialTimeout("tcp", "localhost:8888", 2 * time.Second)

socket 全双工通信

  • 通信双方通过各自获得的 Socket可以在向对方发送数据包的同时接收来自对方的数据包
  • 任何一方的操作系统,都会为已建立的连接分配一个发送缓冲区和一个接收缓冲区
  • 客户端会通过成功连接服务端后得到的 conn封装了底层的 socket向服务端发送数据包
  • 数据包会先进入到己方(客户端)的发送缓冲区中,之后,这些数据会被操作系统内核通过网络设备和链路,发到服务端的接收缓冲区中,服务端程序再通过代表客户端连接的 conn 读取服务端接收缓冲区中的数据

socket 读操作

  • Socket 中无数据,读操作阻塞
  • Socket 中有部分数据,成功读出这部分数据,并返回,而不是等待期望长度数据全部读取后,再返回
  • Socket 中有足够数据读出read操作的数据剩余数据分多次读取
  • Socket 设置读超时SetReadDeadline 方法接受一个绝对时间作为超时的 deadline一旦通过这个方法设置了某个 socket 的 Read deadline无论后续的 Read 操作是否超时,只要不重新设置 Deadline后面与这个 socket 有关的所有读操作,都会返回超时失败错误
  • 取消超时设置,可以使用 SetReadDeadlinetime.Time{}

socket 写操作

  • Write 调用的返回值 n 的值,与预期要写入的数据长度相等,且 err = nil 时,代表写入成功
  • 写阻塞,发送方将对方的接收缓冲区,以及自身的发送缓冲区都写满后,再调用 Write 方法就会出现阻塞
  • 写入部分数据
  • 写入超时SetWriteDeadline如果出现超时无论后续 Write 方法是否成功,如果不重新设置写超时或取消写超时,后续对 Socket 的写操作都将超时失败

socket 并发读写

  • 可以用,但是没必要
  • 并发写,写入顺序会乱
  • 并发读,读取的数据是一部分,业务处理逻辑复杂

socket 关闭

有数据关闭

  • 有数据关闭”是指在客户端关闭连接SocketSocket 中还有服务端尚未读取的数据
  • 服务端的 Read 会成功将剩余数据读取出来
  • 最后一次 Read 操作将得到io.EOF错误码

无数据关闭

  • 服务端直接读到io.EOF

客户端关闭 Socket 后,如果服务端 Socket 尚未关闭,这个时候服务端向 Socket 的写入操作依然可能会成功,因为数据会成功写入己方的内核 socket 缓冲区中,即便最终发不到对方 socket 缓冲区