前言

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

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

介绍

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

  • Low Memory Usage(低内存占用)

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

gws chatroom 架构图

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

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

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

缓冲池

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

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

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 := i
p.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 main

import (
"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.Mutex func 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 - headerLength
copy(contents[m:], header[:headerLength])
buf.Next(m)
return buf, nil
}
  • 错误处理和恢复: 健壮的错误处理和异常恢复机制可以防止个别连接的问题影响整个服务的性能。

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

Golang 高性能 Websocket 库 gws 使用与设计(一)的更多相关文章

  1. golang gorilla websocket例子

    WebSocket协议是基于TCP的一种新的网络协议.它实现了浏览器与服务器全双工(full-duplex)通信--允许服务器主动发送信息给客户端. WebSocket通信协议于2011年被IETF定 ...

  2. Golang 的 TOML库

    TOML 的全称是 Tom's Obvious, Minimal Language,因为它的作者是 GitHub 联合创始人 Tom Preston-Werner. TOML 的目标是成为一个极简的配 ...

  3. golang实现WebSocket的商业化使用的开发逻辑(1)

    WebSocket是什么 WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议.其最大特点之一就是:服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对 ...

  4. Golang一日一库之 日志库 zap

    简介 在开发过程中 会使用到日志库去记录错误的日志,尤其是golang中 有无穷无尽的error 如果不记录,当你的代码出错,就无从排错了. zap 是开源的 Go 高性能日志库 主要有以下特点: 支 ...

  5. 猿题库 iOS 客户端架构设计

    原文: http://mp.weixin.qq.com/s?__biz=MjM5NTIyNTUyMQ==&mid=444322139&idx=1&sn=c7bef4d439f4 ...

  6. golang之websocket 源码分析

    下载go的websocket包. 1. 通过google官方的方法, 需要hg来同步代码. 由于墙的原因, 还需要设置代理. 比较麻烦 2. http://gopm.io/ 通过该网站下载, 这是go ...

  7. 一个很cool的C#的高性能数学库

    High Performance Math Library for C# and .NET是一个很cool的C#的高性能数学库,3D效果也很不错,下图是首页上的一个例子.他也有一个交互的网页,你可以自 ...

  8. Golang实现requests库

    Golang实现requests库 简单的封装下,方便使用,像python的requests库一样. Github地址 Github 支持 GET.POST.PUT.DELETE applicatio ...

  9. Golang笔记(二)面向对象的设计

    Golang笔记(二)面向对象的设计 Golang本质还是面向过程的语言,但它实现了一些OOP的特性,包括抽象.封装.继承和多态. 抽象和封装 Golang和C语言一样以struct为数据结构核心,不 ...

  10. YxdJSON - Delphi 高性能 JSON 库(支持RTTI和序列化操作)

    源:YxdJSON - Delphi 高性能 JSON 库(支持RTTI和序列化操作) Delphi 高性能 JSON 库(支持RTTI和序列化操作) 支持平台: Windows, Android, ...

随机推荐

  1. Android OpenMAX(四)OMX Core

    假设我们已经写好了所有的OMX组件,有vdec.venc.adec.aenc,接下来问题来了,我们应该如何管理这些组件呢(创建.销毁)?这一篇文章我们向上一层学习OMX Core提供的标准API. O ...

  2. Android 13 - Media框架 - 异步消息机制

    关注公众号免费阅读全文,进入音视频开发技术分享群! b7693967-317e-4c46-96d3-d40d9d87e382 由于网上已经有许多优秀的博文讲解了Android的异步消息机制(ALoop ...

  3. 异构数据源同步之数据同步 → DataX 使用细节

    开心一刻 中午我妈微信给我消息 妈:儿子啊,妈电话欠费了,能帮妈充个话费吗 我:妈,我知道了,我帮你充 当我帮我妈把话费充好,正准备回微信的时候,我妈微信给我发消息了 妈:等会儿子,不用充了,刚刚有个 ...

  4. C# 指针简单使用

    1. 使用unsafe C# 支持 unsafe 上下文,你可在其中编写不可验证的代码. 在 unsafe 上下文中,代码可使用指针.分配和释放内存块,以及使用函数指针调用方法. C# 中的不安全代码 ...

  5. Cage 字符串听课笔记

    困困困! KMP 注意到 KMP 的复杂度是均摊的,那么是否可以绕开? 注意到 KMP 实际上一个串的 ACAM,那么考虑可以类似的,在加入一个字符的同时维护 ACAM(考虑 ACAM 的构建过程,前 ...

  6. LNMP集群架构

    网站集群拆分 上一节我们是部署了单机的LNMP,再往下,要进行拆分了,无论是性能.还是安全性,都务必要拆分. 拆分的内容有 nginx集群 mysql nfs共享存储 等 拆分思路 情况1 当前的单机 ...

  7. Scrapy框架(五)--请求传参

    在某些情况下,我们爬取的数据不在同一个页面中,例如,我们爬取一个电影网站,电影的名称,评分在一级页面,而要爬取的其他电影详情在其二级子页面中. 这时我们就需要用到请求传参. 请求传参的使用场景 当我们 ...

  8. vitepress 如何更换 favicon.ico

    favicon.ico 它出现在浏览器标签页上,是网站的标识之一. 准备图标 首先,你需要准备一个符合您要求的图标.通常,favicon.ico 使用的是 .ico 格式的图标文件,大小为 16x16 ...

  9. __proto__和[[Prototype]]的区别

    __proto__和[[Prototype]]的区别 先看下面这一段代码: const obj1 = Object.create(null); // very plain object obj1.__ ...

  10. Mysql RC/RR隔离原理和区别 不可重复读和可重复读

    Mysql RC/RR隔离原理和区别 不可重复读和可重复读 mysql四种隔离级别:1.未提交读(READ UNCOMMITED)脏读2.已提交读 (READ COMMITED)简称(RC) 不可重复 ...