gws
event-driven go websocket server
Highlight
- No dependency
- IO multiplexing support, concurrent message processing and asynchronous non-blocking message writing
- High IOPS and low latency, low CPU usage
- Support fast parsing WebSocket protocol directly from TCP, faster handshake, 30% lower memory usage
- Fully passes the WebSocket autobahn-testsuite
Install
go get -v github.com/lxzan/gws@latest
Event
type Event interface {
OnOpen(socket *Conn)
OnError(socket *Conn, err error)
OnClose(socket *Conn, code uint16, reason []byte)
OnPing(socket *Conn, payload []byte)
OnPong(socket *Conn, payload []byte)
OnMessage(socket *Conn, message *Message)
}
Examples
Quick Start
- server
package main
import (
"github.com/lxzan/gws"
"log"
"net/http"
)
func main() {
upgrader := gws.NewUpgrader(new(Websocket), &gws.ServerOption{
ReadAsyncEnabled: true,
ReadAsyncGoLimit: 4,
CheckOrigin: func(r *http.Request, session gws.SessionStorage) bool {
session.Store("username", r.URL.Query().Get("username"))
return true
},
})
http.HandleFunc("/connect", func(writer http.ResponseWriter, request *http.Request) {
socket, err := upgrader.Accept(writer, request)
if err != nil {
log.Printf("Accept: " + err.Error())
return
}
go socket.Listen()
})
if err := http.ListenAndServe(":3000", nil); err != nil {
log.Fatalf("%+v", err)
}
}
type Websocket struct {
gws.BuiltinEventHandler
}
func (w Websocket) OnMessage(socket *gws.Conn, message *gws.Message) {
defer message.Close()
_ = socket.WriteMessage(message.Opcode, message.Bytes())
}
- client
package main
import (
"fmt"
"github.com/lxzan/gws"
"log"
)
func main() {
socket, _, err := gws.NewClient(new(WebSocket), &gws.ClientOption{
Addr: "ws://127.0.0.1:3000/connect",
})
if err != nil {
log.Printf(err.Error())
return
}
socket.Listen()
}
type WebSocket struct {
gws.BuiltinEventHandler
}
func (c *WebSocket) OnMessage(socket *gws.Conn, message *gws.Message) {
fmt.Printf("recv: %s\n", message.Data.String())
}
Advanced
- WebSocket over TCP
// compared to hijacking http, handshake is faster and more memory efficient
func main() {
srv := gws.NewServer(new(Websocket), nil)
if err := srv.Run(":3000"); err != nil {
log.Fatalln(err.Error())
}
}
- Gin
package main
import (
"github.com/gin-gonic/gin"
"github.com/lxzan/gws"
)
func main() {
app := gin.New()
upgrader := gws.NewUpgrader(new(WebSocket), nil)
app.GET("/connect", func(ctx *gin.Context) {
socket, err := upgrader.Accept(ctx.Writer, ctx.Request)
if err != nil {
return
}
go upgrader.Listen(socket)
})
if err := app.Run(":8080"); err != nil {
panic(err)
}
}
- HeartBeat
const PingInterval = 5 * time.Second
type Websocket struct {
gws.BuiltinEventHandler
}
func (w Websocket) OnOpen(socket *gws.Conn) {
_ = socket.SetDeadline(time.Now().Add(3 * PingInterval))
}
func (w Websocket) OnPing(socket *gws.Conn, payload []byte) {
_ = socket.WritePong(nil)
_ = socket.SetDeadline(time.Now().Add(3 * PingInterval))
}
- Broadcast
func Broadcast(conns []*gws.Conn, opcode gws.Opcode, payload []byte) {
for _, item := range conns {
_ = item.WriteAsync(opcode, payload)
}
}
Autobahn Test
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
Benchmark
-
Machine:
Ubuntu 20.04LTS VM (4C8T)
-
IOPS
// ${message_num} depends on the maximum load capacity of each package
tcpkali -c 1000 --connect-rate 500 -r ${message_num} -T 300s -f assets/1K.txt --ws 127.0.0.1:${port}/connect
- Latency
tcpkali -c 1000 --connect-rate 500 -r 100 -T 300s -f assets/1K.txt --ws 127.0.0.1:${port}/connect
- CPU
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
9898 caster 20 0 721172 39648 7404 S 259.5 1.0 78:44.15 gorilla-linux-a
9871 caster 20 0 721212 41788 7188 S 161.5 1.0 51:39.43 gws-linux-amd64
Acknowledgments
The following project had particular influence on gws's design.