我是 gws 作者「闰土的猹」, 今天向大家介绍下gws的使用和设计. gws 是一个高性能低开销的 WebSocket 框架, 为您的实时通信业务提供助力.

快速开始

gorilla/websocket 在用户层代码循环读取消息不同, gws 封装了这一过程, 提供 WebSocket Event API, 内部做好了连接生命周期管理以及错误处理, 所有 gws.Conn 导出的方法里面错误都是可忽略的.

package main

import (
    "github.com/lxzan/gws"
)

func main() {
    gws.NewServer(new(Handler), nil).Run(":6666")
}

type Handler struct{}

func (c *Handler) OnOpen(socket *gws.Conn) {}

func (c *Handler) OnClose(socket *gws.Conn, err error) {}

func (c *Handler) OnPong(socket *gws.Conn, payload []byte) {}

func (c *Handler) OnPing(socket *gws.Conn, payload []byte) {}

func (c *Handler) OnMessage(socket *gws.Conn, message *gws.Message) {}

主要特性

  • 并发安全
  • 代理拨号
  • IO多路复用
  • 广播

并发安全

gws 不假设用户会使用 chan 写消息, 提供的 Write 系列方法支持并发使用.

代理

gws 里面使用代理拨号是非常简单的, 只需要加一个 NewDialer 配置覆盖默认的直接拨号, 支持 ws, wss.

package main

import (
    "crypto/tls"
    "github.com/lxzan/gws"
    "golang.org/x/net/proxy"
    "log"
)

func main() {
    socket, _, err := gws.NewClient(new(gws.BuiltinEventHandler), &gws.ClientOption{
        Addr:      "wss://example.com/connect",
        TlsConfig: &tls.Config{InsecureSkipVerify: true},
        NewDialer: func() (gws.Dialer, error) {
            return proxy.SOCKS5("tcp", "127.0.0.1:1080", nil, nil)
        },
    })
    if err != nil {
        log.Println(err.Error())
        return
    }
    socket.ReadLoop()
}

IO多路复用

考虑到IO多路复用远胜于连接池, gws 提供了任务队列, 串行读取消息, 并行处理消息.

// 是否开启异步读, 开启的话会并行调用OnMessage
// Whether to enable asynchronous reading, if enabled OnMessage will be called in parallel
ReadAsyncEnabled bool

// 异步读的最大并行协程数量
// Maximum number of parallel concurrent processes for asynchronous reads
ReadAsyncGoLimit int

gws 提供了读/写两个任务队列, 并行度分布是 8 (可修改)和 1 (不可修改). 任务队列是本仓库最精巧的一个数据结构, 它非常的轻量, 每个连接上带上俩也不会增加太多内存开销, 基于有锁队列实现, 不产生常驻的 goroutine.

type (
    workerQueue struct {
        mu             sync.Mutex // 锁
        q              []asyncJob // 任务队列
        maxConcurrency int32      // 最大并发
        curConcurrency int32      // 当前并发
    }

    asyncJob func()
)

// newWorkerQueue 创建一个任务队列
func newWorkerQueue(maxConcurrency int32) *workerQueue {
    c := &workerQueue{
        mu:             sync.Mutex{},
        maxConcurrency: maxConcurrency,
        curConcurrency: 0,
    }
    return c
}

// 获取一个任务
func (c *workerQueue) getJob(delta int32) asyncJob {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.curConcurrency += delta
    if c.curConcurrency >= c.maxConcurrency {
        return nil
    }
    if len(c.q) == 0 {
        return nil
    }
    var result = c.q[0]
    c.q = c.q[1:]
    c.curConcurrency++
    return result
}

// 循环执行任务
func (c *workerQueue) do(job asyncJob) {
    for job != nil {
        job()
        job = c.getJob(-1)
    }
}

// Push 追加任务, 有资源空闲的话会立即执行
func (c *workerQueue) Push(job asyncJob) {
    c.mu.Lock()
    c.q = append(c.q, job)
    c.mu.Unlock()
    if job := c.getJob(0); job != nil {
        go c.do(job)
    }
}

广播

内置广播方案基于任务队列实现, 非常的简单易用. 相比循环调用 WriteAsync , Broadcaster 只会压缩一次消息, 节省大量内存和CPU开销.

func Broadcast(conns []*gws.Conn, opcode gws.Opcode, payload []byte) {
    var b = gws.NewBroadcaster(opcode, payload)
    defer b.Release()
    for _, item := range conns {
        _ = b.Broadcast(item)
    }
}

可以看到, 开启压缩CPU开销是不开启的几十倍

goos: darwin
goarch: arm64
pkg: github.com/lxzan/gws
BenchmarkConn_WriteMessage/compress_disabled-8              4494459           239.2 ns/op           0 B/op           0 allocs/op
BenchmarkConn_WriteMessage/compress_enabled-8                107365         10726 ns/op         509 B/op           0 allocs/op
BenchmarkConn_ReadMessage/compress_disabled-8               3037701           395.6 ns/op         120 B/op           3 allocs/op
BenchmarkConn_ReadMessage/compress_enabled-8                 175388          6355 ns/op        7803 B/op           7 allocs/op
PASS
ok      github.com/lxzan/gws    5.813s

性能压测

  • GOMAXPROCS = 4
  • Connection = 1000
  • Compress Disabled

image.png

压测工具: wsbench
gorilla, nhooyr 未使用流式API

codebeast
51 声望0 粉丝