我是 gws 作者「闰土的猹」, 今天向大家介绍下 gws 的使用和设计. gws 是一个高性能低开销的 WebSocket 框架, 提供IO多路复用, 广播, 代理等诸多特性, 为您的实时通信业务提供强大助力.
快速开始
和 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多路复用
- 广播
并发安全
我们不假设用户会使用 chan 写消息, 提供的 Write 系列方法支持并发调用.
代理
使用代理拨号是非常简单的, 只需要加一个 NewDialer 配置覆盖默认的直接拨号, 支持 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
内置读/写两个任务队列, 并行度分布是 8 (可修改)和 1 (不可修改). 无线缓存的任务队列是本仓库最巧妙的一个数据结构, 它基于有锁队列实现, 不会像 chan 一样产生常驻的 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)
}
}
广播
广播方案也是基于任务队列实现, 比 chan 方案要简单易用许多. 相比常规广播方案 , 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 7280246 137.7 ns/op 0 B/op 0 allocs/op
BenchmarkConn_WriteMessage/compress_enabled-8 144850 8012 ns/op 234 B/op 0 allocs/op
BenchmarkConn_ReadMessage/compress_disabled-8 5141690 236.2 ns/op 120 B/op 3 allocs/op
BenchmarkConn_ReadMessage/compress_enabled-8 220413 5009 ns/op 290 B/op 4 allocs/op
PASS
ok github.com/lxzan/gws 9.695s
性能压测
性能方面, 在众多基于标准网络库的 WebSocket 实现里面, gws 属于第一梯队, 做一个简单的小测试, 结果如下:
- GOMAXPROCS = 4
- Connection = 1000
- Compress Disabled
压测工具: wsbench
gorilla, nhooyr未使用流式API