diff --git a/README.md b/README.md index 53507a60580e4048433539280addbc2e6285a1fb..5e7c150d5331ee9047eaf0a7e741583daca45cb8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ If you have any feedback, please feel free to open an issue. ## Install ```bash -go get nhooyr.io/websocket +go get nhooyr.io/websocket@0.2.0 ``` ## Features @@ -85,9 +85,8 @@ c.Close(websocket.StatusNormalClosure, "") - Minimal API is easier to maintain and learn - Context based cancellation is more ergonomic and robust than setting deadlines - No ping support because TCP keep alives work fine for HTTP/1.1 and they do not make - sense with HTTP/2 (see #1) -- net.Conn is never exposed as WebSocket's over HTTP/2 will not have a net.Conn. -- Structures are nicer than functional options, see [google/go-cloud#908](https://github.com/google/go-cloud/issues/908#issuecomment-445034143) + sense with HTTP/2 (see [#1](https://github.com/nhooyr/websocket/issues/1)) +- net.Conn is never exposed as WebSocket over HTTP/2 will not have a net.Conn. - Using net/http's Client for dialing means we do not have to reinvent dialing hooks and configurations like other WebSocket libraries @@ -105,7 +104,7 @@ in production. https://github.com/gorilla/websocket This package is the community standard but it is 6 years old and over time -has accumulated cruft. There are many ways to do the same thing, usage is not clear +has accumulated cruft. Using is not clear as there are many ways to do things and there are some rough edges. Just compare the godoc of [nhooyr/websocket](https://godoc.org/github.com/nhooyr/websocket) side by side with [gorilla/websocket](https://godoc.org/github.com/gorilla/websocket). @@ -115,11 +114,10 @@ which makes it easy to use correctly. Furthermore, nhooyr/websocket has support for newer Go idioms such as context.Context and also uses net/http's Client and ResponseWriter directly for WebSocket handshakes. -gorilla/websocket writes its handshakes directly to a net.Conn which means +gorilla/websocket writes its handshakes to the underlying net.Conn which means it has to reinvent hooks for TLS and proxying and prevents support of HTTP/2. -Another advantage of nhooyr/websocket is that it supports multiple concurrent writers out -of the box. +Another advantage of nhooyr/websocket is that it supports concurrent writers out of the box. ### x/net/websocket @@ -138,8 +136,9 @@ and clarity. This library is fantastic in terms of performance. The author put in significant effort to ensure its speed and I have applied as many of its optimizations as -I could into nhooyr/websocket. Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) about performant WebSocket servers. +I could into nhooyr/websocket. 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 most users, the API provided by nhooyr/websocket will fit better as it is just as -performant but much easier to use correctly and idiomatic. +but for most users, the API provided by nhooyr/websocket will fit better as it is nearly just +as performant but much easier to use correctly and idiomatic. diff --git a/doc.go b/doc.go index 0b873b48478be8dcbd3d33c406811843428db0ad..6ee4166aba6cb4b801f490f77f9ee7fd12734b8b 100644 --- a/doc.go +++ b/doc.go @@ -2,9 +2,6 @@ // // See https://tools.ietf.org/html/rfc6455 // -// Please see https://nhooyr.io/websocket for overview docs and a -// comparison with existing implementations. -// // Conn, Dial, and Accept are the main entrypoints into this package. Use Dial to dial // a WebSocket server, Accept to accept a WebSocket client dial and then Conn to interact // with the resulting WebSocket connections. @@ -12,4 +9,9 @@ // The examples are the best way to understand how to correctly use the library. // // The wsjson and wspb subpackages contain helpers for JSON and ProtoBuf messages. +// +// Please see https://nhooyr.io/websocket for more overview docs and a +// comparison with existing implementations. +// +// Please be sure to use the https://golang.org/x/xerrors package when inspecting returned errors. package websocket diff --git a/example_echo_test.go b/example_echo_test.go index a90257b82c5c1b774aead4861f08eddda029a95b..ab0e8e70c9e7c3828b518a45f34754508c54d116 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -16,8 +16,8 @@ import ( "nhooyr.io/websocket/wsjson" ) -// This example starts a WebSocket echo server and -// then dials the server and sends 5 different messages +// This example starts a WebSocket echo server, +// dials the server and then sends 5 different messages // and prints out the server's responses. func Example_echo() { // First we listen on port 0, that means the OS will diff --git a/example_test.go b/example_test.go index 7a0528ce342ce9444e96da12a62f7fca7fed26f4..57f0aa5ee072d069d9e39e505c3d140273d20c6a 100644 --- a/example_test.go +++ b/example_test.go @@ -36,7 +36,8 @@ func ExampleAccept() { c.Close(websocket.StatusNormalClosure, "") }) - http.ListenAndServe("localhost:8080", fn) + err := http.ListenAndServe("localhost:8080", fn) + log.Fatal(err) } // This example dials a server, writes a single JSON message and then @@ -47,15 +48,13 @@ func ExampleDial() { c, _, err := websocket.Dial(ctx, "ws://localhost:8080", websocket.DialOptions{}) if err != nil { - log.Println(err) - return + log.Fatal(err) } defer c.Close(websocket.StatusInternalError, "the sky is falling") err = wsjson.Write(ctx, c, "hi") if err != nil { - log.Println(err) - return + log.Fatal(err) } c.Close(websocket.StatusNormalClosure, "") diff --git a/statuscode.go b/statuscode.go index 69b015c32652ea239a56ca7a71700a5f26ce929d..d4223745d3e7a33ecae74a49d8c81ff5faa1b240 100644 --- a/statuscode.go +++ b/statuscode.go @@ -42,6 +42,7 @@ const ( // CloseError represents a WebSocket close frame. // It is returned by Conn's methods when the Connection is closed with a WebSocket close frame. +// You will need to use https://golang.org/x/xerrors to check for this error. type CloseError struct { Code StatusCode Reason string diff --git a/websocket.go b/websocket.go index 275af9da72d3be94438701401bb17e189148bbd0..912508d5635321679d8c2459a66035fac11ab9fb 100644 --- a/websocket.go +++ b/websocket.go @@ -61,8 +61,6 @@ type Conn struct { } func (c *Conn) close(err error) { - err = xerrors.Errorf("websocket closed: %w", err) - c.closeOnce.Do(func() { runtime.SetFinalizer(c, nil) @@ -71,7 +69,7 @@ func (c *Conn) close(err error) { cerr = err } - c.closeErr = cerr + c.closeErr = xerrors.Errorf("websocket closed: %w", cerr) close(c.closed) }) @@ -98,7 +96,7 @@ func (c *Conn) init() { c.readDone = make(chan int) runtime.SetFinalizer(c, func(c *Conn) { - c.Close(StatusInternalError, "connection garbage collected") + c.close(xerrors.New("connection garbage collected")) }) go c.writeLoop() @@ -238,7 +236,7 @@ func (c *Conn) handleControl(h header) { case opClose: ce, err := parseClosePayload(b) if err != nil { - c.close(xerrors.Errorf("read invalid close payload: %w", err)) + c.close(xerrors.Errorf("received invalid close payload: %w", err)) return } if ce.Code == StatusNoStatusRcvd { @@ -302,7 +300,7 @@ func (c *Conn) readLoop() { } } -func (c *Conn) dataReadLoop(h header) (err error) { +func (c *Conn) dataReadLoop(h header) error { maskPos := 0 left := h.payloadLength firstReadDone := false @@ -355,7 +353,6 @@ func (c *Conn) writePong(p []byte) error { // Close closes the WebSocket connection with the given status code and reason. // It will write a WebSocket close frame with a timeout of 5 seconds. -// Concurrent calls to Close are ok. func (c *Conn) Close(code StatusCode, reason string) error { err := c.exportedClose(code, reason) if err != nil { @@ -400,7 +397,7 @@ func (c *Conn) writeClose(p []byte, cerr CloseError) error { return err } - if cerr != c.closeErr { + if !xerrors.Is(c.closeErr, cerr) { return c.closeErr } @@ -420,9 +417,8 @@ func (c *Conn) writeSingleFrame(ctx context.Context, opcode opcode, p []byte) er payload: p, }: case <-ctx.Done(): - err := xerrors.Errorf("control frame write timed out: %w", ctx.Err()) - c.close(err) - return err + c.close(xerrors.Errorf("control frame write timed out: %w", ctx.Err())) + return ctx.Err() } select { @@ -487,7 +483,7 @@ func (w messageWriter) write(p []byte) (int, error) { select { case <-w.ctx.Done(): w.c.close(xerrors.Errorf("data write timed out: %w", w.ctx.Err())) - // Wait for writeLoop to complete so we know p is done. + // Wait for writeLoop to complete so we know p is done with. <-w.c.writeDone return 0, w.ctx.Err() case _, ok := <-w.c.writeDone: @@ -542,25 +538,21 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { } func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { - for !atomic.CompareAndSwapInt64(&c.activeReader, 0, 1) { - select { - case <-c.closed: - return 0, nil, c.closeErr - case c.readBytes <- nil: - select { - case <-ctx.Done(): - return 0, nil, ctx.Err() - case _, ok := <-c.readDone: - if !ok { - return 0, nil, c.closeErr - } - if atomic.LoadInt64(&c.activeReader) == 1 { - return 0, nil, xerrors.New("previous message not fully read") - } - } - case <-ctx.Done(): - return 0, nil, ctx.Err() + if !atomic.CompareAndSwapInt64(&c.activeReader, 0, 1) { + // If the next read yields io.EOF we are good to go. + r := messageReader{ + ctx: ctx, + c: c, } + _, err := r.Read(nil) + if err == nil { + return 0, nil, xerrors.New("previous message not fully read") + } + if !xerrors.Is(err, io.EOF) { + return 0, nil, xerrors.Errorf("failed to check if last message at io.EOF: %w", err) + } + + atomic.StoreInt64(&c.activeReader, 1) } select { @@ -586,7 +578,8 @@ type messageReader struct { func (r messageReader) Read(p []byte) (int, error) { n, err := r.read(p) if err != nil { - // Have to return io.EOF directly for now, cannot wrap. + // Have to return io.EOF directly for now, we cannot wrap as xerrors + // isn't used in stdlib. if err == io.EOF { return n, io.EOF }