Go 语言 socket 编程模型
阻塞 I/O 模型是对开发人员最友好的,也是心智负担最低的模型,而 I/O 多路复用的这种通过回调割裂执行流的模型,对开发人员来说过于复杂了
Go 选择了为开发人员提供阻塞 I/O 模型,在 Goroutine 中以最简单、最易用的“阻塞 I/O 模型”的方式,进行 Socket 操作。
网络轮询器(netpoller)
Go运行时中实现了网络轮询器(netpoller),netpoller 的作用,就是只阻塞执行网络 I/O 操作的 Goroutine,但不阻塞执行 Goroutine 的线程(也就是 M)
对于 Go 程序的用户层(相对于 Go 运行时层)来说,它眼中看到的 goroutine 采用了“阻塞 I/O 模型”进行网络 I/O 操作,Socket 都是“阻塞”的。这样的“假象”,是通过 Go 运行时中的 netpoller I/O 多路复用机制,“模拟”出来的,对应的、真实的底层操作系统 Socket,实际上是非阻塞的。只是运行时拦截了针对底层 Socket 的系统调用返回的错误码,并通过 netpoller 和 Goroutine 调度,让 Goroutine“阻塞”在用户层所看到的 Socket 描述符上。
比如:当用户层针对某个 Socket 描述符发起read操作时,如果这个 Socket 对应的连接上还没有数据,运行时就会将这个 Socket 描述符加入到 netpoller 中监听,同时发起此次读操作的 Goroutine 会被挂起。直到 Go 运行时收到这个 Socket 数据可读的通知,Go 运行时才会重新唤醒等待在这个 Socket 上准备读数据的那个 Goroutine。而这个过程,从 Goroutine 的视角来看,就像是 read 操作一直阻塞在那个 Socket 描述符上一样。
由于最常见的多路复用系统调用 select 有比较多的限制,比如:监听 Socket 的数量有上限(1024)、时间复杂度高,等等。所以,Go 运行时选择了在不同操作系统上,使用操作系统各自实现的高性能多路复用函数
- Linux 上的 epoll
- Windows 上的 iocp
- FreeBSD/MacOS 上的 kqueue
- Solaris 上的 event port
这样可以最大程度提高 netpoller 的调度和执行性能。