Skip to content

Commit 80689e3

Browse files
authored
Merge pull request #81 from nhooyr/fixes
Refactoring
2 parents 3eb559b + 15b8365 commit 80689e3

14 files changed

+568
-361
lines changed

README.md

+5-6
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@ go get nhooyr.io/[email protected]
2222
- Zero dependencies outside of the stdlib for the core library
2323
- JSON and ProtoBuf helpers in the wsjson and wspb subpackages
2424
- High performance
25-
- Concurrent writes
25+
- Concurrent reads and writes out of the box
2626

2727
## Roadmap
2828

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

3231
## Examples
3332

@@ -86,11 +85,11 @@ c.Close(websocket.StatusNormalClosure, "")
8685
- A minimal API is easier to maintain due to less docs, tests and bugs
8786
- A minimal API is also easier to use and learn
8887
- Context based cancellation is more ergonomic and robust than setting deadlines
89-
- No ping support because TCP keep alives work fine for HTTP/1.1 and they do not make
90-
sense with HTTP/2 (see [#1](https://github.com/nhooyr/websocket/issues/1))
9188
- net.Conn is never exposed as WebSocket over HTTP/2 will not have a net.Conn.
9289
- Using net/http's Client for dialing means we do not have to reinvent dialing hooks
9390
and configurations like other WebSocket libraries
91+
- We do not support the compression extension because Go's compress/flate library is very memory intensive
92+
and browsers do not handle WebSocket compression intelligently. See [#5](https://github.com/nhooyr/websocket/issues/5)
9493

9594
## Comparison
9695

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

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

128127
In terms of performance, the only difference is nhooyr/websocket is forced to use one extra
129128
goroutine for context.Context support. Otherwise, they perform identically.

accept.go

+11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package websocket
22

33
import (
4+
"bytes"
45
"crypto/sha1"
56
"encoding/base64"
7+
"io"
68
"net/http"
79
"net/textproto"
810
"net/url"
@@ -75,8 +77,12 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error {
7577

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

134+
// https://github.com/golang/go/issues/32314
135+
b, _ := brw.Reader.Peek(brw.Reader.Buffered())
136+
brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn))
137+
128138
c := &Conn{
129139
subprotocol: w.Header().Get("Sec-WebSocket-Protocol"),
130140
br: brw.Reader,
131141
bw: brw.Writer,
132142
closer: netConn,
133143
}
134144
c.init()
145+
c.Context(r.Context())
135146

136147
return c, nil
137148
}

ci/.codecov.yml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
coverage:
2+
status:
3+
# Prevent small changes in coverage from failing CI.
4+
project:
5+
default:
6+
threshold: 5
7+
patch:
8+
default:
9+
threshold: 5

ci/lint/entrypoint.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ source ci/lib.sh || exit 1
77
shellcheck ./**/*.sh
88
)
99

10-
go vet -composites=false ./...
10+
go vet -composites=false -lostcancel=false ./...
1111
go run golang.org/x/lint/golint -set_exit_status ./...

dial.go

+38-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/http"
1111
"net/url"
1212
"strings"
13+
"sync"
1314

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

113114
c := &Conn{
114115
subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"),
115-
br: bufio.NewReader(rwc),
116-
bw: bufio.NewWriter(rwc),
116+
br: getBufioReader(rwc),
117+
bw: getBufioWriter(rwc),
117118
closer: rwc,
118119
client: true,
119120
}
@@ -140,3 +141,38 @@ func verifyServerResponse(resp *http.Response) error {
140141

141142
return nil
142143
}
144+
145+
// The below pools can only be used by the client because http.Hijacker will always
146+
// have a bufio.Reader/Writer for us so it doesn't make sense to use a pool on top.
147+
148+
var bufioReaderPool = sync.Pool{
149+
New: func() interface{} {
150+
return bufio.NewReader(nil)
151+
},
152+
}
153+
154+
func getBufioReader(r io.Reader) *bufio.Reader {
155+
br := bufioReaderPool.Get().(*bufio.Reader)
156+
br.Reset(r)
157+
return br
158+
}
159+
160+
func returnBufioReader(br *bufio.Reader) {
161+
bufioReaderPool.Put(br)
162+
}
163+
164+
var bufioWriterPool = sync.Pool{
165+
New: func() interface{} {
166+
return bufio.NewWriter(nil)
167+
},
168+
}
169+
170+
func getBufioWriter(w io.Writer) *bufio.Writer {
171+
bw := bufioWriterPool.Get().(*bufio.Writer)
172+
bw.Reset(w)
173+
return bw
174+
}
175+
176+
func returnBufioWriter(bw *bufio.Writer) {
177+
bufioWriterPool.Put(bw)
178+
}

example_echo_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ func Example_echo() {
5151

5252
// Now we dial the server, send the messages and echo the responses.
5353
err = client("ws://" + l.Addr().String())
54+
time.Sleep(time.Second)
5455
if err != nil {
5556
log.Fatalf("client failed: %v", err)
5657
}
@@ -66,6 +67,8 @@ func Example_echo() {
6667
// It ensures the client speaks the echo subprotocol and
6768
// only allows one message every 100ms with a 10 message burst.
6869
func echoServer(w http.ResponseWriter, r *http.Request) error {
70+
log.Printf("serving %v", r.RemoteAddr)
71+
6972
c, err := websocket.Accept(w, r, websocket.AcceptOptions{
7073
Subprotocols: []string{"echo"},
7174
})
@@ -83,15 +86,14 @@ func echoServer(w http.ResponseWriter, r *http.Request) error {
8386
for {
8487
err = echo(r.Context(), c, l)
8588
if err != nil {
86-
return xerrors.Errorf("failed to echo: %w", err)
89+
return xerrors.Errorf("failed to echo with %v: %w", r.RemoteAddr, err)
8790
}
8891
}
8992
}
9093

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

110111
w, err := c.Writer(ctx, typ)
111112
if err != nil {

export_test.go

-18
This file was deleted.

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ require (
1212
golang.org/x/text v0.3.2 // indirect
1313
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
1414
golang.org/x/tools v0.0.0-20190429184909-35c670923e21
15-
golang.org/x/xerrors v0.0.0-20190315151331-d61658bd2e18
15+
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
1616
mvdan.cc/sh v2.6.4+incompatible
1717
)

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
2828
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
2929
golang.org/x/tools v0.0.0-20190429184909-35c670923e21 h1:Kjcw+D2LTzLmxOHrMK9uvYP/NigJ0EdwMgzt6EU+Ghs=
3030
golang.org/x/tools v0.0.0-20190429184909-35c670923e21/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
31-
golang.org/x/xerrors v0.0.0-20190315151331-d61658bd2e18 h1:1AGvnywFL1aB5KLRxyLseWJI6aSYPo3oF7HSpXdWQdU=
32-
golang.org/x/xerrors v0.0.0-20190315151331-d61658bd2e18/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
31+
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
32+
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
3333
mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM=
3434
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=

statuscode.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type CloseError struct {
4949
}
5050

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

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

0 commit comments

Comments
 (0)