gnet: 一个轻量级且高性能的 Go 网络框架
GitHub 主页
https://github.com/panjf2000/gnet
欢迎大家围观 ~~,目前还在持续更新,感兴趣的话可以 star 一下暗中观察哦。
简介
gnet 是一个基于事件驱动的高性能和轻量级网络框架。它直接使用 epoll 和 kqueue 系统调用而非标准 Golang 网络包:net 来构建网络应用,它的工作原理类似两个开源的网络库:netty 和 libuv。
这个项目存在的价值是提供一个在网络包处理方面能和 Redis、Haproxy 这两个项目具有相近性能的 Go 语言网络服务器框架。
gnet 的亮点在于它是一个高性能、轻量级、非阻塞的纯 Go 实现的传输层(TCP/UDP/Unix-Socket)网络框架,开发者可以使用 gnet 来实现自己的应用层网络协议(HTTP、RPC、Redis、WebSocket 等等),从而构建出自己的应用层网络应用:比如在 gnet 上实现 HTTP 协议就可以创建出一个 HTTP 服务器 或者 Web 开发框架,实现 Redis 协议就可以创建出自己的 Redis 服务器等等。
gnet 衍生自另一个项目:evio,但性能远胜之。
功能
- 高性能 的基于多线程/Go 程网络模型的 event-loop 事件驱动
- 内置 Round-Robin 轮询负载均衡算法
- 内置 goroutine 池,由开源库 ants 提供支持
- 内置 bytes 内存池,由开源库 pool 提供支持
- 简洁的 APIs
- 基于 Ring-Buffer 的高效内存利用
- 支持多种网络协议:TCP、UDP、Unix Sockets
- 支持两种事件驱动机制:Linux 里的 epoll 以及 FreeBSD 里的 kqueue
- 支持异步写操作
- 灵活的事件定时器
- SO_REUSEPORT 端口重用
- 内置多种编解码器,支持对 TCP 数据流分包:LineBasedFrameCodec, DelimiterBasedFrameCodec, FixedLengthFrameCodec 和 LengthFieldBasedFrameCodec,参考自 netty codec,而且支持自定制编解码器
- 支持 Windows 平台,基于 IOCP 事件驱动机制 Go 标准网络库
- 加入更多的负载均衡算法:随机、最少连接、一致性哈希等等
- 支持 TLS
- 实现
gnet客户端
核心设计
多线程/Go 程网络模型
主从多 Reactors
gnet 重新设计开发了一个新内置的多线程/Go 程网络模型:『主从多 Reactors』,这也是 netty 默认的多线程网络模型,下面是这个模型的原理图:
它的运行流程如下面的时序图:
主从多 Reactors + 线程/Go 程池
你可能会问一个问题:如果我的业务逻辑是阻塞的,那么在 EventHandler.React 注册方法里的逻辑也会阻塞,从而导致阻塞 event-loop 线程,这时候怎么办?
正如你所知,基于 gnet 编写你的网络服务器有一条最重要的原则:永远不能让你业务逻辑(一般写在 EventHandler.React 里)阻塞 event-loop 线程,否则的话将会极大地降低服务器的吞吐量,这也是 netty 的一条最重要的原则。
我的回答是,基于gnet 的另一种多线程/Go 程网络模型:『带线程/Go 程池的主从多 Reactors』可以解决阻塞问题,这个新网络模型通过引入一个 worker pool 来解决业务逻辑阻塞的问题:它会在启动的时候初始化一个 worker pool,然后在把 EventHandler.React里面的阻塞代码放到 worker pool 里执行,从而避免阻塞 event-loop 线程,
模型的架构图如下所示:
它的运行流程如下面的时序图:
gnet 通过利用 ants goroutine 池(一个基于 Go 开发的高性能的 goroutine 池 ,实现了对大规模 goroutines 的调度管理、goroutines 复用)来实现『主从多 Reactors + 线程/Go 程池』网络模型。关于 ants 的全部功能和使用,可以在 ants 文档 里找到。
gnet 内部集成了 ants 以及提供了 pool.NewWorkerPool 方法来初始化一个 ants goroutine 池,然后你可以把 EventHandler.React 中阻塞的业务逻辑提交到 goroutine 池里执行,最后在 goroutine 池里的代码调用 gnet.Conn.AsyncWrite 方法把处理完阻塞逻辑之后得到的输出数据异步写回客户端,这样就可以避免阻塞 event-loop 线程。
有关在 gnet 里使用 ants goroutine 池的细节可以到这里进一步了解。
自动扩容的 Ring-Buffer
gnet 内置了 inbound 和 outbound 两个 buffers,基于 Ring-Buffer 原理实现,分别用来缓冲输入输出的网络数据以及管理内存。
对于 TCP 协议的流数据,使用 gnet 不需要业务方为了解析应用层协议而自己维护和管理 buffers,gnet 会替业务方完成缓冲和管理网络数据的任务,降低业务代码的复杂性以及降低开发者的心智负担,使得开发者能够专注于业务逻辑而非一些底层功能。
开始使用
前提
gnet 需要 Go 版本 >= 1.9。
安装
go get -u github.com/panjf2000/gnet
gnet 支持作为一个 Go module 被导入,基于 Go 1.11 Modules (Go 1.11+),只需要在你的项目里直接 import "github.com/panjf2000/gnet",然后运行 go [build|run|test] 自动下载和构建需要的依赖包。
使用示例
详细的文档在这里: gnet 接口文档,不过下面我们先来了解下使用 gnet 的简略方法。
用 gnet 来构建网络服务器是非常简单的,只需要实现 gnet.EventHandler接口然后把你关心的事件函数注册到里面,最后把它连同监听地址一起传递给 gnet.Serve 函数就完成了。在服务器开始工作之后,每一条到来的网络连接会在各个事件之间传递,如果你想在某个事件中关闭某条连接或者关掉整个服务器的话,直接把 gnet.Action 设置成 Cosed 或者 Shutdown就行了。
Echo 服务器是一种最简单网络服务器,把它作为 gnet 的入门例子在再合适不过了,下面是一个最简单的 echo server,它监听了 9000 端口:
不带阻塞逻辑的 echo 服务器
package main
import (
"log"
"github.com/panjf2000/gnet"
)
type echoServer struct {
*gnet.EventServer
}
func (es *echoServer) React(c gnet.Conn) (out []byte, action gnet.Action) {
out = c.Read()
c.ResetBuffer()
return
}
func main() {
echo := new(echoServer)
log.Fatal(gnet.Serve(echo, "tcp://:9000", gnet.WithMulticore(true)))
}
正如你所见,上面的例子里 gnet 实例只注册了一个 EventHandler.React 事件。一般来说,主要的业务逻辑代码会写在这个事件方法里,这个方法会在服务器接收到客户端写过来的数据之时被调用,然后处理输入数据(这里只是把数据 echo 回去)并且在处理完之后把需要输出的数据赋值给 out 变量然后返回,之后你就不用管了,gnet 会帮你把数据写回客户端的。
带阻塞逻辑的 echo 服务器
package main
import (
"log"
"time"
"github.com/panjf2000/gnet"
"github.com/panjf2000/gnet/pool"
)
type echoServer struct {
*gnet.EventServer
pool *pool.WorkerPool
}
func (es *echoServer) React(c gnet.Conn) (out []byte, action gnet.Action) {
data := append([]byte{}, c.Read()...)
c.ResetBuffer()
// Use ants pool to unblock the event-loop.
_ = es.pool.Submit(func() {
time.Sleep(1 * time.Second)
c.AsyncWrite(data)
})
return
}
func main() {
p := pool.NewWorkerPool()
defer p.Release()
echo := &echoServer{pool: p}
log.Fatal(gnet.Serve(echo, "tcp://:9000", gnet.WithMulticore(true)))
}
正如我在『主从多 Reactors + 线程/Go 程池』那一节所说的那样,如果你的业务逻辑里包含阻塞代码,那么你应该把这些阻塞代码变成非阻塞的,比如通过把这部分代码通过 goroutine 去运行,但是要注意一点,如果你的服务器处理的流量足够的大,那么这种做法将会导致创建大量的 goroutines 极大地消耗系统资源,所以我一般建议你用 goroutine pool 来做 goroutines 的复用和管理,以及节省系统资源。
各种 gnet 示例:
Echo Server
HTTP Server
Push Server
Codec Client/Server
更详细的代码在这里: gnet 示例。
I/O 事件
gnet 目前支持的 I/O 事件如下:
EventHandler.OnInitComplete当 server 初始化完成之后调用。EventHandler.OnOpened当连接被打开的时候调用。EventHandler.OnClosed当连接被关闭的之后调用。EventHandler.React当 server 端接收到从 client 端发送来的数据的时候调用。(你的核心业务代码一般是写在这个方法里)EventHandler.Tick服务器启动的时候会调用一次,之后就以给定的时间间隔定时调用一次,是一个定时器方法。EventHandler.PreWrite预先写数据方法,在 server 端写数据回 client 端之前调用。
定时器
EventHandler.Tick 会每隔一段时间触发一次,间隔时间你可以自己控制,设定返回的 delay 变量就行。
定时器的第一次触发是在 gnet server 启动之后,如果你要设置定时器,别忘了设置 option 选项:WithTicker(true)。
events.Tick = func() (delay time.Duration, action Action){
log.Printf("tick")
delay = time.Second
return
}
UDP 支持
gnet 支持 UDP 协议,在 gnet.Serve 里绑定 UDP 地址即可,gnet 的 UDP 支持有如下的特性:
- 数据进入服务器之后立刻写回客户端,不做缓存。
EventHandler.OnOpened和EventHandler.OnClosed这两个事件在 UDP 下不可用,唯一可用的事件是React。
使用多核
gnet.WithMulticore(true) 参数指定了 gnet 是否会使用多核来进行服务,如果是 true 的话就会使用多核,否则就是单核运行,利用的核心数一般是机器的 CPU 数量。
负载均衡
gnet 目前内置的负载均衡算法是轮询调度 Round-Robin,暂时不支持自定制。
SO_REUSEPORT 端口复用
服务器支持 SO_REUSEPORT 端口复用特性,允许多个 sockets 监听同一个端口,然后内核会帮你做好负载均衡,每次只唤醒一个 socket 来处理 accept 请求,避免惊群效应。
默认情况下,gnet 也不会有惊群效应,因为 gnet 默认的网络模型是主从多 Reactors,只会有一个主 reactor 在监听端口以及接受新连接。所以,开不开启 SO_REUSEPORT 选项是无关紧要的,只是开启了这个选项之后 gnet 的网络模型将会切换成 evio 的旧网络模型,这一点需要注意一下。
开启这个功能也很简单,使用 functional options 设置一下即可:
gnet.Serve(events, "tcp://:9000", gnet.WithMulticore(true), gnet.WithReusePort(true)))
多种内置的 TCP 流编解码器
gnet 内置了多种用于 TCP 流分包的编解码器。
目前一共实现了 4 种常见的编解码器:LineBasedFrameCodec, DelimiterBasedFrameCodec, FixedLengthFrameCodec 和 LengthFieldBasedFrameCodec,基本上能满足大多数应用场景的需求了;而且 gnet 还允许用户实现自己的编解码器:只需要实现 gnet.ICodec 接口,并通过 functional options 替换掉内部默认的编解码器即可。
这里有一个使用编解码器对 TCP 流分包的例子。
性能测试
同类型的网络库性能对比
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
HTTP Server
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
HTTP Server
证书
gnet 的源码允许用户在遵循 MIT 开源证书 规则的前提下使用。
贡献者
请在提 PR 之前仔细阅读 Contributing Guidelines,感谢那些为 gnet 贡献过代码的开发者!
致谢
相关文章
- A Million WebSockets and Go
- Going Infinite, handling 1M websockets connections in Go
- Go netpoll I/O 多路复用构建原生网络模型之源码深度解析
- gnet: 一个轻量级且高性能的 Golang 网络库
JetBrains 开源证书支持
智能推荐
(Java)反射的应用 - 取得类的结构
文章目录 一、基本概念 二、取得所实现的全部接口 三、取得父类 四、取得全部构造方法 五、取得全部方法 六、取得全部属性 一、基本概念 在反射机制中,还可以通过反射得到一个类的完整结构,这就需要使用 java.lang.reflect 包中的以下几个类: 这三个类都是 AccessibleObject 类的子类: 二、取得所实现的全部接口 要取得一个类所实现的全部接口,必须使用 Class 类中的...
ORM-外键关联基本使用
外键 在Mysql中,外键可以让表之间关系变得更加紧密, 在SQlAlchemy中, 通过ForeignKey类来实现,并且可以指定表的外键约束 FroeignKey的导入 在从表中条件一个模型类.字段(属性)即可 外键关联的代码和示例图 图说明 外键约束的删除 如果删除了主表中的数据, 从表的数据会怎么样? 需要设置 "RESTRICT" : 主表数据被删除, 会阻止删除 &...
Linux操作心得(1)
Ubuntu 16.04 (1)今天遇到一个蜜汁尴尬的情况,一本书上的示例,要求我建一个文件夹及子文件夹,然而明明创建的文件却没有显示 按书上此时应该出现一个文件夹,但并没有: 但可以进入,作为小白看不懂,后来发现是因为/XX指的是将文件建立在根目录了,因此不管怎样,就算用ls,或ll命令都查不到的,此时正确方法应该是去掉/backup前的/,如图就解决了文件夹的创建过程,还有一种傻瓜式方法就是直...
如何写出优美的 JavaScript 代码?
作者:尹锋 链接:https://www.zhihu.com/question/20635785/answer/223515216 1,避免使用 js 糟粕和鸡肋 这些年来,随着 HTML5 和 Node.js 的发展,JavaScript 在各个领域遍地开花,已经从“世界上最被误解的语言”变成了“世界上最流行的语言”。但是由于历史原因,JavaSc...
猜你喜欢
07-zookeeper的watcher机制原理
zookeeper的watcher机制原理 Watcher 的基本流程 zookeeper的watcher机制,总的来说可以分为三个过程: 客户端注册Watcher。 服务器处理Watcher。 客户端回调Watcher。 客户端注册 watcher有3种方式,getData、exists、getChildren。以如下代码为例,来分析整个触发机制的原理 基于zkclient客户端发起一个数据操作...
Linux搭建Nexus私服
Nexus是什么 Nexus是一个强大的Maven仓库管理器,它极大地简化了自己内部仓库的维护和外部仓库的访问。利用Nexus你可以只在一个地方就能够完全控制访问 和部署在你所维护仓库中的每个Artifact。Nexus是一套“开箱即用”的系统不需要数据库,它使用文件系统加Lucene来组织数据。简单来说,它就是我们自己维护管理的maven仓库,仅限本人或公司内部使用,他人...
【Elastic Stack上】Elastic Search快速入门,让你对ELK日志架构不再困惑
课程介绍 Elastic Stack简介 Elasticsearch的介绍与安装 Elasticsearch的快速入门 Elasticsearch的核心讲解中文分词 全文搜索 Elasticsearch集群 Java客户端讲解 1、Elastic Stack简介 如果你没有听说过Elastic Stack,那你一定听说过ELK,实际上ELK是三款软件的简称,分别是Elasticsearch、Log...
浅谈Java中==和equals()区别
Java基础 浅谈Java中==和equals()区别 == 运算符 equals(): 方法 浅谈Java中==和equals()区别 == 运算符 可以使用在基本数据类型变量和引用数据类型变量中 如果比较的是基本数据类型变量,比较两个变量保存的数据是否相等(不一定要类型相同) 如果比较的是引用类型变量,比较的是两个变量的地址值是否相同,即两个引用是否指向同一个对象实体 equals(): 方法...
Python-基础课-第一节-03-Python环境搭建
3.1Python环境搭建 Python是一个跨平台、可移植的编程语言,因此可在windows、Linux和Mac OS X系统 中安装使用。 安装完成后,你会得到Python解释器环境,可以通过终端输入python命令查看本地 是否已经按照python以及python版本。这里有一点需要注意的是,如果没有将 python的安装目录添加到环境变量中,会报错(python不是内部命令或外部命令, 也...
