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

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

Build Status Codecov Go Report Card

Go Godoc for gnet Release

[中文]

gnet 是一个快速、轻量级的 Event-Loop 网络框架。它直接使用 epollkqueue 系统调用,而不是使用标准的 Go net 包,其工作方式类似于 libuvlibevent

该项目的目标是为 Go 创建一个服务器框架,在数据包处理性能上与 RedisHaproxy 相媲美。

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 的默认多线程模型。以下是示意图:

multi_reactor

其工作序列图如下:

reactor

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

multi_reactor_thread_pool

其工作序列图如下:

multi-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 对象,该对象传递给各种事件以区分客户端。您可以在任何时候通过从事件返回 CloseShutdown 操作来关闭客户端或关闭服务器。

让您开始使用 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 地址。

  • 所有传入和传出的数据包不会被缓冲,而是单独发送。
  • OnOpenedOnClosed 事件不适用于 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 服务器

Echo 服务器性能

HTTP 服务器

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 服务器

Echo 服务器性能

HTTP 服务器

HTTP 服务器性能

许可证

gnet 中的源代码可在 MIT 许可证 下获得。

致谢

待办事项

gnet 仍在积极开发中,因此代码和文档将继续更新。如果您对 gnet 感兴趣,请随时为其贡献代码,如果您喜欢 gnet,请给它一个星标 ~~


1 回复

gnet 确实是一个出色的高性能网络库,它通过直接使用系统调用(如 epollkqueue)而非标准的 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 事件在服务器启动后触发,用于日志记录。
  • OnOpenedOnClosed 事件处理连接的打开和关闭。
  • React 事件处理输入数据,这里简单地回显所有接收到的数据。
  • Tick 事件每 5 秒触发一次,用于定期任务。
  • 服务器使用多核模式(Multicore: true),以利用多 CPU 核心。

对于 UDP 支持,您可以将地址协议改为 UDP,例如 "udp://:9000"。注意,UDP 连接不触发 OnOpenedOnClosed 事件,仅 React 事件可用。

gnet 的性能优势在于其多 Reactor 模型和环形缓冲区机制,这减少了 Goroutine 上下文切换的开销,并优化了内存使用。根据官方基准测试,它在 Linux 和 FreeBSD 系统上均能提供与 Redis 和 Haproxy 相媲美的性能。如果您需要构建自定义应用层协议(如 HTTP 或 Redis 协议),可以在 React 事件中实现协议解析逻辑。

总之,gnet 是一个强大且灵活的工具,适用于需要高性能网络处理的场景。

回到顶部