simple, fast, reliable websocket server & client, supports running over tcp/kcp/unix domain socket. keywords: ws, proxy, chat, go, golang...
See the code
GWS (Go WebSocket) is a simple, high‑performance and feature‑complete WebSocket library written in Go. It is designed for high‑concurrency scenarios and is ideal for building API gateways, long‑lived connection hubs, reverse proxies, IM / chat, online games, real‑time streaming, and push / subscribe systems. GWS exposes an extremely minimal, event‑driven API, so you can build a stable WebSocket server or client with very little code.
GWS is built on an event‑driven model: every connection has its own goroutine to drive the event loop, and events can be processed in a non‑blocking way.
Simplicity & Developer Experience
Event interface (OnOpen / OnMessage / OnClose / OnPing / OnPong) mirrors how you think about WebSocket lifecycles.High Performance
Reliability & Standards Compliance
Autobahn test cases and is compliant with RFC 6455 / RFC 7692. Unit tests cover almost all conditional branches.GOMAXPROCS=4, Connection=1000, CompressEnabled=false

# go test -benchmem -run=^$ -bench ^BenchmarkConn_ github.com/lxzan/gws
goos: linux
goarch: amd64
pkg: github.com/lxzan/gws
cpu: 13th Gen Intel(R) Core(TM) i5-13400
BenchmarkConn_WriteMessage/compress_disabled-16 9861134 112.3 ns/op 0 B/op 0 allocs/op
BenchmarkConn_WriteMessage/compress_enabled-16 163303 6729 ns/op 208 B/op 0 allocs/op
BenchmarkConn_ReadMessage/compress_disabled-16 9580408 123.4 ns/op 8 B/op 0 allocs/op
BenchmarkConn_ReadMessage/compress_enabled-16 572290 2097 ns/op 38 B/op 0 allocs/op
PASS
ok github.com/lxzan/gws 5.889s
Event interface, similar to common WebSocket SDKs.Broadcaster, which reuses compressed frames for efficient fan‑out.Dialer (e.g. SOCKS5 / HTTP proxy).WriteFile to reduce peak memory during large transfers.Writev / WritevAsync.gws.Conn can be treated as informational: the library has already taken appropriate action internally (e.g. closing the connection, emitting events).net/http (e.g. call Upgrade inside an HTTP handler), always call ReadLoop inside a separate goroutine, otherwise blocking will prevent the request context from being garbage‑collected in time.go get -v github.com/lxzan/gws@latest
type Event interface {
OnOpen(socket *Conn) // connection is established
OnClose(socket *Conn, err error) // received a close frame or input/output error occurs
OnPing(socket *Conn, payload []byte) // received a ping frame
OnPong(socket *Conn, payload []byte) // received a pong frame
OnMessage(socket *Conn, message *Message) // received a text/binary frame
}
Conn provides three read models. ReadLoop is event-driven and fits most server-side use-cases; ReadMessage manually pulls a complete message; NextReader streams message payloads and helps avoid holding a whole message in memory.
| API | OnOpen | OnMessage | OnPing / OnPong | OnClose | Use case |
|---|---|---|---|---|---|
ReadLoop | ✅ | ✅ | ✅ | ✅ | Regular event-driven server / client |
ReadMessage | ❌ | ❌ | ✅ | ✅ | Manually read one complete message |
NextReader | ❌ | ❌ | ✅ | ✅ | Stream message payloads and reduce whole-message copies |
Notes:
ReadLoop, ReadMessage, or NextReader.ReadLoop inside an upgraded net/http request, run it in a new goroutine so the request context can be garbage-collected.ReadMessage returns a Message; call Close after use to recycle its buffer.NextReader returns a streaming io.Reader. It does not trigger OnMessage and does not validate UTF-8 text payloads. If the previous message was not fully consumed, the next NextReader call discards the remaining bytes first.for {
message, err := socket.ReadMessage()
if err != nil {
return
}
func() {
defer message.Close()
_ = socket.WriteMessage(message.Opcode, message.Bytes())
}()
}
for {
messageType, r, err := socket.NextReader()
if err != nil {
return
}
data, err := io.ReadAll(r)
if err != nil {
return
}
_ = socket.WriteMessage(messageType, data)
}
package main
import "github.com/lxzan/gws"
func main() {
gws.NewServer(&gws.BuiltinEventHandler{}, nil).Run(":6666")
}
package main
import (
"github.com/lxzan/gws"
"net/http"
"time"
)
const (
PingInterval = 5 * time.Second
PingWait = 10 * time.Second
)
func main() {
upgrader := gws.NewUpgrader(&Handler{}, &gws.ServerOption{
ParallelEnabled: true, // Parallel message processing
Recovery: gws.Recovery, // Exception recovery
PermessageDeflate: gws.PermessageDeflate{Enabled: true}, // Enable compression
})
http.HandleFunc("/connect", func(writer http.ResponseWriter, request *http.Request) {
socket, err := upgrader.Upgrade(writer, request)
if err != nil {
return
}
go func() {
socket.ReadLoop() // Blocking prevents the context from being GC.
}()
})
http.ListenAndServe(":6666", nil)
}
type Handler struct{}
func (c *Handler) OnOpen(socket *gws.Conn) {
_ = socket.SetDeadline(time.Now().Add(PingInterval + PingWait))
}
func (c *Handler) OnClose(socket *gws.Conn, err error) {}
func (c *Handler) OnPing(socket *gws.Conn, payload []byte) {
_ = socket.SetDeadline(time.Now().Add(PingInterval + PingWait))
_ = socket.WritePong(nil)
}
func (c *Handler) OnPong(socket *gws.Conn, payload []byte) {}
func (c *Handler) OnMessage(socket *gws.Conn, message *gws.Message) {
defer message.Close()
socket.WriteMessage(message.Opcode, message.Bytes())
}
package main
import (
"log"
"github.com/lxzan/gws"
kcp "github.com/xtaci/kcp-go"
)
func main() {
listener, err := kcp.Listen(":6666")
if err != nil {
log.Println(err.Error())
return
}
app := gws.NewServer(&gws.BuiltinEventHandler{}, nil)
app.RunListener(listener)
}
package main
import (
"github.com/lxzan/gws"
kcp "github.com/xtaci/kcp-go"
"log"
)
func main() {
conn, err := kcp.Dial("127.0.0.1:6666")
if err != nil {
log.Println(err.Error())
return
}
app, _, err := gws.NewClientFromConn(&gws.BuiltinEventHandler{}, nil, conn)
if err != nil {
log.Println(err.Error())
return
}
app.ReadLoop()
}
Dial via proxy, using socks5 protocol.
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)
},
PermessageDeflate: gws.PermessageDeflate{
Enabled: true,
ServerContextTakeover: true,
ClientContextTakeover: true,
},
})
if err != nil {
log.Println(err.Error())
return
}
socket.ReadLoop()
}
Create a Broadcaster instance, call the Broadcast method in a loop to send messages to each client, and close the broadcaster to reclaim memory. The message is compressed only once.
func Broadcast(conns []*gws.Conn, opcode gws.Opcode, payload []byte) {
var b = gws.NewBroadcaster(opcode, payload)
defer b.Close()
for _, item := range conns {
_ = b.Broadcast(item, nil)
}
}
SetDeadline covers most of the scenarios, but if you want to control the timeout for each write, you need to
encapsulate the WriteWithTimeout function, the creation and destruction of the timer will incur some overhead.
func WriteWithTimeout(socket *gws.Conn, p []byte, timeout time.Duration) error {
var sig = atomic.Uint32{}
var timer = time.AfterFunc(timeout, func() {
if sig.CompareAndSwap(0, 1) {
socket.WriteClose(1000, []byte("write timeout"))
}
})
var err = socket.WriteMessage(gws.OpcodeText, p)
if sig.CompareAndSwap(0, 1) {
timer.Stop()
}
return err
}
Use the event_emitter package to implement the publish-subscribe model. Wrap gws.Conn in a structure and implement the
GetSubscriberID method to get the subscription ID, which must be unique. The subscription ID is used to identify the
subscriber, who can only receive messages on the subject of his subscription.
This example is useful for building chat rooms or push messages using gws. This means that a user can subscribe to one or more topics via websocket, and when a message is posted to that topic, all subscribers will receive the message.
package main
import (
"github.com/lxzan/event_emitter"
"github.com/lxzan/gws"
)
type Subscriber gws.Conn
func NewSubscriber(conn *gws.Conn) *Subscriber { return (*Subscriber)(conn) }
func (c *Subscriber) GetSubscriberID() int64 {
userId, _ := c.GetMetadata().Load("userId")
return userId.(int64)
}
func (c *Subscriber) GetMetadata() event_emitter.Metadata { return c.Conn().Session() }
func (c *Subscriber) Conn() *gws.Conn { return (*gws.Conn)(c) }
func Subscribe(em *event_emitter.EventEmitter[int64, *Subscriber], s *Subscriber, topic string) {
em.Subscribe(s, topic, func(msg any) {
_ = msg.(*gws.Broadcaster).Broadcast(s.Conn(), nil)
})
}
func Publish(em *event_emitter.EventEmitter[int64, *Subscriber], topic string, msg []byte) {
var broadcaster = gws.NewBroadcaster(gws.OpcodeText, msg)
defer broadcaster.Close()
em.Publish(topic, broadcaster)
}
cd examples/autobahn
mkdir reports
docker run -it --rm \
-v ${PWD}/config:/config \
-v ${PWD}/reports:/reports \
crossbario/autobahn-testsuite \
wstest -m fuzzingclient -s /config/fuzzingclient.json
微信需要先添加好友再拉群, 请注明来自 GitHub
The following project had particular influence on gws's design.
Overall
Maintained
Code review
Go module
Latest release
Releases this year
325 followers · starred Apr 2026
514 followers · starred Oct 2025
125 followers · starred May 2023
553 followers · starred Nov 2025
simple, fast, reliable websocket server & client, supports running over tcp/kcp/unix domain socket. keywords: ws, proxy, chat, go, golang...
See the code
GWS (Go WebSocket) is a simple, high‑performance and feature‑complete WebSocket library written in Go. It is designed for high‑concurrency scenarios and is ideal for building API gateways, long‑lived connection hubs, reverse proxies, IM / chat, online games, real‑time streaming, and push / subscribe systems. GWS exposes an extremely minimal, event‑driven API, so you can build a stable WebSocket server or client with very little code.
GWS is built on an event‑driven model: every connection has its own goroutine to drive the event loop, and events can be processed in a non‑blocking way.
Simplicity & Developer Experience
Event interface (OnOpen / OnMessage / OnClose / OnPing / OnPong) mirrors how you think about WebSocket lifecycles.High Performance
Reliability & Standards Compliance
Autobahn test cases and is compliant with RFC 6455 / RFC 7692. Unit tests cover almost all conditional branches.GOMAXPROCS=4, Connection=1000, CompressEnabled=false

