Network Poller 如何工作
Network Poller初始化
- poll_runtime_pollServerInit()
- 使用原子操作保证只初始化一次
- 调用netpollinit()
pollcache与pollDesc
type pollCache struct {
lock mutex
first *pollDesc // 放链表头
}
type pollDesc struct {
link *pollDesc // 后续指针
// ...
fd uintptr // socket的ID
rg uintptr // pdReady(1), pdWait(2), 等待读的协程的地址
wg uintptr // pdReady(1), pdWait(2), 等待写的协程的地址
}
- pollcache: 一个带锁的链表头
- polDesc: 链表的成员
- pollDesc是runtime包对Socket的详细描述(记录了哪些协程对该socket感兴趣)
- rg, rw: 初始(0), pdReady(1), pdWait(2), 或者等待协程G的地址
Network Poller新增监听Socket
- poll_runtime_pollOpen()
- 在pollcache链表中分配一个pollDesc
- 初始化pollDesc(rg, rw为0)
- 调用netpollopen(见上节, 注册各种epoll事件)
Network Poller收发数据
收发数据分为两个场景:
- 协程需要收发数据时, Socket已经可读可写
- 协程需要收发数据时, Socket暂时无法读写
场景1: Socket已经可读可写
- runtime循环调用netpoll()方法(g0协程, 最终是通过垃圾回收器调用, gcStart, 一个hook)
- 发现Socket可读写时, 给对应的rg或者wg置为pdReady(1)
- 协程调用poll_runtime_pollWait()
- 判断rg或者wg已经置为pdReady(1), 返回0
场景2: Socket暂时无法读写
- runtime循环调用netpoll()方法
- 协程调用poll_runtime_pollWait()
- 发现对应的rg或者wg为0
- 给对应的rg或者wg置为协程地址
- 休眠等待
- runtime循环调用netpoll方法
- 发现Socket可读写时, 查看对应的rg或者wg
- 若为协程地址, 返回协程地址(有协程在监听)
- 调度器开始调度对应协程
总结
- Network Poller是Runtime的强大工具
- 抽象了多路复用器的操作
- Network Poller可以自动监测多个Socket状态
- 在Socket状态可用时,快速返回成功
- 在Socket状态不可用时,休眠等待
Go 抽象Socket
net包
- net包是go原生的网络包
- net包支持了TCP, UDP, HTTP等网络操作
lis, err := net.Listen("tcp", ":8888") // 监听8888端口
if err != nil {
panic(err)
}
conn, err := lis.Accept()
if err != nil {
panic(err)
}
var body [100]byte
for {
_, err := conn.Read(body[:])
if err != nil {
break
}
fmt.Printf("收到消息: %s\n", body)
_, err = conn.Write(body[:])
if err != nil {
break
}
}
net.Listen()
- 新建Socket, 并执行bind操作
- 新建一个FD(net包对Socket的详情描述)
- 返回一个TCPListener对象
- 将TCPListener的FD信息加入监听
- TCPListener对象本质上是一个Listen状态的Socket
TCPListener.Accept()
- 直接调用Socket的accept()
- 如果失败,休眠等待新的连接
- 将新的Socket包装为TCPConn变量返回
- 将TCPConn的FD信息加入监听
- TCPConn本质上是一个ESTABLISHED状态的Socket
TCPConn.Read() / Write()
- 直接调用Socket原生读写方法
- 如果失败,休眠等待可读/可写
- 被唤醒后调用系统Socket
总结
- net包抽象了TCP网络操作
- 使用net.Listen()得到TCPListener(LISTEN状态的Socket)
- 使用TCPListener..Accept()得到TCPConn(ESTABLISHED)
- TCPConn.Read() / Write() 进行读写Socket的操作
- Network Poller 作为上述功能的底层支撑
Go搭建TCP Server
结合阻塞模型和多路复用
- 用主协程监听Listener
- 每个Conn使用一个新携程处理
package main
import (
"fmt"
"net"
)
func handleConnection(conn net.Conn) {
defer func() {
if err := conn.Close(); err != nil {
panic(err)
}
}()
var body [100]byte
for {
_, err := conn.Read(body[:])
if err != nil {
break
}
fmt.Printf("收到消息: %s\n", body)
_, err = conn.Write(body[:])
if err != nil {
break
}
}
}
func main() {
lis, err := net.Listen("tcp", ":8888") // 监听8888端口
if err != nil {
panic(err)
}
for {
conn, err := lis.Accept()
if err != nil {
panic(err)
}
go handleConnection(conn)
}
}
总结
系统IO模型
- 操作系统提供了Socket作为TCP通信的抽象
- lO模型指的是操作Socket的方案
- 阻塞模型最利于业务编写,但是性能差
- 多路复用性能好,但业务编写麻烦
Epoll的抽象
- Go将多路复用器的操作进行了抽象和适配:
- 将新建多路复用器抽象为了netpollinit()
- 将插入监听事件抽象为了netpollopent()
- 将查询事件抽象为了netpoll()
- 但不是返回事件, 而是返回等待事件的协程列表
Network Poller的原理
- Network Poller是Runtime的强大工具
- 抽象了多路复用器的操作
- Network Poller可以自动监测多个Socket状态
- 在Socket状态可用时,快速返回成功
- 在Socket状态不可用时,休眠等待
Net包
- net包抽象了TCP网络操作使用net.Listen()得到TCPListener(LISTEN状态的Socket)
- 使用TCPListener.Accept()得到TCPConn(ESTABLISHED)
- TCPConn.Read()/Write进行读写Socket的操作
- Network Poller作为上述功能的底层支撑
goroutine-per-connection
- 用主协程监听Listener
- 每个Conn使用一个新协程处理
- 结合了多路复用的性能和阻塞模型的简洁