Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring #81

Merged
merged 18 commits into from
May 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ go get nhooyr.io/websocket@v0.2.0
- Zero dependencies outside of the stdlib for the core library
- JSON and ProtoBuf helpers in the wsjson and wspb subpackages
- High performance
- Concurrent writes
- Concurrent reads and writes out of the box

## Roadmap

- [ ] WebSockets over HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4)
- [ ] Deflate extension support [#5](https://github.com/nhooyr/websocket/issues/5)

## Examples

Expand Down Expand Up @@ -86,11 +85,11 @@ c.Close(websocket.StatusNormalClosure, "")
- A minimal API is easier to maintain due to less docs, tests and bugs
- A minimal API is also easier to use 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](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
- We do not support the compression extension because Go's compress/flate library is very memory intensive
and browsers do not handle WebSocket compression intelligently. See [#5](https://github.com/nhooyr/websocket/issues/5)

## Comparison

Expand Down Expand Up @@ -122,8 +121,8 @@ also uses net/http's Client and ResponseWriter directly for WebSocket handshakes
gorilla/websocket writes its handshakes to the underlying net.Conn which means
it has to reinvent hooks for TLS and proxies and prevents support of HTTP/2.

Some more advantages of nhooyr/websocket are that it supports concurrent writes and makes it
very easy to close the connection with a status code and reason.
Some more advantages of nhooyr/websocket are that it supports concurrent reads,
writes and makes it very easy to close the connection with a status code and reason.

In terms of performance, there is no significant difference between the two. Will update
with benchmarks soon ([#75](https://github.com/nhooyr/websocket/issues/75)).
Expand Down
11 changes: 11 additions & 0 deletions accept.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package websocket

import (
"bytes"
"crypto/sha1"
"encoding/base64"
"io"
"net/http"
"net/textproto"
"net/url"
Expand Down Expand Up @@ -75,8 +77,12 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error {

// Accept accepts a WebSocket handshake from a client and upgrades the
// the connection to WebSocket.
//
// Accept will reject the handshake if the Origin domain is not the same as the Host unless
// the InsecureSkipVerify option is set.
//
// The returned connection will be bound by r.Context(). Use c.Context() to change
// the bounding context.
func Accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn, error) {
c, err := accept(w, r, opts)
if err != nil {
Expand Down Expand Up @@ -125,13 +131,18 @@ func accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn,
return nil, err
}

// https://github.com/golang/go/issues/32314
b, _ := brw.Reader.Peek(brw.Reader.Buffered())
brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn))

c := &Conn{
subprotocol: w.Header().Get("Sec-WebSocket-Protocol"),
br: brw.Reader,
bw: brw.Writer,
closer: netConn,
}
c.init()
c.Context(r.Context())

return c, nil
}
Expand Down
9 changes: 9 additions & 0 deletions ci/.codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
coverage:
status:
# Prevent small changes in coverage from failing CI.
project:
default:
threshold: 5
patch:
default:
threshold: 5
2 changes: 1 addition & 1 deletion ci/lint/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ source ci/lib.sh || exit 1
shellcheck ./**/*.sh
)

go vet -composites=false ./...
go vet -composites=false -lostcancel=false ./...
go run golang.org/x/lint/golint -set_exit_status ./...
40 changes: 38 additions & 2 deletions dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"strings"
"sync"

"golang.org/x/xerrors"
)
Expand Down Expand Up @@ -112,8 +113,8 @@ func dial(ctx context.Context, u string, opts DialOptions) (_ *Conn, _ *http.Res

c := &Conn{
subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"),
br: bufio.NewReader(rwc),
bw: bufio.NewWriter(rwc),
br: getBufioReader(rwc),
bw: getBufioWriter(rwc),
closer: rwc,
client: true,
}
Expand All @@ -140,3 +141,38 @@ func verifyServerResponse(resp *http.Response) error {

return nil
}

// The below pools can only be used by the client because http.Hijacker will always
// have a bufio.Reader/Writer for us so it doesn't make sense to use a pool on top.

var bufioReaderPool = sync.Pool{
New: func() interface{} {
return bufio.NewReader(nil)
},
}

func getBufioReader(r io.Reader) *bufio.Reader {
br := bufioReaderPool.Get().(*bufio.Reader)
br.Reset(r)
return br
}

func returnBufioReader(br *bufio.Reader) {
bufioReaderPool.Put(br)
}

var bufioWriterPool = sync.Pool{
New: func() interface{} {
return bufio.NewWriter(nil)
},
}

func getBufioWriter(w io.Writer) *bufio.Writer {
bw := bufioWriterPool.Get().(*bufio.Writer)
bw.Reset(w)
return bw
}

func returnBufioWriter(bw *bufio.Writer) {
bufioWriterPool.Put(bw)
}
7 changes: 4 additions & 3 deletions example_echo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func Example_echo() {

// Now we dial the server, send the messages and echo the responses.
err = client("ws://" + l.Addr().String())
time.Sleep(time.Second)
if err != nil {
log.Fatalf("client failed: %v", err)
}
Expand All @@ -66,6 +67,8 @@ func Example_echo() {
// It ensures the client speaks the echo subprotocol and
// only allows one message every 100ms with a 10 message burst.
func echoServer(w http.ResponseWriter, r *http.Request) error {
log.Printf("serving %v", r.RemoteAddr)

c, err := websocket.Accept(w, r, websocket.AcceptOptions{
Subprotocols: []string{"echo"},
})
Expand All @@ -83,15 +86,14 @@ func echoServer(w http.ResponseWriter, r *http.Request) error {
for {
err = echo(r.Context(), c, l)
if err != nil {
return xerrors.Errorf("failed to echo: %w", err)
return xerrors.Errorf("failed to echo with %v: %w", r.RemoteAddr, err)
}
}
}

// echo reads from the websocket connection and then writes
// the received message back to it.
// The entire function has 10s to complete.
// The received message is limited to 32768 bytes.
func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error {
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
Expand All @@ -105,7 +107,6 @@ func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error {
if err != nil {
return err
}
r = io.LimitReader(r, 32768)

w, err := c.Writer(ctx, typ)
if err != nil {
Expand Down
18 changes: 0 additions & 18 deletions export_test.go

This file was deleted.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ require (
golang.org/x/text v0.3.2 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
golang.org/x/tools v0.0.0-20190429184909-35c670923e21
golang.org/x/xerrors v0.0.0-20190315151331-d61658bd2e18
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
mvdan.cc/sh v2.6.4+incompatible
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190429184909-35c670923e21 h1:Kjcw+D2LTzLmxOHrMK9uvYP/NigJ0EdwMgzt6EU+Ghs=
golang.org/x/tools v0.0.0-20190429184909-35c670923e21/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20190315151331-d61658bd2e18 h1:1AGvnywFL1aB5KLRxyLseWJI6aSYPo3oF7HSpXdWQdU=
golang.org/x/xerrors v0.0.0-20190315151331-d61658bd2e18/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
2 changes: 1 addition & 1 deletion statuscode.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type CloseError struct {
}

func (ce CloseError) Error() string {
return fmt.Sprintf("websocket closed with status = %v and reason = %q", ce.Code, ce.Reason)
return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason)
}

func parseClosePayload(p []byte) (CloseError, error) {
Expand Down