# go test -benchmem -run=^$ -bench ^BenchmarkConn_ github.com/lxzan/gws
goos: linux
goarch: amd64
pkg: github.com/lxzan/gws
cpu: 13th Gen Intel(R) Core(TM) i5-13400
BenchmarkConn_WriteMessage/compress_disabled-16 9861134 112.3 ns/op 0 B/op 0 allocs/op
BenchmarkConn_WriteMessage/compress_enabled-16 163303 6729 ns/op 208 B/op 0 allocs/op
BenchmarkConn_ReadMessage/compress_disabled-16 9580408 123.4 ns/op 8 B/op 0 allocs/op
BenchmarkConn_ReadMessage/compress_enabled-16 572290 2097 ns/op 38 B/op 0 allocs/op
PASS
ok github.com/lxzan/gws 5.889s
Event interface, similar to common WebSocket SDKs.Broadcaster, which reuses compressed frames for efficient fan‑out.Dialer (e.g. SOCKS5 / HTTP proxy).WriteFile to reduce peak memory during large transfers.Writev / WritevAsync.gws.Conn can be treated as informational: the library has already taken appropriate action internally (e.g. closing the connection, emitting events).net/http (e.g. call Upgrade inside an HTTP handler), always call ReadLoop inside a separate goroutine, otherwise blocking will prevent the request context from being garbage‑collected in time.go get -v github.com/lxzan/gws@latest
type Event interface {
OnOpen(socket *Conn) // connection is established
OnClose(socket *Conn, err error) // received a close frame or input/output error occurs
OnPing(socket *Conn, payload []byte) // received a ping frame
OnPong(socket *Conn, payload []byte) // received a pong frame
OnMessage(socket *Conn, message *Message) // received a text/binary frame
}
Conn provides three read models. ReadLoop is event-driven and fits most server-side use-cases; ReadMessage manually pulls a complete message; NextReader streams message payloads and helps avoid holding a whole message in memory.
| API | OnOpen | OnMessage | OnPing / OnPong | OnClose | Use case |
|---|---|---|---|---|---|
ReadLoop | ✅ | ✅ | ✅ | ✅ | Regular event-driven server / client |
ReadMessage | ❌ | ❌ | ✅ | ✅ | Manually read one complete message |
NextReader | ❌ | ❌ | ✅ | ✅ | Stream message payloads and reduce whole-message copies |
Notes:
ReadLoop, ReadMessage, or NextReader.ReadLoop inside an upgraded net/http request, run it in a new goroutine so the request context can be garbage-collected.ReadMessage returns a Message; call Close after use to recycle its buffer.NextReader returns a streaming io.Reader. It does not trigger OnMessage and does not validate UTF-8 text payloads. If the previous message was not fully consumed, the next NextReader call discards the remaining bytes first.for {
message, err := socket.ReadMessage()
if err != nil {
return
}
func() {
defer message.Close()
_ = socket.WriteMessage(message.Opcode, message.Bytes())
}()
}
for {
messageType, r, err := socket.NextReader()
if err != nil {
return
}
data, err := io.ReadAll(r)
if err != nil {
return
}
_ = socket.WriteMessage(messageType, data)
}
package main
import "github.com/lxzan/gws"
func main() {
gws.NewServer(&gws.BuiltinEventHandler{}, nil).Run(":6666")
}
package main
import (
"github.com/lxzan/gws"
"net/http"
"time"
)
const (
PingInterval = 5 * time.Second
PingWait = 10 * time.Second
)
func main() {
upgrader := gws.NewUpgrader(&Handler{}, &gws.ServerOption{
ParallelEnabled: true, // Parallel message processing
Recovery: gws.Recovery, // Exception recovery
PermessageDeflate: gws.PermessageDeflate{Enabled: true}, // Enable compression
})
http.HandleFunc("/connect", func(writer http.ResponseWriter, request *http.Request) {
socket, err := upgrader.Upgrade(writer, request)
if err != nil {
return
}
go func() {
socket.ReadLoop() // Blocking prevents the context from being GC.
}()
})
http.ListenAndServe(":6666", nil)
}
type Handler struct{}
func (c *Handler) OnOpen(socket *gws.Conn) {
_ = socket.SetDeadline(time.Now().Add(PingInterval + PingWait))
}
func (c *Handler) OnClose(socket *gws.Conn, err error) {}
func (c *Handler) OnPing(socket *gws.Conn, payload []byte) {
_ = socket.SetDeadline(time.Now().Add(PingInterval + PingWait))
_ = socket.WritePong(nil)
}
func (c *Handler) OnPong(socket *gws.Conn, payload []byte) {}
func (c *Handler) OnMessage(socket *gws.Conn, message *gws.Message) {
defer message.Close()
socket.WriteMessage(message.Opcode, message.Bytes())
}
package main
import (
"log"
"github.com/lxzan/gws"
kcp "github.com/xtaci/kcp-go"
)
func main() {
listener, err := kcp.Listen(":6666")
if err != nil {
log.Println(err.Error())
return
}
app := gws.NewServer(&gws.BuiltinEventHandler{}, nil)
app.RunListener(listener)
}
package main
import (
"github.com/lxzan/gws"
kcp "github.com/xtaci/kcp-go"
"log"
)
func main() {
conn, err := kcp.Dial("127.0.0.1:6666")
if err != nil {
log.Println(err.Error())
return
}
app, _, err := gws.NewClientFromConn(&gws.BuiltinEventHandler{}, nil, conn)
if err != nil {
log.Println(err.Error())
return
}
app.ReadLoop()
}
Dial via proxy, using socks5 protocol.
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)
},
PermessageDeflate: gws.PermessageDeflate{
Enabled: true,
ServerContextTakeover: true,
ClientContextTakeover: true,
},
})
if err != nil {
log.Println(err.Error())
return
}
socket.ReadLoop()
}
Create a Broadcaster instance, call the Broadcast method in a loop to send messages to each client, and close the broadcaster to reclaim memory. The message is compressed only once.
func Broadcast(conns []*gws.Conn, opcode gws.Opcode, payload []byte) {
var b = gws.NewBroadcaster(opcode, payload)
defer b.Close()
for _, item := range conns {
_ = b.Broadcast(item, nil)
}
}
SetDeadline covers most of the scenarios, but if you want to control the timeout for each write, you need to
encapsulate the WriteWithTimeout function, the creation and destruction of the timer will incur some overhead.
func WriteWithTimeout(socket *gws.Conn, p []byte, timeout time.Duration) error {
var sig = atomic.Uint32{}
var timer = time.AfterFunc(timeout, func() {
if sig.CompareAndSwap(0, 1) {
socket.WriteClose(1000, []byte("write timeout"))
}
})
var err = socket.WriteMessage(gws.OpcodeText, p)
if sig.CompareAndSwap(0, 1) {
timer.Stop()
}
return err
}
Use the event_emitter package to implement the publish-subscribe model. Wrap gws.Conn in a structure and implement the
GetSubscriberID method to get the subscription ID, which must be unique. The subscription ID is used to identify the
subscriber, who can only receive messages on the subject of his subscription.
This example is useful for building chat rooms or push messages using gws. This means that a user can subscribe to one or more topics via websocket, and when a message is posted to that topic, all subscribers will receive the message.
package main
import (
"github.com/lxzan/event_emitter"
"github.com/lxzan/gws"
)
type Subscriber gws.Conn
func NewSubscriber(conn *gws.Conn) *Subscriber { return (*Subscriber)(conn) }
func (c *Subscriber) GetSubscriberID() int64 {
userId, _ := c.GetMetadata().Load("userId")
return userId.(int64)
}
func (c *Subscriber) GetMetadata() event_emitter.Metadata { return c.Conn().Session() }
func (c *Subscriber) Conn() *gws.Conn { return (*gws.Conn)(c) }
func Subscribe(em *event_emitter.EventEmitter[int64, *Subscriber], s *Subscriber, topic string) {
em.Subscribe(s, topic, func(msg any) {
_ = msg.(*gws.Broadcaster).Broadcast(s.Conn(), nil)
})
}
func Publish(em *event_emitter.EventEmitter[int64, *Subscriber], topic string, msg []byte) {
var broadcaster = gws.NewBroadcaster(gws.OpcodeText, msg)
defer broadcaster.Close()
em.Publish(topic, broadcaster)
}
cd examples/autobahn
mkdir reports
docker run -it --rm \
-v ${PWD}/config:/config \
-v ${PWD}/reports:/reports \
crossbario/autobahn-testsuite \
wstest -m fuzzingclient -s /config/fuzzingclient.json
微信需要先添加好友再拉群, 请注明来自 GitHub
The following project had particular influence on gws's design.
Overall
Maintained
Code review
Go module
Latest release
Releases this year
325 followers · starred Apr 2026
514 followers · starred Oct 2025
125 followers · starred May 2023
553 followers · starred Nov 2025