【开源】gnet: 一个轻量级且高性能的 Golang 网络库

panjf200 · · 2199 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

![](https://ask.qcloudimg.com/http-save/1303222/sipe2g9n9h.png) # Github 主页 [https://github.com/panjf2000/gnet](https://github.com/panjf2000/gnet) 欢迎大家围观~~,目前还在持续更新,感兴趣的话可以 star 一下暗中观察哦。 # 原文博客 [gnet: 一个轻量级且高性能的 Golang 网络库 ](https://taohuawu.club/go-event-loop-networking-library-gnet) # 简介 `gnet` 是一个基于 Event-Loop 事件驱动的高性能和轻量级网络库。这个库直接使用 [epoll](https://en.wikipedia.org/wiki/Epoll) 和 [kqueue](https://en.wikipedia.org/wiki/Kqueue) 系统调用而非标准 Golang 网络包:[net](https://golang.org/pkg/net/) 来构建网络应用,它的工作原理类似于两个开源的网络库:[libuv](https://github.com/libuv/libuv) 和 [libevent](https://github.com/libevent/libevent)。 这个项目存在的价值是提供一个在网络包处理方面能和 [Redis](http://redis.io)、[Haproxy](http://www.haproxy.org) 这两个项目具有相近性能的Go 语言网络服务器框架。 `gnet` 的亮点在于它是一个高性能、轻量级、非阻塞的纯 Go 实现的传输层(TCP/UDP/Unix-Socket)网络库,开发者可以使用 `gnet` 来实现自己的应用层网络协议,从而构建出自己的应用层网络应用:比如在 `gnet` 上实现 HTTP 协议就可以创建出一个 HTTP 服务器 或者 Web 开发框架,实现 Redis 协议就可以创建出自己的 Redis 服务器等等。 **`gnet`** **衍生自另一个项目:\*\***`evio`\***\*,但是性能更好。** # 功能 - 高性能 的基于多线程模型的 Event-Loop 事件驱动 - 内置 Round-Robin 轮询负载均衡算法 - 简洁的 APIs - 基于 Ring-Buffer 的高效内存利用 - 支持多种网络协议:TCP、UDP、Unix Sockets - 支持两种事件驱动机制:Linux 里的 epoll 以及 FreeBSD 里的 kqueue - 支持异步写操作 - 允许多个网络监听地址绑定在一个 Event-Loop 上 - 灵活的事件定时器 - SO\_REUSEPORT 端口重用 # 核心设计 ## 多线程/Go程模型 ### 主从多 Reactors 模型 `gnet` 重新设计开发了一个新内置的多线程/Go程模型:『主从多 Reactors』,这也是 `netty` 默认的线程模型,下面是这个模型的原理图: <p align="center"> ![](https://ask.qcloudimg.com/http-save/1303222/f1kmbmqgq3.png) 它的运行流程如下面的时序图: <p align="center"> ![](https://ask.qcloudimg.com/http-save/1303222/md4p172zmi.png) ### 主从多 Reactors + 线程/Go程池 你可能会问一个问题:如果我的业务逻辑是阻塞的,那么在 `Event.React()` 注册方法里的逻辑也会阻塞,从而导致阻塞 event-loop 线程,这时候怎么办? 正如你所知,基于 `gnet` 编写你的网络服务器有一条最重要的原则:永远不能让你业务逻辑(一般写在 `Event.React()` 里)阻塞 event-loop 线程,否则的话将会极大地降低服务器的吞吐量,这也是 `netty` 的一条最重要的原则。 我的回答是,现在我正在为 `gnet` 开发一个新的多线程/Go程模型:『带线程/Go程池的主从多 Reactors』,这个新网络模型将通过引入一个 worker pool 来解决业务逻辑阻塞的问题:它会在启动的时候初始化一个 worker pool,然后在把 `Event.React()`里面的阻塞代码放到 worker pool 里执行,从而避免阻塞 event-loop 线程, 这个模型还在持续开发中并且很快就能完成,模型的架构图如下所示: ![64918783-90de3b80-d7d5-11e9-9190-ff8277c95db1.png](https://ask.qcloudimg.com/draft/1303222/w9448bjpjz.png) 它的运行流程如下面的时序图: ![](https://ask.qcloudimg.com/http-save/1303222/nk3tnisthu.png) 不过,在这个新的网络模型开发完成之前,你依然可以通过一些其他的外部开源 goroutine pool 来处理你的阻塞业务逻辑,在这里我推荐个人开发的一个开源 goroutine pool:[ants](https://github.com/panjf2000/ants),它是一个基于 Go 开发的高性能的 goroutine pool ,实现了对大规模 goroutine 的调度管理、goroutine 复用。 你可以在开发 `gnet` 网络应用的时候集成 `ants` 库,然后把那些阻塞业务逻辑提交到 `ants` 池里去执行,从而避免阻塞 event-loop 线程。 ## 通信机制 `gnet` 的『主从 Reactors 多线程』模型是基于 Golang 里的 Goroutines的,一个 Reactor 挂载在一个 Goroutine 上,所以在 `gnet` 的这个网络模型里主 Reactor/Goroutine 与从 Reactors/Goroutines 有海量通信的需求,因此 `gnet` 里必须要有一个能在 Goroutines 之间进行高效率的通信的机制,我没有选择 Golang 里的主流方案:基于 Channel 的 CSP 模型,而是选择了性能更好、基于 Ring-Buffer 的 Disruptor 方案。 所以我最终选择了 [go-disruptor](https://github.com/smartystreets-prototypes/go-disruptor):高性能消息分发队列 LMAX Disruptor 的 Golang 实现。 ## 自动扩容的 Ring-Buffer `gnet` 利用 Ring-Buffer 来缓存 TCP 流数据以及管理内存使用。 ![](https://ask.qcloudimg.com/http-save/1303222/405gs3dnfi.gif) # 开始使用 ## 安装 ``` $ go get -u github.com/panjf2000/gnet ``` ## 使用示例 用 `gnet` 来构建网络服务器是非常简单的,只需要把你关心的事件注册到 `gnet.Events` 里面,然后把它和绑定的监听地址一起传递给 `gnet.Serve` 方法就完成了。在服务器开始工作之后,每一条到来的网络连接会在各个事件之间传递,如果你想在某个事件中关闭某条连接或者关掉整个服务器的话,直接把 `gnet.Action` 设置成 `Cosed` 或者 `Shutdown`就行了。 Echo 服务器是一种最简单网络服务器,把它作为 `gnet` 的入门例子在再合适不过了,下面是一个最简单的 echo server,它监听了 9000 端口: ### 不带阻塞逻辑的 echo 服务器 ``` package main import ( "log" "strings" "github.com/panjf2000/gnet" ) func main() { var trace bool var events gnet.Events events.React = func(c gnet.Conn) (out []byte, action gnet.Action) { top, tail := c.ReadPair() out = append(top, tail...) c.ResetBuffer() if trace { log.Printf("%s", strings.TrimSpace(string(top)+string(tail))) } return } log.Fatal(gnet.Serve(events, "tcp://:9000", gnet.WithMulticore(true))) } ``` 正如你所见,上面的例子里 `gnet` 实例只注册了一个 `React` 事件。一般来说,主要的业务逻辑代码会写在这个事件方法里,这个方法会在服务器接收到客户端写过来的数据之时被调用,然后处理输入数据(这里只是把数据 echo 回去)并且在处理完之后把需要输出的数据赋值给 `out` 变量然后返回,之后你就不用管了,`gnet` 会帮你把数据写回客户端的。 ### 带阻塞逻辑的 echo 服务器 ``` package main import ( "log" "time" "github.com/panjf2000/ants" "github.com/panjf2000/gnet" ) func main() { var events gnet.Events // Create a goroutine pool. poolSize := 256 * 1024 pool, _ := ants.NewPool(poolSize, ants.WithNonblocking(true)) defer pool.Release() events.React = func(c gnet.Conn) (out []byte, action gnet.Action) { data := c.ReadBytes() c.ResetBuffer() action = DataRead // Use ants pool to unblock the event-loop. _ = pool.Submit(func() { time.Sleep(1 * time.Second) c.AsyncWrite(data) }) return } log.Fatal(gnet.Serve(events, "tcp://:9000", gnet.WithMulticore(true))) } ``` 正如我在『主从多 Reactors + 线程/Go程池』那一节所说的那样,如果你的业务逻辑里包含阻塞代码,那么你应该把这些阻塞代码变成非阻塞的,比如通过把这部分代码通过 goroutine 去运行,但是要注意一点,如果你的服务器处理的流量足够的大,那么这种做法将会导致创建大量的 goroutines 极大地消耗系统资源,所以我一般建议你用 goroutine pool 来做 goroutines 的复用和管理,以及节省系统资源。 ## I/O 事件 `gnet` 目前支持的 I/O 事件如下: - `OnInitComplete` 当 server 初始化完成之后调用。 - `OnOpened` 当连接被打开的时候调用。 - `OnClosed` 当连接被关闭的时候调用。 - `React` 当 server 端接收到从 client 端发送来的数据的时候调用。(你的核心业务代码一般是写在这个方法里) - `Tick` 服务器启动的时候会调用一次,之后就以给定的时间间隔定时调用一次,是一个定时器方法。 - `PreWrite` 预先写数据方法,在 server 端写数据回 client 端之前调用。 ### 定时器 `Tick` 会每隔一段时间触发一次,间隔时间你可以自己控制,设定返回的 `delay` 变量就行。 定时器的第一次触发是在 `gnet.Serving` 事件之后。 ``` events.Tick = func() (delay time.Duration, action Action){ log.Printf("tick") delay = time.Second return } ``` ## UDP 支持 `gnet` 支持 UDP 协议,在 `gnet.Serve` 里绑定 UDP 地址即可,`gnet` 的 UDP 支持有如下的特性: - 数据进入服务器之后立刻写回客户端,不做缓存。 - `OnOpened` 和 `OnClosed` 这两个事件在 UDP 下不可用,唯一可用的事件是 `React`。 ## 使用多核 `gnet.WithMulticore(true)` 参数指定了 `gnet` 是否会使用多核来进行服务,如果是 `true` 的话就会使用多核,否则就是单核运行,利用的核心数一般是机器的 CPU 数量。 ## 负载均衡 `gnet` 目前内置的负载均衡算法是轮询调度 Round-Robin,暂时不支持自定制。 ## SO\_REUSEPORT 端口复用 服务器支持 [SO\_REUSEPORT](https://lwn.net/Articles/542629/) 端口复用特性,允许多个 sockets 监听同一个端口,然后内核会帮你做好负载均衡,每次只唤醒一个 socket 来处理 accept 请求,避免惊群效应。 开启这个功能也很简单,设置 options 参数即可: ``` gnet.Serve(events, "tcp://:9000", gnet.WithMulticore(true))) ``` # 性能测试 ## Linux (epoll) ### 系统参数 ``` # Machine information OS : Ubuntu 18.04/x86_64 CPU : 8 Virtual CPUs Memory : 16.0 GiB # Go version and configurations Go Version : go1.12.9 linux/amd64 GOMAXPROCS=8 ``` ### 同类型的网络库性能对比: #### Echo Server ![echolinux.png](https://ask.qcloudimg.com/http-save/1303222/g3c6hz902p.png) #### HTTP Server ![httplinux.png](https://ask.qcloudimg.com/http-save/1303222/1jq6mtct5j.png) ## FreeBSD (kqueue) ### 系统参数 ``` # Machine information OS : macOS Mojave 10.14.6/x86_64 CPU : 4 CPUs Memory : 8.0 GiB # Go version and configurations Go Version : go version go1.12.9 darwin/amd64 GOMAXPROCS=4 ``` #### Echo Server ![echomac.png](https://ask.qcloudimg.com/http-save/1303222/dkn8yx6nwv.png) #### HTTP Server ![httpmac.png](https://ask.qcloudimg.com/http-save/1303222/lrtzlusmka.png) # 证书 `gnet` 的源码允许用户在遵循 MIT [开源证书](https://github.com/panjf2000/gnet/blob/master/LICENSE) 规则的前提下使用。 # 相关文章 - [A Million WebSockets and Go](https://www.freecodecamp.org/news/million-websockets-and-go-cc58418460bb/) - [Going Infinite, handling 1M websockets connections in Go](https://speakerdeck.com/eranyanay/going-infinite-handling-1m-websockets-connections-in-go) - [gnet: 一个轻量级且高性能的 Golang 网络库](https://taohuawu.club/go-event-loop-networking-library-gnet) # 待做事项 > gnet 还在持续开发的过程中,所以这个仓库的代码和文档会一直持续更新,如果你对 gnet 感兴趣的话,欢迎给这个开源库贡献你的代码,还有你要是喜欢 gnet 的话,可以给个星星鼓励一下哦 ~~

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2199 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传