Golang 高性能 Websocket 库 gws 使用与设计(一)

devtools/2024/9/24 15:51:58/

前言

大家好这里是,白泽,这期分析一下 golang 开源高性能 websocket 库 gws。

视频讲解请关注📺B站:白泽talk

image-20240726234405804

介绍

  1. gws:https://github.com/lxzan/gws |GitHub 🌟 1.2k,高性能的 websocket 库,代码双语注释,适合有开发经验的同学进阶学习。
  2. gws 的两个特性
  • High IOPS Low Latency(高I/O,低延迟)

  • Low Memory Usage(低内存占用)

可以从下图看到: payload 越高,性能相比其他 websocket 库越是优越,如何做到?

image-20240723220947562

gws chatroom 架构图

这是 gws 的官方聊天室 demo 的架构图,绘制在这里帮助各位理解什么是全双工的通信模式。

image-20240723212541706

WebSocket 与 HTTP 一样是应用层的协议,只需要 TCP 完成三次握手之后,Golang 的 net/http 库提供了 Hijack() 方法,将 TCP 套接字(活跃的一个会话),从 HTTP 劫持,此后 tcp 的连接将由 WebSocket 管理,脱离了 HTTP 协议的范畴。

而只要获取了 TCP 的套接字,何时发送和接受数据,都是由应用层决定的,传输层的 TCP 套接字只是被编排的对象(单工/双工),自然可以实现服务端主动发送数据。

缓冲池

为什么 payload 越高,性能相比其他 websocket 库越是优越?

原因:gws 中的读写操作,全部使用了缓冲池。

image-20240726220546231

binaryPool    = internal.NewBufferPool(128, 256*1024) // 缓冲池

读缓冲:每次读取是一次系统调用,因此可以读取一段数据,且用一个 offset 定位消费的位置,减少读取次数。

写缓冲:每次写入是一次系统调用,因此可以多次写入 buffer,统一 flush。

缓冲池:为不同大小的 buffer 提供了缓冲池,大段 buffer 的创建次数减少,减少 GC 压力 & 创建对象和销毁对象时间。

// NewBufferPool Creating a memory pool
// Left, right indicate the interval range of the memory pool, they will be transformed into pow(2,n)。
// Below left, Get method will return at least left bytes; above right, Put method will not reclaim the buffer.
func NewBufferPool(left, right uint32) *BufferPool {var begin, end = int(binaryCeil(left)), int(binaryCeil(right))var p = &BufferPool{begin:  begin,end:    end,shards: map[int]*sync.Pool{},}for i := begin; i <= end; i *= 2 {capacity := ip.shards[i] = &sync.Pool{New: func() any { return bytes.NewBuffer(make([]byte, 0, capacity)) },}}return p
}

使用循环从 beginend,每次容量翻倍(乘以2),为每个容量创建一个 sync.Pool 实例。sync.Pool 是Go语言标准库中的一个类型,用于存储和回收临时对象。

使用缓冲池中的 bufferconn(网络连接)中读取和写入数据时,通常会执行以下步骤:

  1. 从缓冲池获取缓冲区:使用 Get 方法从缓冲池中获取一个 buffer
  2. 读取数据:如果需要从 conn 读取数据,可以将 buffer 用作读取操作的目的地。
  3. 处理数据:根据需要处理读取到的数据。
  4. 写入数据:如果需要写入数据,可以将数据写入从缓冲池获取的 buffer,然后从 buffer 写入 conn
  5. 释放缓冲区:使用完毕后,将 buffer 放回缓冲池,以便重用。

设计一个 WebScket 库

编写WebSocket库时,有几个关键点会影响其性能,尤其是在高并发场景下。

下面针对这些场景,部分给出一些 demo 写法(伪代码),可以从中提炼一些通用的项目设计方法:

  • 事件驱动模型: 使用非阻塞的事件驱动架构可以提高性能,因为它允许WebSocket库在单个线程内处理多个连接,而不会因等待I/O操作而阻塞。
package mainimport ("fmt""time"
)func main() {eventChan := make(chan string)readyChan := make(chan bool)// 模拟WebSocket连接go func() {time.Sleep(2 * time.Second)eventChan <- "connected"readyChan <- true}()// 事件处理循环for {select {case event := <-eventChan:fmt.Println("Event received:", event)case <-readyChan:fmt.Println("WebSocket is ready to use")return}}
}
  • 并发处理: 库如何处理并发连接和消息是影响性能的重要因素。使用goroutines或线程池可以提高并发处理能力。

  • 消息压缩: 支持消息压缩(如permessage-deflate扩展)可以减少传输数据量,但同时也会增加CPU的使用率,需要找到合适的平衡点。

  • 内存管理: 优化内存使用,比如通过减少内存分配和重用缓冲区,可以提高性能并减少垃圾回收的压力。

