V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
RememberCurry
V2EX  ›  Go 编程语言

用 Go 基于 epoll 实现一个最小化 IO 库

  •  1
     
  •   RememberCurry · 2023-06-04 10:22:40 +08:00 · 4467 次点击
    这是一个创建于 530 天前的主题,其中的信息可能已经有所发展或是发生改变。

    目前 Go 圈有很多款异步的网络框架:

    排名不分先后。

    这里面最早的实现是 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
    }
    

    源码地址:https://github.com/wuqinqiang/easyio

    38 条回复    2023-07-25 12:15:59 +08:00
    midknight
        1
    midknight  
       2023-06-04 10:26:45 +08:00
    有点意思,mark 一下
    julyclyde
        2
    julyclyde  
       2023-06-04 10:41:29 +08:00
    那为啥不去改进标准库呢?

    这些年,各种“yet another”已经太多了。绝大多数都没长久的生命力
    hankai17
        3
    hankai17  
       2023-06-04 10:43:32 +08:00
    go 用的少 有些疑问

    "一个 conn 一个 goroutine 导致利用率低"
    这个应该是优点吧 写起来方便 没看过 go 的协程 按理说性能不会太低吧

    "用户无法感知 conn 状态"
    同上 回调用起来不方便
    RememberCurry
        4
    RememberCurry  
    OP
       2023-06-04 10:47:41 +08:00
    @julyclyde
    我上面说了,
    > 都分析了好几篇的代码了,那么咋么说也得自己动手搞一个来达成学习目的。
    RememberCurry
        5
    RememberCurry  
    OP
       2023-06-04 10:48:59 +08:00
    @hankai17 海量连接情况下一个连接一个 g 并不划算
    julyclyde
        6
    julyclyde  
       2023-06-04 10:49:10 +08:00
    @RememberCurry 学习是目的,“单独搞一个”是手段,这完全两码事
    如果贡献到标准库,你的作品还能多活几年
    RememberCurry
        7
    RememberCurry  
    OP
       2023-06-04 10:50:29 +08:00
    @julyclyde 你说的都对。
    lindt99cocoa
        8
    lindt99cocoa  
       2023-06-04 11:00:20 +08:00   ❤️ 7
    @hankai17 中国特色 go 协程问题
    codehz
        9
    codehz  
       2023-06-04 11:23:53 +08:00
    3202 年是不是可以用上 io uring 了(
    RememberCurry
        10
    RememberCurry  
    OP
       2023-06-04 11:28:12 +08:00
    @codehz 俺不会,等俺学学😭
    z3phyr
        11
    z3phyr  
       2023-06-04 11:51:40 +08:00 via Android
    2023 年还在写回调,无异于开历史倒车
    matrix1010
        12
    matrix1010  
       2023-06-04 12:11:18 +08:00
    io 圈好卷。有没有人写 cache 框架跟我 pk 一下
    lesismal
        13
    lesismal  
       2023-06-04 12:22:44 +08:00   ❤️ 1
    支持 OP 一下!
    lesismal
        14
    lesismal  
       2023-06-04 12:23:18 +08:00   ❤️ 1
    > 2023 年还在写回调,无异于开历史倒车

    @z3phyr
    试试我这个,http 基本兼容标准库,http 和 websocket 来消息时都是可以写同步代码的。
    https://github.com/lesismal/nbio
    lesismal
        15
    lesismal  
       2023-06-04 12:31:29 +08:00
    > 3202 年是不是可以用上 io uring 了(

    @codehz 单就网络 io 这块,不同场景下 io uring 、epoll 性能好像是各有优劣,并不是所有情况都一边倒,所以综合网络库 epoll 有历史加持、足够了。特定场景的话倒是可以考虑定向优化
    fgwmlhdkkkw
        16
    fgwmlhdkkkw  
       2023-06-04 13:08:43 +08:00   ❤️ 1
    脱裤子放屁,多此一举。
    Nazz
        17
    Nazz  
       2023-06-04 13:10:41 +08:00 via Android
    @julyclyde 标准库你想改就能改吗
    Nazz
        18
    Nazz  
       2023-06-04 13:12:00 +08:00 via Android
    sunny1688
        19
    sunny1688  
       2023-06-04 13:27:20 +08:00
    这样说的话,那我也来发一下我实现的轮子吧~~
    netman: https://github.com/ikilobyte/netman

    支持 TCP 、UDP 、WebSocket 、同时 TCP 支持路由模式
    tyrantZhao
        20
    tyrantZhao  
       2023-06-04 14:14:56 +08:00 via iPhone
    上 iouring
    RememberCurry
        21
    RememberCurry  
    OP
       2023-06-04 14:21:23 +08:00   ❤️ 8
    我花了一个小时,仔细研读了社区条款以及一些法律文献,反复的把自己上述的文字重新编排组合,生怕遗漏一丝自己犯罪的细节,头上的汗水止不住地往下流,双手也不停地颤抖,可还是没有看到一丝犯罪,割菜,吹牛逼的行为,

    可,评论区的一些评论我实在看不懂~
    matrix1010
        22
    matrix1010  
       2023-06-04 14:27:56 +08:00 via iPhone
    @Nazz 卷不动了,光搞 cache 就很费脑细胞了
    Nazz
        23
    Nazz  
       2023-06-04 14:46:12 +08:00 via Android
    @z3phyr IO 异步,业务逻辑同步,没什么影响
    fds
        24
    fds  
       2023-06-04 16:00:20 +08:00
    @julyclyde #2 标准库不可能接受这种异步的实现方式。go 标准都是用同步写逻辑的。
    fds
        25
    fds  
       2023-06-04 16:13:12 +08:00
    op 不要灰心,大部分不需要连上万客户端的场景确实没必要这样优化,但我确实遇到过需要的情况。我当时是先试了 https://github.com/panjf2000/gnet 后来用的是 https://github.com/xtaci/gaio 但结果跑了一个月遇到了一次死锁,运维直接重启了,也没日志,公司当时还有别的事要忙,就没继续改进。
    fds
        26
    fds  
       2023-06-04 16:21:11 +08:00   ❤️ 1
    @lesismal #14 哇,大佬✨ nbio 在 windows 下能支持 iocp 吗?不过我看 issue 里都没人提耶,看来需求不大…… 我就是问问哈,不是要大佬做哈,给大佬跪一个先🧎‍♂️。
    lesismal
        27
    lesismal  
       2023-06-04 16:23:17 +08:00
    @fds
    > 标准库不可能接受这种异步的实现方式。go 标准都是用同步写逻辑的。

    标准库底层是非阻塞 io ,net.Conn 给用户提供阻塞接口 Read/Write ,用户需要主动 Read
    nbio 底层也是非阻塞 io ,nbio 的 http/websocket nonblocking 模式下给用户提供的是非阻塞接口 Write ,用户不需要主动 Read 。nbio 基本兼容标准库,用户基于 nbio 可以像写标准库 http 一样,少量不兼容比如涉及 io.Copy ,其他的普通功能,只要把 io 替换成 nbio 就可以了、业务代码都不需要改,gin/echo 之类的也都能轻松用 nbio 替换 std http server 。
    代码看下就能用了:
    https://github.com/lesismal/nbio-examples/blob/master/http/server/server.go
    https://github.com/lesismal/nbio-examples/blob/master/http_with_other_frameworks/gin_server/gin_server.go

    > op 不要灰心,大部分不需要连上万客户端的场景确实没必要这样优化,但我确实遇到过需要的情况。我当时是先试了 > https://github.com/panjf2000/gnet 后来用的是 https://github.com/xtaci/gaio 但结果跑了一个月遇到了一次死锁,> 运维直接重启了,也没日志,公司当时还有别的事要忙,就没继续改进。

    来吧,用 nbio ,还有救:
    https://github.com/lesismal/nbio

    这里有百万连接 websocket 的:
    https://github.com/lesismal/go-websocket-benchmark
    lesismal
        28
    lesismal  
       2023-06-04 16:24:21 +08:00   ❤️ 1
    @fds #26 windows 只为方便开发。。打死我也不会去支持 iocp 了,哈哈哈,太难搞了
    julyclyde
        29
    julyclyde  
       2023-06-04 20:06:32 +08:00
    @Nazz 能不能改是看你写的好不好的,而不是看你是不是名人的
    Nazz
        30
    Nazz  
       2023-06-04 20:32:30 +08:00 via Android   ❤️ 1
    @RememberCurry 自己觉得有意义就好
    mindddd
        31
    mindddd  
       2023-06-04 21:02:12 +08:00
    @z3phyr 此话怎讲,请细说
    zhaohua
        32
    zhaohua  
       2023-06-04 22:09:50 +08:00   ❤️ 1
    我一直不太理解,为啥 go 要上 epoll ,原生的 goroutine 在绝大多数场景下都没有性能问题,性能要求高的话可以上 rpc connect 复用。 感觉是 java 转过去的人卷的。
    lesismal
        33
    lesismal  
       2023-06-04 22:19:15 +08:00
    > 可,评论区的一些评论我实在看不懂~

    你可能还不知道吧,以前只是站着说话不腰疼,现在是躺着说话他也不腰疼呐
    kkocdko
        34
    kkocdko  
       2023-06-05 01:09:09 +08:00 via Android
    我想说楼主自己写着玩没问题,我也喜欢整天搓这类底层小玩具。

    但是楼主为了推广自己的玩具,不惜妄顾事实,说出“一个 conn 一个 goroutine 导致利用率低”这种笑话,那就没必要说什么“评论实在看不懂”了。

    退一万步讲,在超大流量的负载均衡需求下才会有这种对极限性能的追求,这固然很酷,但是大部分场景都不会根据 plain text 跑分来做选型,这并不是性能瓶颈。

    https://www.techempower.com/benchmarks/#section=test&runid=f35979a9-4e5e-41db-9ba2-9790167667e9&test=plaintext
    lysS
        35
    lysS  
       2023-06-05 02:53:49 +08:00
    目的就是一个连接对应一个协程,从而避免写异步代码。话说哪些异步回调有什么用?其实很难在中间插入什么逻辑的
    Nazz
        36
    Nazz  
       2023-06-05 07:29:39 +08:00 via Android   ❤️ 2
    @kkocdko
    1. 海量连接不等于超大流量
    2. 海量连接场景下使用标准网络库会耗费大量内存,goroutine 调度性能下降. 很明显,reactor 模式能节省 read_buffer_size * num_connections 的内存,以及海量 goroutine 的栈内存。
    3. op 没放任何压测数据.
    wkong
        37
    wkong  
       2023-07-25 10:59:27 +08:00
    我也基于上面大佬们的思想开发了我的单机百万通用实时通讯服务,感兴趣可以 star 下

    https://github.com/WuKongIM/WuKongIM
    RememberCurry
        38
    RememberCurry  
    OP
       2023-07-25 12:15:59 +08:00
    @wkong 发现已经 star 过这个项目了,哈哈
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2831 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 02:19 · PVG 10:19 · LAX 18:19 · JFK 21:19
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.