前言:
此刻我们对“打地鼠源代码”大概比较着重,各位老铁们都需要学习一些“打地鼠源代码”的相关内容。那么小编也在网摘上汇集了一些关于“打地鼠源代码””的相关文章,希望你们能喜欢,大家快快来了解一下吧!evio 是一个基于事件驱动的网络框架,它非常轻量而且相比 Go net 标准库更快。其底层使用epoll 和 kqueue 系统调度实现。
转自:
参考:go语言中文文档;
原理
evio 是 Reactor 模式的简单实现。Reactor 本质就是“non-blocking IO + IO multiplexing”,通过非阻塞IO+ IO 多路复用来处理并发。程序运行一个或者多个事件循环,通过在事件循环中注册回调的方式实现业务逻辑。
evio 将所有文件描述符设为非阻塞,并注册到事件循环( epoll / kqueue )中。相较于传统的 per thread per connection 的处理方法,线程使用更少,线程资源利用率更高。
evio 需要在服务启动前,注册回调函数,当事件循环中有事件到来时,会调用回调函数处理。
使用示例
先从一个简单的 echo server 的例子来了解 evio 。
package mainimport ( "flag" "fmt" "log" "strings" "github.com/tidwall/evio")func main() { var port int var loops int var udp bool var trace bool var reuseport bool var stdlib bool flag.IntVar(&port, "port", 5000, "server port") flag.BoolVar(&udp, "udp", false, "listen on udp") flag.BoolVar(&reuseport, "reuseport", false, "reuseport (SO_REUSEPORT)") flag.BoolVar(&trace, "trace", false, "print packets to console") flag.IntVar(&loops, "loops", 0, "num loops") flag.BoolVar(&stdlib, "stdlib", false, "use stdlib") flag.Parse() var events evio.Events events.NumLoops = loops events.Serving = func(srv evio.Server) (action evio.Action) { log.Printf("echo server started on port %d (loops: %d)", port, srv.NumLoops) if reuseport { log.Printf("reuseport") } if stdlib { log.Printf("stdlib") } return } events.Data = func(c evio.Conn, in []byte) (out []byte, action evio.Action) { if trace { log.Printf("%s", strings.TrimSpace(string(in))) } out = in return } scheme := "tcp" if udp { scheme = "udp" } if stdlib { scheme += "-net" } log.Fatal(evio.Serve(events, fmt.Sprintf("%s://:%d?reuseport=%t", scheme, port, reuseport)))}
上面的例子主要就是注册了两个回调函数:events.Serving 和 events.Data 。
当 server 启动时,会来执行注册的 events.Serving 回调函数;
当有数据到来时,执行 events.Data 回调函数。
程序最后调用 evio.Serve 方法开启事件循环,程序在此处不断循环检测是否有事件发生并处理(有数据到来,有数据要发送…)。
evio 都是通过回调函数来执行业务逻辑的。当客户端有数据发送过来时,调用用户注册的 events.Data 函数。
需要发送数据给客户端时,只可以通过注册的回调函数的返回值来返回,evio 框架来负责发送(有bug)。
回调函数的返回值主要有两个 out []byte, action evio.Action , out 就是需要发送给客户端的, Action 就是返回一些状态,用来关闭连接,或者服务器退出啥的操作。主要状态如下:
const ( // None indicates that no action should occur following an event. None Action = iota // Detach detaches a connection. Not available for UDP connections. Detach // Close closes the connection. Close // Shutdown shutdowns the server. Shutdown)evio 的事件循环evio.Serve
我们先来看下 evio.Serve 方法的实现
func Serve(events Events, addr ...string) error { var lns []*listener defer func() { // 这个函数如果推出,需要关闭所有 listener for _, ln := range lns { ln.close() } }() var stdlib bool // 可以选择使用 stdlib(stdlib 主要是为了支持 非 *unix 平台) for _, addr := range addr { // 生成 listener var ln listener var stdlibt bool ln.network, ln.addr, ln.opts, stdlibt = parseAddr(addr) if stdlibt { stdlib = true } if ln.network == "unix" { os.RemoveAll(ln.addr) } var err error if ln.network == "udp" { if ln.opts.reusePort { ln.pconn, err = reuseportListenPacket(ln.network, ln.addr) } else { ln.pconn, err = net.ListenPacket(ln.network, ln.addr) } } else { if ln.opts.reusePort { ln.ln, err = reuseportListen(ln.network, ln.addr) } else { ln.ln, err = net.Listen(ln.network, ln.addr) } } if err != nil { return err } if ln.pconn != nil { ln.lnaddr = ln.pconn.LocalAddr() } else { ln.lnaddr = ln.ln.Addr() } if !stdlib { if err := ln.system(); err != nil { return err } } lns = append(lns, &ln) } if stdlib { return stdserve(events, lns) // 使用 std net 库 启动server } return serve(events, lns) // 使用 epoll or kqueue 启动server}
从 Serve 函数签名中可以看出 evio 是支持绑定多地址监听的
func Serve(events Events, addr ...string) error
使用方式如下:
evio.Serve(events, "tcp://localhost:5000", "tcp://192.168.0.10:5001");
现在我们看看 evio 的核心部分: serve(events, lns)
,这里会启动 evio 的 sever 。
func serve(events Events, listeners []*listener) error { numLoops := events.NumLoops // 确定启动的事件循环数量 if numLoops <= 0 { if numLoops == 0 { numLoops = 1 } else { numLoops = runtime.NumCPU() } } s := &server{} s.events = events s.lns = listeners s.cond = sync.NewCond(&sync.Mutex{}) s.balance = events.LoadBalance s.tch = make(chan time.Duration) //println("-- server starting") if s.events.Serving != nil { // 如果注册了回调函数,就执行 var svr Server svr.NumLoops = numLoops svr.Addrs = make([]net.Addr, len(listeners)) for i, ln := range listeners { svr.Addrs[i] = ln.lnaddr } action := s.events.Serving(svr) switch action { case None: case Shutdown: return nil } } defer func() { // server 退出后的清理工作 // wait on a signal for shutdown s.waitForShutdown() // notify all loops to close by closing all listeners for _, l := range s.loops { l.poll.Trigger(errClosing) } // wait on all loops to complete reading events s.wg.Wait() // close loops and all outstanding connections for _, l := range s.loops { for _, c := range l.fdconns { loopCloseConn(s, l, c, nil) } l.poll.Close() } //println("-- server stopped") }() // create loops locally and bind the listeners. for i := 0; i < numLoops; i++ { l := &loop{ idx: i, poll: internal.OpenPoll(), packet: make([]byte, 0xFFFF), // event loop 的 read 缓冲区 fdconns: make(map[int]*conn), } for _, ln := range listeners { l.poll.AddRead(ln.fd) // 将 fd 注册到 epoll 中并监听可读事件 } s.loops = append(s.loops, l) } // start loops in background s.wg.Add(len(s.loops)) for _, l := range s.loops { // 启动所有的 event loop go loopRun(s, l) } return nil}
serve 主要做这些事:
根据配置启动指定数量的 event loop,如果传入配置的 loop 数量为 0 则设置启动一个事件循环,如果传入配置小于 0 则设置为运行平台的CPU核心数量如果设置了回调函数 events.Serving ,运行它按照指定 event loop 数量,创建 epoll 句柄生成 loop ,并将所有的 listener 注册到 epoll 监听可读事件(有客户端连接)启动所有事件循环(一个事件循环一个 goroutine)
需要注意的是,evio 将所有的 listener 的 fd 在每一个事件循环的 epoll 中都注册了。也就是说,如果有三个事件循环,一个 listener ,那么这个 listener 的 fd 会注册到三个 epoll 中。这就会出现 epoll 的惊群现象,感兴趣的可以自己搜索了解下。
evio 当一个新连接到来时,所有的事件循环都会唤醒,但是最终只有一个线程可以accept调用返回成功,其他线程(协程)的accept函数调用返回EAGAIN错误 。
loopRun
下面我们看看 loopRun 的内部实现
func loopRun(s *server, l *loop) { defer func() { //fmt.Println("-- loop stopped --", l.idx) s.signalShutdown() s.wg.Done() }() if l.idx == 0 && s.events.Tick != nil { go loopTicker(s, l) } //fmt.Println("-- loop started --", l.idx) l.poll.Wait(func(fd int, note interface{}) error { if fd == 0 { return loopNote(s, l, note) } c := l.fdconns[fd] switch { case c == nil: return loopAccept(s, l, fd) case !c.opened: return loopOpened(s, l, c) case len(c.out) > 0: return loopWrite(s, l, c) case c.action != None: return loopAction(s, l, c) default: return loopRead(s, l, c) } })}
l.poll.Wait 传入一个回调函数作为参数,当 epoll 收到事件通知时,会执行这个回调函数。
在这个函数中接受客户端连接,读取客户端数据,调用客户回调函数处理业务逻辑…
我们先来看下 poll.Wait 的内部实现,再看看 loopAccept,loopOpened,loopWrite 等函数。
loopRun 方法中最重要的就是 poll.Wait ,我们看看 Linux 下 epoll 的实现
func (p *Poll) Wait(iter func(fd int, note interface{}) error) error { events := make([]syscall.EpollEvent, 64) for { n, err := syscall.EpollWait(p.fd, events, -1) if err != nil && err != syscall.EINTR { return err } if err := p.notes.ForEach(func(note interface{}) error { return iter(0, note) }); err != nil { return err } for i := 0; i < n; i++ { if fd := int(events[i].Fd); fd != p.wfd { if err := iter(fd, nil); err != nil { return err } } else { } } }}
这个函数中是一个死循环,程序会阻塞在此处等待 epoll 的”通知“,然后处理就绪的 fd (读取/发送数据、执行用户注册的回调函数)。
当有 fd 就绪的时候,syscall.EpollWait 函数返回,并且将就绪的 fd 通过 events 传出,返回值 n 为就绪 fd 的个数。
然后循环逐个遍历就绪的 fd,调用回调函数处理。
for i := 0; i < n; i++ { if fd := int(events[i].Fd); fd != p.wfd { if err := iter(fd, nil); err != nil { return err } } else { }}evio 的事件处理
l.poll.Wait(func(fd int, note interface{}) error { if fd == 0 { return loopNote(s, l, note) } c := l.fdconns[fd] switch { case c == nil: return loopAccept(s, l, fd) case !c.opened: return loopOpened(s, l, c) case len(c.out) > 0: return loopWrite(s, l, c) case c.action != None: return loopAction(s, l, c) default: return loopRead(s, l, c) }})
当 epoll 检测到有就绪的 fd 时,会逐个调用上面的回调函数,evio 的主要逻辑也在这里。
当 fd == 0 时,会执行 loopNote 函数。loopNote 主要是用来处理一些非 fd 就绪的事件,比如定时任务、强制退出等。当然,我们都知道 fd 为 0 是标准输入,所以此处并不是真的去处理 fd 为 0 的文件描述符(注册到 epoll 的文件描述 >= 3)。作者知道 epoll 返回的就绪 fd 中不会有为 0 的情况,所以此处 fd 为 0,是作者调用时传入,用来表示一种特殊的唤醒场景。
func (p *Poll) Wait(iter func(fd int, note interface{}) error) error {... p.changes = p.changes[:0] if err := p.notes.ForEach(func(note interface{}) error { return iter(0, note)...
我们跳到调用它的地方,可以看到只有在 p.notes.ForEach 这个函数中注册的回调函数中才会传入 fd 为 0 来执行 iter 回调函数。
notes noteQueue
noteQueue 的实现在 internal 目录中的 notequeue.go , 是一个无锁队列。我们不详细分析,只看下 ForEach 这个方法:
func (q *noteQueue) ForEach(iter func(note interface{}) error) error { q.mu.Lock() if len(q.notes) == 0 { q.mu.Unlock() return nil } notes := q.notes q.notes = nil q.mu.Unlock() for _, note := range notes { if err := iter(note); err != nil { // 执行回调函数 return err } } return nil}
当队列中有数据时, 会执行回调函数,即
func(note interface{}) error { return iter(0, note)}
从上面的分析中可以我们已经知道为什么会有 fd 为 0 ,下面我们看下 loopNote 做什么。
loopNote
func loopNote(s *server, l *loop, note interface{}) error { var err error switch v := note.(type) { case time.Duration: delay, action := s.events.Tick() switch action { case None: case Shutdown: err = errClosing } s.tch <- delay case error: // shutdown err = v case *conn: // Wake called for connection if l.fdconns[v.fd] != v { return nil // ignore stale wakes } return loopWake(s, l, v) } return err}
传入的 note 是 interface{} ,首先对 note 进行类型判断。
当 note 是 time.Duration 时,调用回调函数 events.Tick() ,这是 evio 提供的定时任务接口。
在 loopRun 函数中,如果设置了定时回调函数,会启动一个协程来来运行 loopTicker
if l.idx == 0 && s.events.Tick != nil { go loopTicker(s, l)}
loopTicker 实现如下,可以看出会定时去触发 l.poll.Trigger,并且传入 time.Duration(0)
func loopTicker(s *server, l *loop) { for { if err := l.poll.Trigger(time.Duration(0)); err != nil { break } time.Sleep(<-s.tch) }}
我们跳到 poll.Trigger 的 linux 下的实现,可以发现 evio 在此处 p.notes.Add(note) ,也就是 time.Duration(0)
func (p *Poll) Trigger(note interface{}) error { p.notes.Add(note) _, err := syscall.Write(p.wfd, []byte{0, 0, 0, 0, 0, 0, 0, 1}) return err}
poll.Trigger 这个函数不仅仅是在 p.notes 里增加了一个 note,还唤醒了事件循环。
当 epoll 中注册 fd 都没有就绪事件时,线程会挂起,epoll 的 wait 方法会处于阻塞状态。evio 使用
linux 提供的 eventfd 来实现事件循环的唤醒,也就是代码上中的 syscall.Write(p.wfd, []byte{0, 0, 0, 0, 0, 0, 0, 1}) ,往 p.wfd 这个文件描述符中写入了 8 个字节的数据。
p.wfd 是一个 eventfd , 是 Poll 结构体的成员,在 OpenPoll 时赋值,即打开一个 eventfd 代码如下:
type Poll struct { fd int // epoll fd wfd int // wake fd notes noteQueue}func OpenPoll() *Poll { l := new(Poll) p, err := syscall.EpollCreate1(0) if err != nil { panic(err) } l.fd = p r0, _, e0 := syscall.Syscall(syscall.SYS_EVENTFD2, 0, 0, 0) if e0 != 0 { syscall.Close(p) panic(err) } l.wfd = int(r0) l.AddRead(l.wfd) return l}
syscall.Syscall(syscall.SYS_EVENTFD2, 0, 0, 0) 创建了一个 eventfd ,然后将这个 eventfd 注册到了 epoll 监听可读事件。当 syscall.Write(p.wfd, []byte{0, 0, 0, 0, 0, 0, 0, 1}) 时候,epoll 就会唤醒。
但是,我翻了好久,也没有找到 evio 在哪里读取 eventfd 写入的8个字节(epoll)。这是一个 bug,所以在 linux 机器上,这是不能用的。
这个bug会造成 epoll 不断唤醒,cpu被长期占用
当我们注册了 evio 的定时任务 Tick 回调函数,程序启动后会往 eventfd 里写入 8 个字节数据,但是 evio 并没有读取,并且 evio 使用的是 epoll 的默认模式 LT,即只要可读缓冲区里还有数据,epoll 会一直不断唤醒,这是一个严重的 bug,作者应该没有在 linux 环境下严格测试过。
我们抛开这个 bug, 继续来看 note 为 error 类型的情况。在 serve 函数中,当函数退出时,通过l.poll.Trigger(errClosing) 来通知每个事件循环退出。
func serve(events Events, listeners []*listener) error {...defer func() { // wait on a signal for shutdown s.waitForShutdown() // notify all loops to close by closing all listeners for _, l := range s.loops { l.poll.Trigger(errClosing) } // wait on all loops to complete reading events s.wg.Wait() // close loops and all outstanding connections for _, l := range s.loops { for _, c := range l.fdconns { loopCloseConn(s, l, c, nil) } l.poll.Close() } //println("-- server stopped")}()...
当 note 为 *conn 这种情况,是用来提供给使用者主动唤醒当前事件循环
func (c *conn) Wake() { if c.loop != nil { c.loop.poll.Trigger(c) }}loopAccept
c := l.fdconns[fd] switch { case c == nil: return loopAccept(s, l, fd)
type loop struct { idx int // loop index in the server loops list poll *internal.Poll // epoll or kqueue packet []byte // read packet buffer fdconns map[int]*conn // loop connections fd -> conn count int32 // connection count}
fdconns 是用来存储已连接的TCP connection 信息,key 为 fd, value 为 *conn 。
当 epoll 唤醒时,如果 fd 不在当前事件循环的连接,那就说明它是新连接,则执行 loopAccept 。
func loopAccept(s *server, l *loop, fd int) error { for i, ln := range s.lns { if ln.fd == fd { if len(s.loops) > 1 { switch s.balance { case LeastConnections: n := atomic.LoadInt32(&l.count) for _, lp := range s.loops { if lp.idx != l.idx { if atomic.LoadInt32(&lp.count) < n { return nil // do not accept } } } case RoundRobin: idx := int(atomic.LoadUintptr(&s.accepted)) % len(s.loops) if idx != l.idx { return nil // do not accept } atomic.AddUintptr(&s.accepted, 1) } } if ln.pconn != nil { return loopUDPRead(s, l, i, fd) } nfd, sa, err := syscall.Accept(fd) if err != nil { if err == syscall.EAGAIN { return nil } return err } if err := syscall.SetNonblock(nfd, true); err != nil { return err } c := &conn{fd: nfd, sa: sa, lnidx: i, loop: l} l.fdconns[c.fd] = c l.poll.AddReadWrite(c.fd) atomic.AddInt32(&l.count, 1) break } } return nil}
因为 evio 支持多地址监听,所以会存在多个 listener ,也就是 s.lns 。
第一步,先遍历所有的 listener 看看当前 epoll 中就绪的 fd 是哪一个 listener ,然后执行客户端的负载策略,决定新的客户端连接放在哪一个事件循环中。
这里关于客户端的负载策略,evio 利用了 epoll 的惊群效果,所有的事件循环都会唤醒进入loopAccept,不符合负载策略直接 return nil。关于这边的更多细节,可以看我的另一篇文章 【Golang 网络库 evio 一些问题/bug和思考】()。
接下来就是常规操作了,syscall.Accept(fd) 接受连接,然后 syscall.SetNonblock(nfd, true) 设置成非阻塞模式,l.poll.AddReadWrite(c.fd) 最后加入事件循环,注册可读可写事件。
loopOpened
func loopOpened(s *server, l *loop, c *conn) error { c.opened = true c.addrIndex = c.lnidx c.localAddr = s.lns[c.lnidx].lnaddr c.remoteAddr = internal.SockaddrToAddr(c.sa) if s.events.Opened != nil { out, opts, action := s.events.Opened(c) if len(out) > 0 { c.out = append([]byte{}, out...) } c.action = action c.reuse = opts.ReuseInputBuffer if opts.TCPKeepAlive > 0 { if _, ok := s.lns[c.lnidx].ln.(*net.TCPListener); ok { internal.SetKeepAlive(c.fd, int(opts.TCPKeepAlive/time.Second)) } } } if len(c.out) == 0 && c.action == None { l.poll.ModRead(c.fd) } return nil}
loopOpened 是在 loopAccept 执行完成后,epoll 会立马再次唤醒然后执行的。
因为在 loopAccept 中最后将新的客户端连接加入 epoll 管理时注册的是可读可写事件,当前的内核写缓冲区肯定是为空的,所以 epoll 会再次唤醒。
...case !c.opened: return loopOpened(s, l, c)...
唤醒后会执行到这个 case case !c.opened:,因为在 loopAccept 中并没有去设置这个值。
loopOpened 内部的操作,主要就是设置一下 conn 的属性,然后调用客户注册的回调函数 events.Opened 。
如果在回调函数中,没有给客户端发送数据,则需要重新注册,只注册可读事件,不然 epoll 会一直唤醒(可写事件)。
loopAction
func loopAction(s *server, l *loop, c *conn) error { switch c.action { default: c.action = None case Close: return loopCloseConn(s, l, c, nil) case Shutdown: return errClosing case Detach: return loopDetachConn(s, l, c, nil) } if len(c.out) == 0 && c.action == None { l.poll.ModRead(c.fd) } return nil}
case c.action != None: return loopAction(s, l, c)
loopAction 会在 case c.action != None: 的情况下执行, c.action 是执行完用户回调函数后会被赋值的状态。
在会有 action 的 loopXXX 中都会有如下类似操作。
if len(c.out) != 0 || c.action != None { l.poll.ModReadWrite(c.fd)}
也就是说 loopAction 依赖于 epoll 被可写事件再次唤醒来执行,这样会不会有问题呢?内核缓冲区满了??
loopAction 内部的主要操作就是根据 action 做一些处理,关闭连接等等。
loopRead 和 loopWrite
loopRead 和 loopWrite 主要就是调用系统调用读取和发送数据,并且调用用户回调函数,根据回调函数返回值来重新注册 epoll 的可读可写事件。
func loopRead(s *server, l *loop, c *conn) error { var in []byte n, err := syscall.Read(c.fd, l.packet) if n == 0 || err != nil { if err == syscall.EAGAIN { return nil } return loopCloseConn(s, l, c, err) } in = l.packet[:n] if !c.reuse { in = append([]byte{}, in...) } if s.events.Data != nil { out, action := s.events.Data(c, in) c.action = action if len(out) > 0 { c.out = append([]byte{}, out...) } } if len(c.out) != 0 || c.action != None { l.poll.ModReadWrite(c.fd) } return nil}
调用 n, err := syscall.Read(c.fd, l.packet) 读取内核缓冲区的数据,如果返回出错 err == syscall.EAGAIN 意思是再试一次,直接返回。
如果 n == 0 或者 err 错误不为 syscall.EAGAIN ,则说明对方关闭了连接或是其他错误,直接 loopCloseConn 。
然后调用用户回调函数 s.events.Data ,根据返回值做相应操作。c.action = action
如果 out 里有数据,则赋给 c.out , 并且注册可读可写事件。
如果 c.action != None ,同样需要注册可读可写事件,原因上面已经说过了。
loopWrite 操作也大同小异,就不细说了。
但是其实关于 loopWrite 和 loopRead 的处理是会有 bug 的,详细可以看另一篇文章 【Golang 网络库 evio 一些问题/bug和思考】()。
标签: #打地鼠源代码