温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

开源一个轻量级且高性能的Go网络框架gnet方法是什么

发布时间:2021-11-15 10:48:43 来源:亿速云 阅读:284 作者:iii 栏目:大数据

这篇文章主要介绍“开源一个轻量级且高性能的Go网络框架gnet方法是什么”,在日常操作中,相信很多人在开源一个轻量级且高性能的Go网络框架gnet方法是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”开源一个轻量级且高性能的Go网络框架gnet方法是什么”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

gnet 是一个基于事件驱动的高性能和轻量级网络框架。它直接使用 epoll 和 kqueue 系统调用而非标准 Golang 网络包:net 来构建网络应用,它的工作原理类似两个开源的网络库:netty 和 libuv。

这个项目存在的价值是提供一个在网络包处理方面能和 Redis、Haproxy 这两个项目具有相近性能的 Go 语言网络服务器框架。

gnet 的亮点在于它是一个高性能、轻量级、非阻塞的纯 Go 实现的传输层(TCP/UDP/Unix-Socket)网络框架,开发者可以使用 gnet 来实现自己的应用层网络协议,从而构建出自己的应用层网络应用:比如在 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 端口重用

核心设计

多线程/Go程模型

主从多 Reactors 模型

gnet 重新设计开发了一个新内置的多线程/Go程模型:『主从多 Reactors』,这也是 netty 默认的线程模型,下面是这个模型的原理图:

开源一个轻量级且高性能的Go网络框架gnet方法是什么

它的运行流程如下面的时序图: 开源一个轻量级且高性能的Go网络框架gnet方法是什么

主从多 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 线程,

模型的架构图如下所示:

开源一个轻量级且高性能的Go网络框架gnet方法是什么

它的运行流程如下面的时序图: 开源一个轻量级且高性能的Go网络框架gnet方法是什么

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 利用 Ring-Buffer 来缓冲网络数据以及管理内存。

开源一个轻量级且高性能的Go网络框架gnet方法是什么

开始使用

安装

$ go get -u github.com/panjf2000/gnet

使用示例

详细的文档在这里: 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 示例。

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.Serving 事件之后,如果你要设置定时器,别忘了设置 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.OnOpenedEventHandler.OnClosed 这两个事件在 UDP 下不可用,唯一可用的事件是 React

使用多核

gnet.WithMulticore(true) 参数指定了 gnet 是否会使用多核来进行服务,如果是 true 的话就会使用多核,否则就是单核运行,利用的核心数一般是机器的 CPU 数量。

负载均衡

gnet 目前内置的负载均衡算法是轮询调度 Round-Robin,暂时不支持自定制。

SO_REUSEPORT 端口复用

服务器支持 SO_REUSEPORT 端口复用特性,允许多个 sockets 监听同一个端口,然后内核会帮你做好负载均衡,每次只唤醒一个 socket 来处理 accept 请求,避免惊群效应。

开启这个功能也很简单,使用 functional options 设置一下即可:

gnet.Serve(events, "tcp://:9000", gnet.WithMulticore(true), gnet.WithReusePort(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

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

到此,关于“开源一个轻量级且高性能的Go网络框架gnet方法是什么”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

go
AI