纯Go编写的高性能轻量级非阻塞事件循环网络库gnet详解
纯Go编写的高性能轻量级非阻塞事件循环网络库gnet详解

[中文]
gnet 是一个快速、轻量级的 Event-Loop 网络框架。它直接使用 epoll 和 kqueue 系统调用,而不是使用标准的 Go net 包,其工作方式类似于 libuv 和 libevent。
该项目的目标是为 Go 创建一个服务器框架,在数据包处理性能上与 Redis 和 Haproxy 相媲美。
gnet 自称是一个用纯 Go 编写的高性能、轻量级、非阻塞的网络库,工作在传输层,支持 TCP/UDP/Unix-Socket 协议,因此允许开发者在 gnet 之上实现自己的应用层协议,构建多样化的网络应用程序。例如,如果你在 gnet 上实现 HTTP 协议,你将得到一个 HTTP 服务器或 Web 框架;如果你在 gnet 上实现 Redis 协议,你将拥有一个 Redis 服务器,依此类推。
gnet 源自项目 evio,但具有更高的性能。
特性
- 高性能 多线程/goroutine 模型下的 Event-Loop
- 内置负载均衡算法:轮询
- 简洁的 API
- 高效的内存使用:环形缓冲区
- 支持多种协议:TCP、UDP 和 Unix 套接字
- 支持两种事件通知机制:Linux 下的 epoll 和 FreeBSD 下的 kqueue
- 支持异步写操作
- 允许在同一个 Event-Loop 上绑定多个网络地址
- 灵活的定时器事件
- SO_REUSEPORT 套接字选项
关键设计
多线程/goroutine 模型
gnet 重新设计并实现了一个新的内置多线程/goroutine 模型:“多 Reactors”,这也是 netty 的默认多线程模型。以下是示意图:

其工作序列图如下:

gnet 的后续多线程/goroutine 模型:“带线程/goroutine 池的多 Reactors” 正在开发中,即将交付。新模型的架构图如下:

其工作序列图如下:

通信机制
gnet 在 Go 的 Goroutines 下构建其"多 Reactors"模型,每个 Goroutine 一个 Reactor。因此,在 gnet 的这种网络模型中,需要在 Goroutines 之间处理极大量的消息,这意味着 gnet 需要一种高效的 Goroutines 间通信机制。我选择了一种巧妙的 Disruptor(环形缓冲区)解决方案,它在网络中提供更高的消息分发性能,而不是 Go 最佳实践推荐的模式:CSP(Channel)。
这就是为什么我最终选择了 go-disruptor:LMAX Disruptor(一个高性能的线程间消息传递库)的 Go 语言移植版。
自动扩展环形缓冲区
gnet 利用环形缓冲区来缓存 TCP 流并在网络中管理内存缓存。

入门指南
安装
$ go get -u github.com/panjf2000/gnet
使用
使用 gnet 创建网络服务器很容易。您只需将事件注册到 gnet.Events 并将其与绑定地址一起传递给 gnet.Serve 函数。每个连接都表示为一个 gnet.Conn 对象,该对象传递给各种事件以区分客户端。您可以在任何时候通过从事件返回 Close 或 Shutdown 操作来关闭客户端或关闭服务器。
让您开始使用 gnet 的最简单示例是 echo 服务器。以下是一个基于 gnet 的最简单的 echo 服务器,监听 9000 端口:
package main
import (
"log"
"github.com/panjf2000/gnet"
"github.com/panjf2000/gnet/ringbuffer"
)
func main() {
var events gnet.Events
events.Multicore = true
events.React = func(c gnet.Conn, inBuf *ringbuffer.RingBuffer) (out []byte, action gnet.Action) {
top, tail := inBuf.PreReadAll()
out = append(top, tail...)
inBuf.Reset()
return
}
log.Fatal(gnet.Serve(events, "tcp://:9000"))
}
如您所见,这个 echo 服务器示例只设置了 React 函数,您通常在这里编写主要的业务代码,当服务器从客户端接收到输入数据时,该函数将被调用。然后,输出数据将通过分配 out 变量并在业务代码完成数据处理后返回(在这种情况下,它只是将数据回显)发送回该客户端。
I/O 事件
gnet 中当前支持的 I/O 事件:
OnInitComplete在服务器准备好接受新连接时激活。OnOpened在连接打开时激活。OnClosed在连接关闭时激活。OnDetached在使用Detach返回操作分离连接时激活。React在服务器从连接接收到新数据时激活。Tick在服务器启动后立即激活,并在指定间隔后再次触发。PreWrite在任何数据写入任何客户端套接字之前激活。
多地址
// 将 TCP 和 Unix-Socket 绑定到同一个 gnet 服务器。
gnet.Serve(events, "tcp://:9000", "unix://socket")
定时器
Tick 事件以指定间隔触发定时器。
第一个定时器在 Serving 事件之后立即触发。
events.Tick = func() (delay time.Duration, action Action){
log.Printf("tick")
delay = time.Second
return
}
UDP
Serve 函数可以绑定到 UDP 地址。
- 所有传入和传出的数据包不会被缓冲,而是单独发送。
OnOpened和OnClosed事件不适用于 UDP 套接字,只有React事件可用。
多线程
Events.Multicore 指示服务器是否有效地使用多核创建,如果是,则必须注意在所有事件回调之间同步内存,否则服务器将以单线程运行。服务器中的线程数将自动分配为 runtime.NumCPU() 的值。
负载均衡
gnet 中当前内置的负载均衡算法是轮询。
SO_REUSEPORT
服务器可以利用 SO_REUSEPORT 选项,该选项允许同一主机上的多个套接字绑定到同一端口,操作系统内核负责为您进行负载均衡,它在每个 accept 事件到来时唤醒一个套接字,以解决"惊群"问题。
只需向地址提供 reuseport=true 即可享受此功能:
gnet.Serve(events, "tcp://:9000?reuseport=true"))
性能
在 Linux 上 (epoll)
测试环境
Go Version : go1.12.9 linux/amd64
OS : Ubuntu 18.04/x86_64
CPU : 8 Virtual CPUs
Memory : 16.0 GiB
Echo 服务器

