简介
gnet
是一个基于 Event-Loop 事件驱动的高性能和轻量级网络库。这个库直接使用 epoll 和 kqueue 系统调用而非标准 Golang 网络包:net 来构建网络应用,它的工作原理类似于两个开源的网络库:libuv 和 libevent。
这个项目存在的价值是提供一个在网络包处理方面能和 Redis、Haproxy 这两个项目具有相近性能的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
默认的线程模型
主从多 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 线程,
通信机制
gnet
的『主从 Reactors 多线程』模型是基于 Golang 里的 Goroutines的,一个 Reactor 挂载在一个 Goroutine 上,所以在 gnet
的这个网络模型里主 Reactor/Goroutine 与从 Reactors/Goroutines 有海量通信的需求,因此 gnet
里必须要有一个能在 Goroutines 之间进行高效率的通信的机制,gnet
没有选择 Golang 里的主流方案:基于 Channel 的 CSP 模型,而是选择了性能更好、基于 Ring-Buffer 的 Disruptor 方案。
go-disruptor:高性能消息分发队列 LMAX Disruptor 的 Golang 实现。
自动扩容的 Ring-Buffer
gnet
利用 Ring-Buffer 来缓存 TCP 流数据以及管理内存使用。
开始使用
安装
1 | $ go get -u github.com/panjf2000/gnet |
使用示例
用 gnet
来构建网络服务器是非常简单的,只需要把关心的事件注册到 gnet.Events
里面,然后把它和绑定的监听地址一起传递给 gnet.Serve
方法就完成了。在服务器开始工作之后,每一条到来的网络连接会在各个事件之间传递,如果你想在某个事件中关闭某条连接或者关掉整个服务器的话,直接把 gnet.Action
设置成 Cosed
或者 Shutdown
就行了。
Echo 服务器是一种最简单网络服务器,把它作为 gnet
的入门例子在再合适不过了,下面是一个最简单的 echo server,它监听了 9000 端口:
不带阻塞逻辑的 echo 服务器
1 | package main |
上面的例子里 gnet
实例只注册了一个 React
事件。一般来说,主要的业务逻辑代码会写在这个事件方法里,这个方法会在服务器接收到客户端写过来的数据之时被调用,然后处理输入数据(这里只是把数据 echo 回去)并且在处理完之后把需要输出的数据赋值给 out
变量然后返回,之后你就不用管了,gnet
会帮你把数据写回客户端的。
带阻塞逻辑的 echo 服务器
1 | package main |
如果业务逻辑里包含阻塞代码,那么应该把这些阻塞代码变成非阻塞的,比如通过把这部分代码通过 goroutine 去运行,但是要注意一点,如果你的服务器处理的流量足够的大,那么这种做法将会导致创建大量的 goroutines 极大地消耗系统资源,所以我一般建议你用 goroutine pool 来做 goroutines 的复用和管理,以及节省系统资源。
I/O 事件
gnet
目前支持的 I/O 事件如下:
OnInitComplete
当 server 初始化完成之后调用。OnOpened
当连接被打开的时候调用。OnClosed
当连接被关闭的时候调用。React
当 server 端接收到从 client 端发送来的数据的时候调用。(你的核心业务代码一般是写在这个方法里)Tick
服务器启动的时候会调用一次,之后就以给定的时间间隔定时调用一次,是一个定时器方法。PreWrite
预先写数据方法,在 server 端写数据回 client 端之前调用。
多地址绑定
1 | // 在同一个 Server 上同时绑定 TCP 和 Unix-Socket 两个地址 |
定时器
Tick
会每隔一段时间触发一次,间隔时间你可以自己控制,设定返回的 delay
变量就行。
定时器的第一次触发是在 gnet.Serving
事件之后。
1 | events.Tick = func() (delay time.Duration, action Action){ |
UDP 支持
gnet
支持 UDP 协议,在 gnet.Serve
里绑定 UDP 地址即可,gnet
的 UDP 支持有如下的特性:
- 数据进入服务器之后立刻写回客户端,不做缓存。
OnOpened
和OnClosed
这两个事件在 UDP 下不可用,唯一可用的事件是React
。
使用多核
Events.Multicore
参数指定了 gnet
是否会使用多核来进行服务,如果是 true
的话就会使用多核,否则就是单核运行,利用的核心数一般是机器的 CPU 数量。
负载均衡
gnet
目前内置的负载均衡算法是轮询调度 Round-Robin,暂时不支持自定制。
SO_REUSEPORT 端口复用
服务器支持 SO_REUSEPORT 端口复用特性,允许多个 sockets 监听同一个端口,然后内核会帮你做好负载均衡,每次只唤醒一个 socket 来处理 accept 请求,避免惊群效应。
开启这个功能也很简单,在要绑定的监听地址后面设置 reuseport=true
即可:
1 | gnet.Serve(events, "tcp://:9000?reuseport=true")) |