用 Go 基于 epoll 实现一个最小化 IO 库
目前 Go 圈有很多款异步的网络框架:
- https://github.com/tidwall/evio
- https://github.com/lesismal/nbio
- https://github.com/panjf2000/gnet
- https://github.com/cloudwego/netpoll
- .......
排名不分先后。
这里面最早的实现是 evio 。evio 也存在一些问题,之前也写过evio文章介绍过。 其他比如 nbio 和 gnet 也写过一些源码分析。
为什么会出现这些框架?之前也提到过,由于标准库 netpoll 的一些特性:
- 一个 conn 一个 goroutine 导致利用率低
- 用户无法感知 conn 状态
- .....
这些框架在应用层上做了很多优化,比如:Worker Pool,Buffer,Ring Buffer,NoCopy......。
都分析了好几篇的代码了,那么咋么说也得自己动手搞一个来达成学习目的。
没错,这就是easyio的由来。
它是一个最小化的 IO 框架,只实现最核心的部分,加起来不超过 500 行代码。
也没有用户端上层应用的优化,且目前只实现了 linux 的 epoll ,以及只能运行 tcp 协议。
简单的 demo ,
服务端:
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/wuqinqiang/easyio"
)
var _ easyio.EventHandler = (*Handler)(nil)
type Handler struct{}
type EasyioKey struct{}
type Message struct{ Msg string }
var CtxKey EasyioKey
func (h Handler) OnOpen(c easyio.Conn) context.Context {
return context.WithValue(context.Background(), CtxKey, Message{Msg: "helloword"})
}
func (h Handler) OnRead(ctx context.Context, c easyio.Conn) {
_, ok := ctx.Value(CtxKey).(Message)
if !ok {
return
}
var b = make([]byte, 100)
_, err := c.Read(b)
if err != nil {
fmt.Println("err:", err)
}
fmt.Println("[Handler] read data:", string(b))
if _, err = c.Write(b); err != nil {
panic(err)
}
}
func (h Handler) OnClose(_ context.Context, c easyio.Conn) {
fmt.Println("[Handler] closed", c.Fd())
}
func main() {
e := easyio.New("tcp", ":8090",easyio.WithNumPoller(4), easyio.WithEventHandler(Handler{}))
if err := e.Start(); err != nil {
panic(err)
}
defer e.Stop()
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT)
<-c
}
上面的代码,初始化一个 easyio ,启动一个 tcp 服务,监听端口 8090 ,options 里面设置 epoll 的数量,以及设置事件处理器。
当一个新连接到来时会回调 OnOpen 函数,此时你可以设置自定义的 ctx ,那么当对应连接读事件到来 OnRead 回调,你可以拿到之前设置的 ctx ,调用 conn.Read 读取数据,且通过 Write 向对端写数据。
这里需要注意的是,一个连接如果数据没读完,当 OnRead 执行结束,下一轮会继续触发回调代码,因为底层 epoll 采用的是 LT 触发方式。
简单的客户端
package main
import (
"fmt"
"net"
"os"
"os/signal"
"syscall"
)
func main() {
conn, err := net.Dial("tcp", ":8090")
if err != nil {
panic(err)
}
n, err := conn.Write([]byte("hello world"))
if err != nil {
panic(err)
}
go func() {
b := make([]byte, 100)
if n, err = conn.Read(b); err != nil {
panic(err)
}
fmt.Println("read data:", n, string(b))
}()
defer conn.Close()
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
<-ch
}