我是 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
压测工具: wsbenchgorilla
,nhooyr
未使用流式API
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。