var buffer = make([]byte, 0, 1024)func readMessage(conn *websocket.Conn) {_, buffer, err := conn.ReadMessage()if err != nil {// 处理错误}// 使用buffer中的数据
}
  • 连接池管理: 有效的连接池管理可以减少连接建立和关闭的开销,特别是在长连接和频繁通信的场景下。
type WebSocketPool struct {pool map[*websocket.Conn]struct{}
}func (p *WebSocketPool) Add(conn *websocket.Conn) {p.pool[conn] = struct{}{}
}func (p *WebSocketPool) Remove(conn *websocket.Conn) {delete(p.pool, conn)
}func (p *WebSocketPool) Broadcast(message []byte) {for conn := range p.pool {conn.WriteMessage(websocket.TextMessage, message)}
}
  • 锁和同步机制: 在多线程或goroutine环境中,合理的锁和同步机制是必要的,以避免竞态条件和死锁,但过多的锁竞争会降低性能。
import "sync"var pool = &WebSocketPool{pool: make(map[*websocket.Conn]struct{}),
}
var mu sync.Mutexfunc broadcast(message []byte) {mu.Lock()defer mu.Unlock()for conn := range pool.pool {conn.WriteMessage(websocket.TextMessage, message)}
}
  • I/O模型: 使用非阻塞I/O或异步I/O模型可以提高性能,因为它们允许在等待网络数据时执行其他任务。
func handleConnection(conn *websocket.Conn) {go func() {for {_, message, err := conn.ReadMessage()if err != nil {return // 处理错误}// 处理接收到的消息}}()
}
  • 协议实现: 精确且高效的WebSocket协议实现,包括帧的处理、掩码的添加和去除、以及控制帧的管理,都是影响性能的因素。
func (c *Conn) genFrame(opcode Opcode, payload internal.Payload, isBroadcast bool) (*bytes.Buffer, error) {if opcode == OpcodeText && !payload.CheckEncoding(c.config.CheckUtf8Enabled, uint8(opcode)) {return nil, internal.NewError(internal.CloseUnsupportedData, ErrTextEncoding)}var n = payload.Len()if n > c.config.WriteMaxPayloadSize {return nil, internal.CloseMessageTooLarge}var buf = binaryPool.Get(n + frameHeaderSize)buf.Write(framePadding[0:])if c.pd.Enabled && opcode.isDataFrame() && n >= c.pd.Threshold {return c.compressData(buf, opcode, payload, isBroadcast)}var header = frameHeader{}headerLength, maskBytes := header.GenerateHeader(c.isServer, true, false, opcode, n)_, _ = payload.WriteTo(buf)var contents = buf.Bytes()if !c.isServer {internal.MaskXOR(contents[frameHeaderSize:], maskBytes)}var m = frameHeaderSize - headerLengthcopy(contents[m:], header[:headerLength])buf.Next(m)return buf, nil
}
  • 错误处理和恢复: 健壮的错误处理和异常恢复机制可以防止个别连接的问题影响整个服务的性能。

  • 测试和基准: 通过广泛的测试和基准测试来识别性能瓶颈,并根据测试结果进行优化。


http://www.ppmy.cn/devtools/85260.html

相关文章

mac下010editor的配置文件路径

1.打开访达&#xff0c;点击前往&#xff0c;输入~/.config 2.打开这个文件夹 把里面的 010 Editor.ini 文件删除即可&#xff0c;重新安装010 Editor即可

SpringBoot 项目配置文件注释乱码的问题解决方案

一、问题描述 在项目的配置文件中&#xff0c;我们写了一些注释&#xff0c;如下所示&#xff1a; 但是再次打开注释会变成乱码&#xff0c;如下所示&#xff1a; 那么如何解决呢&#xff1f; 二、解决方案 1. 点击” File→Setting" 2. 搜索“File Encodings”, 将框…

探索 IPython %%sql 魔术:数据库交互的高效工具

探索 IPython %%sql 魔术&#xff1a;数据库交互的高效工具 在数据科学和分析领域&#xff0c;IPython 提供了一个强大的交互式环境&#xff0c;允许用户执行 Python 代码并与各种数据源进行交互。%%sql 魔术命令是 IPython 环境中的一个特殊命令&#xff0c;它允许用户直接在…

【Android】广播机制

前言 广播机制是Android中一种非常重要的通信机制&#xff0c;用于在应用程序之间或应用程序的不同组件之间传递信息。广播可以是系统广播&#xff0c;也可以是自定义广播。广播机制主要包括标准广播和有序广播两种类型。 简介 在Android中&#xff0c;广播&#xff08;Broa…

在SQL Server中设置端口

在SQL Server中设置端口,通常是通过SQL Server配置管理器(SQL Server Configuration Manager, 简称SSCM)来完成的。以下是详细的步骤: 一、打开SQL Server配置管理器 通过开始菜单:在Windows的开始菜单中,搜索“SQL Server Configuration Manager”并打开它。通过运行命令…

java 开发学习总结

一&#xff0c;注解 Bean是一个注解,用于告诉 Spring 框架将标注的方法返回的对象注册为一个 Bean。 Bean注解的方法名作为对象的名字。 Bean 一般和 Component或者Configuration 一起使用。 Component 注解的类中不能定义类内部依赖的Bean注解的方法。Configuration可以。 Con…

go语言day19 使用git上传包文件到github Gin框架入门

git分布式版本控制系统_git切换head指针-CSDN博客 获取请求参数并和struct结构体绑定_哔哩哔哩_bilibili &#xff08;gin框架&#xff09; GO: 引入GIn框架_go 引入 gin-CSDN博客 使用git上传包文件 1&#xff09;创建一个github账户&#xff0c;进入Repositories个人仓…

【数学建模】基于贪心算法的电力市场的输电阻塞管理(附论文及matlab、lingo代码)

适合数学建模新手研究的题目&#xff0c;备战国赛的同学可以拿这道题目练手&#xff0c;本文含论文代码&#xff0c;帮助解题理解思路。 题目&#xff1a; &#xff08;1&#xff09;题目信息&#xff1a; 某电网有若干台发电机组和若干条主要线路&#xff0c;每条线路上的有…