golang netpoller封装了不同平台的网络模型,由于我们的游戏服务器程几乎只运行在linux上,这里介绍epoll。epoll是linux下非阻塞的高效的网络模型,对大量文件描述符读写拥有优异的性能。下面先简单介绍epoll的原理,再从源码角度来看golang是如何封装epoll的。
一、epoll实现
1.1 例子
先通过一个简单的例子来看一下epoll
是如何使用的,流程如下:
- 创建套接字、绑定、监听
epoll_create
接口创建epol
l对象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