首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

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

编程知识
2024年07月27日 16:43

前言

大家好这里是,白泽,这期分析一下 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 := 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
}
  • 错误处理和恢复: 健壮的错误处理和异常恢复机制可以防止个别连接的问题影响整个服务的性能。

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

From:https://www.cnblogs.com/YLTFY1998/p/18327266
本文地址: http://shuzixingkong.net/article/488
0评论
提交 加载更多评论
其他文章 useRoute 函数的详细介绍与使用示例
title: useRoute 函数的详细介绍与使用示例 date: 2024/7/27 updated: 2024/7/27 author: cmdragon excerpt: 摘要:本文介绍了Nuxt.js中useRoute函数的详细用途与示例,展示了如何在组合式API中使用useRoute获取
useRoute 函数的详细介绍与使用示例 useRoute 函数的详细介绍与使用示例
【AppStore】IOS应用上架Appstore的一些小坑
前言 上一篇文章写到如何上架IOS应用到Appstore,其中漏掉了些许期间遇到的小坑,现在补上 审核不通过原因 5.1.1 Guideline 5.1.1 - Legal - Privacy - Data Collection and Storage 5.1.1(ii) Permission Ap
【AppStore】IOS应用上架Appstore的一些小坑 【AppStore】IOS应用上架Appstore的一些小坑 【AppStore】IOS应用上架Appstore的一些小坑
带你学习通过GitHub Actions如何快速构建和部署你自己的项目,打造一条属于自己的流水线
本文主要讲解通过github的actions来对我们项目进行ci/cd System.out.println(&quot;原文地址:https://www.cnblogs.com/ancold/p/18327097&quot;); 一、actions简介 GitHub Actions 是一种持续集成
带你学习通过GitHub Actions如何快速构建和部署你自己的项目,打造一条属于自己的流水线 带你学习通过GitHub Actions如何快速构建和部署你自己的项目,打造一条属于自己的流水线 带你学习通过GitHub Actions如何快速构建和部署你自己的项目,打造一条属于自己的流水线
Spring 常用的三种拦截器详解
在开发过程中,我们常常使用到拦截器来处理一些逻辑。最常用的三种拦截器分别是 AOP、 Interceptor 、 Filter,但其实很多人并不知道什么时候用AOP,什么时候用Interceptor,什么时候用Filter,也不知道其拦截顺序,内部原理。今天我们详细介绍一下这三种拦截器。
Spring 常用的三种拦截器详解 Spring 常用的三种拦截器详解
Java SE 文件上传和文件下载的底层原理
1. Java SE 文件上传和文件下载的底层原理 @目录1. Java SE 文件上传和文件下载的底层原理2. 文件上传2.1 文件上传应用实例2.2 文件上传注意事项和细节3. 文件下载3.1 文件下载应用实例3.2 文件下载注意事项和细节4. 总结:5. 最后: 2. 文件上传 文件的上传和下
Java SE 文件上传和文件下载的底层原理 Java SE 文件上传和文件下载的底层原理 Java SE 文件上传和文件下载的底层原理
Android低功耗子系统的投票机制以及触发进入系统休眠的过程
从kernel角度看,系统是否进入休眠应该由内核来控制,因此Linux引入了 wakeup source以及autosleep机制 关于wakeup source的介绍,请参考: Wakeup Source框架设计与实现 关于autosleep机制,请参考:autosleep框架设计与实现 在内核中
Android低功耗子系统的投票机制以及触发进入系统休眠的过程 Android低功耗子系统的投票机制以及触发进入系统休眠的过程 Android低功耗子系统的投票机制以及触发进入系统休眠的过程
自写ApiTools工具,功能参考Postman和ApiPost
近日在使用ApiPost的时候,发现新版本8和7不兼容,也就是说8不支持离线操作,而7可以。 我想说,我就是因为不想登录使用才从Postman换到ApiPost的。 众所周知,postman时国外软件,登录经常性抽风,离线支持也不太好。 所以使用apipost,开始用apipost7一直很好用。可是
自写ApiTools工具,功能参考Postman和ApiPost 自写ApiTools工具,功能参考Postman和ApiPost
Git的存储原理
目录Git 设计原理Git vs SVNGit 存储模型.git 目录结构Git 基本数据对象Git 包文件Git 引用 Git 设计原理 概括的讲,Git 就是一个基于快照的内容寻址文件系统。 往下慢慢看。 Git vs SVN Git 出现前,主流版本控制系统(SVN...)一般为基于增量(de
Git的存储原理 Git的存储原理 Git的存储原理