HTTP 服务器

在 FreeBSD 上 (kqueue)
测试环境
Go Version : go version go1.12.9 darwin/amd64
OS : macOS Mojave 10.14.6/x86_64
CPU : 4 CPUs
Memory : 8.0 GiB
Echo 服务器

HTTP 服务器

许可证
gnet 中的源代码可在 MIT 许可证 下获得。
致谢
待办事项
gnet 仍在积极开发中,因此代码和文档将继续更新。如果您对 gnet 感兴趣,请随时为其贡献代码,如果您喜欢 gnet,请给它一个星标 ~~
gnet 确实是一个出色的高性能网络库,它通过直接使用系统调用(如 epoll 和 kqueue)而非标准的 Go net 包,实现了事件驱动的非阻塞 I/O 模型。这种设计使其在处理高并发连接时表现出色,特别适合构建需要低延迟和高吞吐量的服务器应用,如实时通信系统或自定义协议服务器。
以下是一个基于 gnet 的简单 TCP 服务器示例,演示了如何设置事件处理函数并启动服务器。这个示例扩展了基本的 echo 功能,添加了连接管理和定时器事件:
package main
import (
"log"
"time"
"github.com/panjf2000/gnet"
"github.com/panjf2000/gnet/ringbuffer"
)
type echoServer struct {
*gnet.EventServer
}
func (es *echoServer) OnInitComplete(server gnet.Server) (action gnet.Action) {
log.Printf("Echo server is listening on %s (multi-cores: %t, loops: %d)\n",
server.Addr.String(), server.Multicore, server.NumLoops)
return
}
func (es *echoServer) OnOpened(c gnet.Conn) (out []byte, action gnet.Action) {
log.Printf("Connection opened: %s\n", c.RemoteAddr().String())
return
}
func (es *echoServer) OnClosed(c gnet.Conn, err error) (action gnet.Action) {
log.Printf("Connection closed: %s, error: %v\n", c.RemoteAddr().String(), err)
return
}
func (es *echoServer) React(c gnet.Conn, inBuf *ringbuffer.RingBuffer) (out []byte, action gnet.Action) {
data, _ := inBuf.ReadAll()
out = data
return
}
func (es *echoServer) Tick() (delay time.Duration, action gnet.Action) {
log.Println("Tick event triggered")
delay = time.Second * 5
return
}
func main() {
echo := &echoServer{}
log.Fatal(gnet.Serve(echo, "tcp://:9000", gnet.WithMulticore(true)))
}
在这个示例中:
OnInitComplete事件在服务器启动后触发,用于日志记录。OnOpened和OnClosed事件处理连接的打开和关闭。React事件处理输入数据,这里简单地回显所有接收到的数据。Tick事件每 5 秒触发一次,用于定期任务。- 服务器使用多核模式(
Multicore: true),以利用多 CPU 核心。
对于 UDP 支持,您可以将地址协议改为 UDP,例如 "udp://:9000"。注意,UDP 连接不触发 OnOpened 和 OnClosed 事件,仅 React 事件可用。
gnet 的性能优势在于其多 Reactor 模型和环形缓冲区机制,这减少了 Goroutine 上下文切换的开销,并优化了内存使用。根据官方基准测试,它在 Linux 和 FreeBSD 系统上均能提供与 Redis 和 Haproxy 相媲美的性能。如果您需要构建自定义应用层协议(如 HTTP 或 Redis 协议),可以在 React 事件中实现协议解析逻辑。
总之,gnet 是一个强大且灵活的工具,适用于需要高性能网络处理的场景。

