RFC6455 WebSocket implementation in Go.
- Zero-copy upgrade
- No intermediate allocations during I/O
- Low-level API which allows to build your own packet handling and buffers reuse logic
- High-level wrappers and helpers around API in
wsutil
pacakge, which allows to start fast without digging the protocol internals
Existing WebSocket implementations does not allow to reuse I/O buffers between connections in clear way. This library aims to export lower-level interface for working with the protocol without forcing only one way it could be used. Also one more aim is performance.
By the way, if you want get the higher-level interface, you could use wsutil
sub-package.
This implementation of RFC6455 passes Autobahn Test Suite and currently has 71.6% coverage.
The library is not tagged as v1.0.0
yet so it could be broken during some improvements
or refactoring.
The higher-level example of WebSocket echo server:
import (
"net/http"
"github.com/gobwas/ws"
"github.com/gobwas/wsutil"
)
func main() {
http.HandleFunc("/websocket", func(w http.ResponseWriter, r *http.Request) {
conn, _, _, err := ws.UpgradeHTTP(r, w, nil)
if err != nil {
// handle error
}
go func() {
defer conn.Close()
for {
msg, op, err := wsutil.ReadClientData(conn)
if err != nil {
// handle error
}
err = wsutil.WriteServerMessage(conn, op, msg)
if err != nil {
// handle error
}
}
}()
})
}
Lower-level, but still high-level example:
import (
"net/http"
"github.com/gobwas/ws"
"github.com/gobwas/wsutil"
)
func main() {
http.HandleFunc("/websocket", func(w http.ResponseWriter, r *http.Request) {
conn, _, _, err := ws.UpgradeHTTP(r, w, nil)
if err != nil {
// handle error
}
go func() {
defer conn.Close()
var (
state = ws.StateServerSide
reader = wsutil.NewReader(conn, state)
writer = wsutil.NewWriter(conn, state, ws.OpText)
)
for {
header, err := reader.NextFrame()
if err != nil {
// handle error
}
// Reset writer to write frame with right operation code.
writer.Reset(conn, state, header.OpCode)
if _, err = io.Copy(writer, reader), err != nil {
// handle error
}
if err = writer.Flush(); err != nil {
// handle error
}
}
}()
})
}
The lower-level example without wsutil
:
import (
"net/http"
"github.com/gobwas/ws"
)
func main() {
ln, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
for {
conn, err := ln.Accept()
if err != nil {
// handle error
}
_, err := ws.Upgrade(conn)
if err != nil {
// handle error
}
go func() {
defer conn.Close()
for {
header, err := ws.ReadHeader(conn)
if err != nil {
// handle error
}
payload, err := ioutil.ReadAll(conn, header.Length)
if err != nil {
// handle error
}
if header.Masked {
ws.Cipher(payload, header.Mask, 0)
}
// Reset the Masked flag, server frames must not be masked as
// RFC6455 says.
header.Masked = false
if err := ws.WriteHeader(conn, header); err != nil {
// handle error
}
if err := conn.Write(payload); err != nil {
// handle error
}
if header.OpCode == ws.OpClose {
return
}
}
}()
}
}
Zero copy upgrade helps to avoid unnecessary allocations and copies while handling HTTP Upgrade request.
Processing of all non-websocket headers is made in place with use of registered user callbacks, when arguments are only valid until callback returns.
The simple example looks like this:
import (
"net"
"net/http"
"log"
"github.com/gobwas/ws"
"github.com/gobwas/httphead"
)
func main() {
ln, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}
u := ws.Upgrader{
OnHeader: func(key, value []byte) (err error, code int) {
log.Printf("non-websocket header: %q=%q", key, value)
return
},
}
for {
conn, err := ln.Accept()
if err != nil {
// handle error
}
_, err := u.Upgrade(conn)
if err != nil {
// handle error
}
}
}
Use of ws.Upgrader
here brings ability to control incoming connections on tcp
level, and simply do not accept them by your custom logic.
Zero-copy upgrade are intended for high-load services with need to control many resources such as alive connections and their buffers.
The real life example could be like this:
import (
"net"
"net/http"
"log"
"github.com/gobwas/ws"
"github.com/gobwas/httphead"
)
func main() {
ln, err := net.Listen("tcp", "localhost:8080")
if err != nil {
// handle error
}
header := http.Header{
"X-Go-Version": []string{runtime.Version()},
}
u := ws.Upgrader{
OnRequest: func(host, uri []byte) (err error, code int) {
if string(host) == "github.com" {
return fmt.Errorf("unexpected host: %s", host), 403
}
return
},
OnHeader: func(key, value []byte) (err error, code int) {
if string(key) != "Cookie" {
return
}
ok := httphead.ScanCookie(value, func(key, value []byte) bool {
// Check session here or do some other stuff with cookies.
// Maybe copy some values for future use.
})
if !ok {
return fmt.Errorf("bad cookie"), 400
}
return
},
BeforeUpgrade: func() (headerWriter func(io.Writer), err error, code int) {
return ws.HeaderWriter(header)
},
}
for {
conn, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
_, err := u.Upgrade(conn)
if err != nil {
log.Printf("upgrade error: %s", err)
}
}
}