golang netpoller封装了不同平台的网络模型,由于我们的游戏服务器程几乎只运行在linux上,这里介绍epoll。epoll是linux下非阻塞的高效的网络模型,对大量文件描述符读写拥有优异的性能。下面先简单介绍epoll的原理,再从源码角度来看golang是如何封装epoll的。
一、epoll实现
1.1 例子
先通过一个简单的例子来看一下epoll是如何使用的,流程如下:
- 创建套接字、绑定、监听
epoll_create接口创建epoll对象epoll_ctl接口注册套接字的事件epoll_wait接口轮询是否有事件发生,并通过events参数返回就绪(触发)的事件列表
1 | int s = socket(AF_INET, SOCK_STREAM, 0); |
1.2 结构体
这里只例出了介绍epoll原理必要的字段
1 | struct eventpoll { |
1.3 原理
- 当调用
epoll_create,其实是创建了一个eventpoll结构体对象,在epoll运行期间的相关数据都存在此结构里面。 - 接着是通过
epoll_ctl注册socket s感兴趣的事件,结构中的rbr就是用来存放所有注册的socket。同时epoll_ctl接口还会注册回调函数ep_poll_callback。 - 网卡收到数据后,会把数据复制到内核空间,并触发回调函数
ep_poll_callback,ep_poll_callback会把就绪的fd指针放入rdllist,并检查wq中是否有阻塞的线程,如果有则唤醒它们。 - 调用
epoll_wait函数检查是否有事件触发(就绪),如果有,则通过参数2返回(这里其实就是检查rdllist是否为空,如果不为空则返回事件列表)。参数4为阻塞时间,若不为0,在rdllist为空时,调用epoll_wait的线程会被阻塞,并放到wq中,如果阻塞时间结束,仍然没有事件发生,则被唤醒;如果等待期间有事件发生内核触发ep_poll_callback回调并唤醒这个fd上阻塞的线程。
1.4 事件
EPOLLIN :fd可读
EPOLLOUT:fd可写
EPOLLPRI:fd有紧急事件数据到达
EPOLLERR:fd发生错误
EPOLLHUP:fd被挂断
EPOLLET: 设置epoll为边沿触发,默认为水平触发
EPOLLONESHOT:只监听一次事件
二、Golang Netpoll 实现
使用golang可以快速开发一个网络应用,通常不需要借助第三方网络库就能满足需求。那么它是如何做到简单高效的呢?理解了epoll的原理后,下面介绍golang netpoll是如何实现的。
1.1 例子
还是先通过一个简单的例子回顾一下golang网络模块基本用法
1 | func main() { |
1.2 结构体
TCP网络listener, 当我们调用net.Listen时,会返回一个TCPListener对象
1 | type TCPListener struct { |
网络文件描述符,其实是对FD的封装
1 | type netFD struct { |
FD是文件描述符。net和os包使用此类型表示一个网络连接或os文件
1 | type FD struct { |
对连接的读写都是通过此结构的方法实现的
1 | type pollDesc struct { |
1.3 源码
上面的例子主要涉及了接口Listen、Accept、Read、Write、Close,我们通过源码来看下这几个接口是怎么实现的,以及在上层无感知的情况下如何跟epoll完美绑定到一起的。
1.1 Listen
当我们在应用层调用net.Listen时,Listen接口会依次调用:
sysListener.listenTCP:创建TCPListener对象socket:返回一个使用network poller异步I/O的网络文件描述符(在socket函数中会创建netFD对象)。netFD.listenStream:设置套接字参数,绑定,监听。ollDesc.init:init函数中会调用runtime_pollServerInit、runtime_pollOpen。runtime_pollServerInit:调用epoll_create创建epoll。runtime_pollOpen:调用epoll_ctl添加监听事件。
就这样Listen接口成功和epoll的epoll_create、epoll_ctl接口关联起来了,下面是详细过程。
1 | func Listen(network, address string) (Listener, error) { |
1.2 Accept
Accept的调用流程相对简单,当例子中调用listen.Accept时,会依次调用TCPListener.accept(), netFD.accept(), FD.accept()。FD.accept()会重置pollDesc中的rg,并调用原始套接字的accept接口,直到有连接到来或发生错误返回,如果返回EGAIN,则当前g被gorpark。若成功等到连接则创建netFD对象,再调用netFD.init()进入跟Listen中netFD.init()一样的流程。这里需要注意的是同一进程epoll只会被创建一次(runtime_pollServerInit用的sync.One不管调用多少次,只有第一次会被执行)
1 | func (ln *TCPListener) accept() (*TCPConn, error) { |
1.3 Read/Write
Read和Write的流程非常相似,这里只介绍Read。在调用例子中conn.Read(buf)时,调用流程为conn.Read->netFD.Read->FD.Read。FD.Read首先重置pollDesc中的rg为0,检查是否有可读数据,有则读取返回;若返回EAGAIN则gopark当前g。
1 | // Read implements the Conn Read method. |
1.4 轮询epoll
在1.1~1.3的源码中,我们已经看到了golang对epoll中epoll_create、epoll_ctrl的封装,但没看到epoll_wait。而且在1.2,1.3的源码中,accept, Read如果返回EAGAIN时,当前g会被gopark。下面我们就来看下epoll_wait是何时被调用的,以及由于EAGAIN被gopark的g是何时被唤醒的。在sysmon和findrunnable中会调用netpoll函数,返回所有就绪fd上的g,并加入到全局队列中。accept、Read、Write被gopark的g的指针都保存在pollDesc中,所以fd一旦就绪,我们可通过wg或rg找到g并交runtime,runtime会把它从_Gwaiting状态置为_Grunnable,此时就绪的g就可以接着gopark时的状态继续执行。需要注意的是ep_poll_callback只会唤醒内核线程,被gopark的g则在runtime中唤醒的。
1 | func netpoll(block bool) gList { |
参考
https://taohuawu.club/go-netpoll-io-multiplexing-reactor
http://gityuan.com/2019/01/06/linux-epoll/
https://zhuanlan.zhihu.com/p/64746509