diff --git a/README.md b/README.md index c927e8c1b6189c0e3fe13a918f1827f800d97837..477a59ff3d81292dd1ac481d2a715fcd837d1520 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,17 @@ go get nhooyr.io/websocket ## Features - Minimal and idiomatic API -- Tiny codebase at 2200 lines - First class [context.Context](https://blog.golang.org/context) support - Thorough tests, fully passes the [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) - [Zero dependencies](https://godoc.org/nhooyr.io/websocket?imports) - JSON and ProtoBuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages -- Highly optimized by default - - Zero alloc reads and writes -- Concurrent writes out of the box -- [Complete Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) support -- [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) -- Full support of [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression extension +- Zero alloc reads and writes +- Concurrent writes +- WebSocket [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) +- [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper +- WebSocket [Pings](https://godoc.org/nhooyr.io/websocket#Conn.Ping) +- [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression +- [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) ## Roadmap @@ -34,11 +34,7 @@ go get nhooyr.io/websocket ## Examples -For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [example_echo_test.go](./example_echo_test.go). - -Use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). -There is also [websocket.CloseStatus](https://godoc.org/nhooyr.io/websocket#CloseStatus) to quickly grab the close status code out of a [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). -See the [CloseStatus godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseStatus). +For a production quality example that demonstrates the full API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). ### Server @@ -87,83 +83,45 @@ c.Close(websocket.StatusNormalClosure, "") ## Comparison -Before the comparison, I want to point out that gorilla/websocket was extremely useful in implementing the -WebSocket protocol correctly so _big thanks_ to its authors. In particular, I made sure to go through the -issue tracker of gorilla/websocket to ensure I implemented details correctly and understood how people were -using WebSockets in production. - -### gorilla/websocket - -https://github.com/gorilla/websocket - -The implementation of gorilla/websocket is 6 years old. As such, it is -widely used and very mature compared to nhooyr.io/websocket. - -On the other hand, it has grown organically and now there are too many ways to do -the same thing. Compare the godoc of -[nhooyr/websocket](https://godoc.org/nhooyr.io/websocket) with -[gorilla/websocket](https://godoc.org/github.com/gorilla/websocket) side by side. - -The API for nhooyr.io/websocket has been designed such that there is only one way to do things. -This makes it easy to use correctly. Not only is the API simpler, the implementation is -only 2200 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain, -more code to test, more code to document and more surface area for bugs. - -Moreover, nhooyr.io/websocket supports newer Go idioms such as context.Context. -It also uses net/http's Client and ResponseWriter directly for WebSocket handshakes. -gorilla/websocket writes its handshakes to the underlying net.Conn. -Thus it has to reinvent hooks for TLS and proxies and prevents easy support of HTTP/2. - -Some more advantages of nhooyr.io/websocket are that it supports concurrent writes and -makes it very easy to close the connection with a status code and reason. In fact, -nhooyr.io/websocket even implements the complete WebSocket close handshake for you whereas -with gorilla/websocket you have to perform it manually. See [gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448). - -The ping API is also nicer. gorilla/websocket requires registering a pong handler on the Conn -which results in awkward control flow. With nhooyr.io/websocket you use the Ping method on the Conn -that sends a ping and also waits for the pong. - -Additionally, nhooyr.io/websocket can compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) for the browser. - -In terms of performance, the differences mostly depend on your application code. nhooyr.io/websocket -reuses message buffers out of the box if you use the wsjson and wspb subpackages. -As mentioned above, nhooyr.io/websocket also supports concurrent writers. - -The WebSocket masking algorithm used by this package is [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) -faster than gorilla/websocket while using only pure safe Go. - -The [permessage-deflate compression extension](https://tools.ietf.org/html/rfc7692) is fully supported by this library -whereas gorilla only supports no context takeover mode. See our godoc for the differences. This will make a big -difference on bandwidth used in most use cases. - -The only performance con to nhooyr.io/websocket is that it uses a goroutine to support -cancellation with context.Context. This costs 2 KB of memory which is cheap compared to -the benefits. - -### x/net/websocket - -https://godoc.org/golang.org/x/net/websocket - -Unmaintained and the API does not reflect WebSocket semantics. Should never be used. - -See https://github.com/golang/go/issues/18152 - -### gobwas/ws - -https://github.com/gobwas/ws - -This library has an extremely flexible API but that comes at the cost of usability -and clarity. - -Due to its flexibility, it can be used in a event driven style for performance. -Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) about performant WebSocket servers. - -If you want a library that gives you absolute control over everything, this is the library. -But for 99.9% of use cases, nhooyr.io/websocket will fit better as it is both easier and -faster for normal idiomatic Go. The masking implementation is [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) -faster, the compression extensions are fully supported and as much as possible is reused by default. - -See the gorilla/websocket comparison for more performance details. +### [gorilla/websocket](https://github.com/gorilla/websocket) + +Advantages of nhooyr.io/websocket: + - Minimal and idiomatic API + - Compare godoc of [nhooyr.io/websocket](https://godoc.org/nhooyr.io/websocket) with [gorilla/websocket](https://godoc.org/github.com/gorilla/websocket) side by side. + - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper + - Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) + - Full [context.Context](https://blog.golang.org/context) support + - Uses [net/http.Client](https://golang.org/pkg/net/http/#Client) for dialing + - Will enable easy HTTP/2 support in the future + - Gorilla writes directly to a net.Conn and so duplicates features from net/http.Client. + - Concurrent writes + - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) + - Idiomatic [ping](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API + - gorilla/websocket requires registering a pong callback and then sending a Ping + - Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) + - Transparent buffer reuse with [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages + - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go + - Gorilla's implementation depends on unsafe and is slower + - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support + - Gorilla only supports no context takeover mode + - [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper + - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) + +Advantages of gorilla/websocket: + - Widely used and mature + +### [x/net/websocket](https://godoc.org/golang.org/x/net/websocket) + +Deprecated. See ([golang/go/issues/18152](https://github.com/golang/go/issues/18152)). + +The [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper will ease in transitioning to nhooyr.io/websocket. + +### [gobwas/ws](https://github.com/gobwas/ws) + +This library has an extremely flexible API that allows it to be used in an unidiomatic event driven style +for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). + +When writing idiomatic Go, nhooyr.io/websocket is a better choice as it will be faster and easier to use. ## Users diff --git a/close.go b/close.go index baa1a7e07f0dc11d327c6f1ab2a12d6d761f4215..a02dc7d9e108cce3242d0814add8a9a628b519a1 100644 --- a/close.go +++ b/close.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log" + "nhooyr.io/websocket/internal/errd" "time" "nhooyr.io/websocket/internal/bpool" @@ -96,15 +97,13 @@ func CloseStatus(err error) StatusCode { // Close will unblock all goroutines interacting with the connection once // complete. func (c *Conn) Close(code StatusCode, reason string) error { - err := c.closeHandshake(code, reason) - if err != nil { - return fmt.Errorf("failed to close WebSocket: %w", err) - } - return nil + return c.closeHandshake(code, reason) } -func (c *Conn) closeHandshake(code StatusCode, reason string) error { - err := c.cw.sendClose(code, reason) +func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { + defer errd.Wrap(&err, "failed to close WebSocket") + + err = c.cw.sendClose(code, reason) if err != nil { return err } @@ -115,7 +114,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) error { func (cw *connWriter) error(code StatusCode, err error) { cw.c.setCloseErr(err) cw.sendClose(code, err.Error()) - cw.c.close(nil) + cw.c.closeWithErr(nil) } func (cw *connWriter) sendClose(code StatusCode, reason string) error { @@ -135,7 +134,7 @@ func (cw *connWriter) sendClose(code StatusCode, reason string) error { } func (cr *connReader) waitClose() error { - defer cr.c.close(nil) + defer cr.c.closeWithErr(nil) return nil diff --git a/conn.go b/conn.go index 5c041b8dc28324ab30e2449165a20cfc04ddc8ce..d9001791103690fed67f2ad9f702f73395bed449 100644 --- a/conn.go +++ b/conn.go @@ -33,11 +33,10 @@ const ( // frames will not be handled. See the docs on Reader and CloseRead. // // Be sure to call Close on the connection when you -// are finished with it to release the associated resources. +// are finished with it to release associated resources. // -// Every error from Read or Reader will cause the connection -// to be closed so you do not need to write your own error message. -// This applies to the Read methods in the wsjson/wspb subpackages as well. +// On any error from any method, the connection is closed +// with an appropriate reason. type Conn struct { subprotocol string rwc io.ReadWriteCloser @@ -69,11 +68,12 @@ type connConfig struct { } func newConn(cfg connConfig) *Conn { - c := &Conn{} - c.subprotocol = cfg.subprotocol - c.rwc = cfg.rwc - c.client = cfg.client - c.copts = cfg.copts + c := &Conn{ + subprotocol: cfg.subprotocol, + rwc: cfg.rwc, + client: cfg.client, + copts: cfg.copts, + } c.cr.init(c, cfg.br) c.cw.init(c, cfg.bw) @@ -82,7 +82,7 @@ func newConn(cfg connConfig) *Conn { c.activePings = make(map[string]chan<- struct{}) runtime.SetFinalizer(c, func(c *Conn) { - c.close(errors.New("connection garbage collected")) + c.closeWithErr(errors.New("connection garbage collected")) }) go c.timeoutLoop() @@ -96,7 +96,7 @@ func (c *Conn) Subprotocol() string { return c.subprotocol } -func (c *Conn) close(err error) { +func (c *Conn) closeWithErr(err error) { c.closeMu.Lock() defer c.closeMu.Unlock() @@ -135,7 +135,7 @@ func (c *Conn) timeoutLoop() { c.cw.error(StatusPolicyViolation, errors.New("timed out")) return case <-writeCtx.Done(): - c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) + c.closeWithErr(fmt.Errorf("write timed out: %w", writeCtx.Err())) return } } @@ -185,7 +185,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { return c.closeErr case <-ctx.Done(): err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) - c.close(err) + c.closeWithErr(err) return err case <-pong: return nil diff --git a/read.go b/read.go index 13c8d703b4b2fbd2ba6ac6c4e74e5ee2f04779ce..7dba832a8c511109393188dc53c22652e63223d9 100644 --- a/read.go +++ b/read.go @@ -199,7 +199,7 @@ func (cr *connReader) frameHeader(ctx context.Context) (header, error) { case <-ctx.Done(): return header{}, ctx.Err() default: - cr.c.close(err) + cr.c.closeWithErr(err) return header{}, err } } @@ -229,7 +229,7 @@ func (cr *connReader) framePayload(ctx context.Context, p []byte) (int, error) { return n, ctx.Err() default: err = fmt.Errorf("failed to read frame payload: %w", err) - cr.c.close(err) + cr.c.closeWithErr(err) return n, err } }