Skip to content

Commit 9c8f1d1

Browse files
authored
Merge pull request #84 from nhooyr/stable
Polish for stable release
2 parents 80689e3 + 3821939 commit 9c8f1d1

16 files changed

+665
-500
lines changed

Diff for: .gitignore

-3
This file was deleted.

Diff for: README.md

+24-13
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,22 @@
55

66
websocket is a minimal and idiomatic WebSocket library for Go.
77

8-
This library is not final and the API is subject to change.
9-
108
## Install
119

1210
```bash
13-
go get nhooyr.io/websocket@v0.2.0
11+
go get nhooyr.io/websocket@v1.0.0
1412
```
1513

1614
## Features
1715

1816
- Minimal and idiomatic API
19-
- Tiny codebase at 1400 lines
17+
- Tiny codebase at 1700 lines
2018
- First class context.Context support
2119
- Thorough tests, fully passes the [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite)
2220
- Zero dependencies outside of the stdlib for the core library
2321
- JSON and ProtoBuf helpers in the wsjson and wspb subpackages
24-
- High performance
25-
- Concurrent reads and writes out of the box
22+
- Highly optimized by default
23+
- Concurrent writes out of the box
2624

2725
## Roadmap
2826

@@ -88,8 +86,9 @@ c.Close(websocket.StatusNormalClosure, "")
8886
- net.Conn is never exposed as WebSocket over HTTP/2 will not have a net.Conn.
8987
- Using net/http's Client for dialing means we do not have to reinvent dialing hooks
9088
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)
89+
- We do not support the deflate compression extension because Go's compress/flate library
90+
is very memory intensive and browsers do not handle WebSocket compression intelligently.
91+
See [#5](https://github.com/nhooyr/websocket/issues/5)
9392

9493
## Comparison
9594

@@ -111,7 +110,7 @@ Just compare the godoc of
111110

112111
The API for nhooyr/websocket has been designed such that there is only one way to do things
113112
which makes it easy to use correctly. Not only is the API simpler, the implementation is
114-
only 1400 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain,
113+
only 1700 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain,
115114
more code to test, more code to document and more surface area for bugs.
116115

117116
The future of gorilla/websocket is also uncertain. See [gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370).
@@ -121,11 +120,23 @@ also uses net/http's Client and ResponseWriter directly for WebSocket handshakes
121120
gorilla/websocket writes its handshakes to the underlying net.Conn which means
122121
it has to reinvent hooks for TLS and proxies and prevents support of HTTP/2.
123122

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.
123+
Some more advantages of nhooyr/websocket are that it supports concurrent writes and
124+
makes it very easy to close the connection with a status code and reason.
125+
126+
nhooyr/websocket also responds to pings, pongs and close frames in a separate goroutine so that
127+
your application doesn't always need to read from the connection unless it expects a data message.
128+
gorilla/websocket requires you to constantly read from the connection to respond to control frames
129+
even if you don't expect the peer to send any messages.
130+
131+
In terms of performance, the differences depend on your application code. nhooyr/websocket
132+
reuses buffers efficiently out of the box if you use the wsjson and wspb subpackages whereas
133+
gorilla/websocket does not. As mentioned above, nhooyr/websocket also supports concurrent
134+
writers out of the box.
126135

127-
In terms of performance, the only difference is nhooyr/websocket is forced to use one extra
128-
goroutine for context.Context support. Otherwise, they perform identically.
136+
The only performance con to nhooyr/websocket is that uses two extra goroutines. One for
137+
reading pings, pongs and close frames async to application code and another to support
138+
context.Context cancellation. This costs 4 KB of memory which is cheap compared
139+
to the benefits.
129140

130141
### x/net/websocket
131142

Diff for: accept.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error {
7676
}
7777

7878
// Accept accepts a WebSocket handshake from a client and upgrades the
79-
// the connection to WebSocket.
79+
// the connection to a WebSocket.
8080
//
8181
// Accept will reject the handshake if the Origin domain is not the same as the Host unless
8282
// the InsecureSkipVerify option is set.
8383
//
84-
// The returned connection will be bound by r.Context(). Use c.Context() to change
84+
// The returned connection will be bound by r.Context(). Use conn.Context() to change
8585
// the bounding context.
8686
func Accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn, error) {
8787
c, err := accept(w, r, opts)
@@ -107,15 +107,15 @@ func accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn,
107107

108108
hj, ok := w.(http.Hijacker)
109109
if !ok {
110-
err = xerrors.New("response writer must implement http.Hijacker")
110+
err = xerrors.New("passed ResponseWriter does not implement http.Hijacker")
111111
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
112112
return nil, err
113113
}
114114

115115
w.Header().Set("Upgrade", "websocket")
116116
w.Header().Set("Connection", "Upgrade")
117117

118-
handleKey(w, r)
118+
handleSecWebSocketKey(w, r)
119119

120120
subproto := selectSubprotocol(r, opts.Subprotocols)
121121
if subproto != "" {
@@ -163,7 +163,7 @@ func selectSubprotocol(r *http.Request, subprotocols []string) string {
163163

164164
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
165165

166-
func handleKey(w http.ResponseWriter, r *http.Request) {
166+
func handleSecWebSocketKey(w http.ResponseWriter, r *http.Request) {
167167
key := r.Header.Get("Sec-WebSocket-Key")
168168
h := sha1.New()
169169
h.Write([]byte(key))
@@ -185,5 +185,5 @@ func authenticateOrigin(r *http.Request) error {
185185
if strings.EqualFold(u.Host, r.Host) {
186186
return nil
187187
}
188-
return xerrors.Errorf("request origin %q is not authorized for host %q", origin, r.Host)
188+
return xerrors.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host)
189189
}

Diff for: ci/bench/entrypoint.sh

+6-8
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22

33
source ci/lib.sh || exit 1
44

5-
mkdir -p profs
6-
7-
go test --vet=off --run=^$ -bench=. \
8-
-cpuprofile=profs/cpu \
9-
-memprofile=profs/mem \
10-
-blockprofile=profs/block \
11-
-mutexprofile=profs/mutex \
5+
go test --vet=off --run=^$ -bench=. -o=ci/out/websocket.test \
6+
-cpuprofile=ci/out/cpu.prof \
7+
-memprofile=ci/out/mem.prof \
8+
-blockprofile=ci/out/block.prof \
9+
-mutexprofile=ci/out/mutex.prof \
1210
.
1311

1412
set +x
1513
echo
16-
echo "profiles are in ./profs
14+
echo "profiles are in ./ci/out/*.prof
1715
keep in mind that every profiler Go provides is enabled so that may skew the benchmarks"

Diff for: 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 -lostcancel=false ./...
10+
go vet ./...
1111
go run golang.org/x/lint/golint -set_exit_status ./...

Diff for: ci/out/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*

Diff for: ci/test/entrypoint.sh

+5-7
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
source ci/lib.sh || exit 1
44

5-
mkdir -p profs
6-
75
set +x
86
echo
97
echo "this step includes benchmarks for race detection and coverage purposes
@@ -12,15 +10,15 @@ accurate numbers"
1210
echo
1311
set -x
1412

15-
go test -race -coverprofile=profs/coverage --vet=off -bench=. ./...
16-
go tool cover -func=profs/coverage
13+
go test -race -coverprofile=ci/out/coverage.prof --vet=off -bench=. ./...
14+
go tool cover -func=ci/out/coverage.prof
1715

1816
if [[ $CI ]]; then
19-
bash <(curl -s https://codecov.io/bash) -f profs/coverage
17+
bash <(curl -s https://codecov.io/bash) -f ci/out/coverage.prof
2018
else
21-
go tool cover -html=profs/coverage -o=profs/coverage.html
19+
go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html
2220

2321
set +x
2422
echo
25-
echo "please open profs/coverage.html to see detailed test coverage stats"
23+
echo "please open ci/out/coverage.html to see detailed test coverage stats"
2624
fi

Diff for: dial.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import (
1818
// DialOptions represents the options available to pass to Dial.
1919
type DialOptions struct {
2020
// HTTPClient is the http client used for the handshake.
21-
// Its Transport must use HTTP/1.1 and return writable bodies
22-
// for WebSocket handshakes. This was introduced in Go 1.12.
23-
// http.Transport does this all correctly.
21+
// Its Transport must return writable bodies
22+
// for WebSocket handshakes.
23+
// http.Transport does this correctly beginning with Go 1.12.
2424
HTTPClient *http.Client
2525

2626
// HTTPHeader specifies the HTTP headers included in the handshake request.
@@ -30,7 +30,7 @@ type DialOptions struct {
3030
Subprotocols []string
3131
}
3232

33-
// We use this key for all client requests as the Sec-WebSocket-Key header is useless.
33+
// We use this key for all client requests as the Sec-WebSocket-Key header doesn't do anything.
3434
// See https://stackoverflow.com/a/37074398/4283659.
3535
// We also use the same mask key for every message as it too does not make a difference.
3636
var secWebSocketKey = base64.StdEncoding.EncodeToString(make([]byte, 16))
@@ -108,7 +108,7 @@ func dial(ctx context.Context, u string, opts DialOptions) (_ *Conn, _ *http.Res
108108

109109
rwc, ok := resp.Body.(io.ReadWriteCloser)
110110
if !ok {
111-
return nil, resp, xerrors.Errorf("response body is not a read write closer: %T", rwc)
111+
return nil, resp, xerrors.Errorf("response body is not a io.ReadWriteCloser: %T", rwc)
112112
}
113113

114114
c := &Conn{

Diff for: example_echo_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
// dials the server and then sends 5 different messages
2121
// and prints out the server's responses.
2222
func Example_echo() {
23-
// First we listen on port 0, that means the OS will
23+
// First we listen on port 0 which means the OS will
2424
// assign us a random free port. This is the listener
2525
// the server will serve on and the client will connect to.
2626
l, err := net.Listen("tcp", "localhost:0")
@@ -51,7 +51,6 @@ 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)
5554
if err != nil {
5655
log.Fatalf("client failed: %v", err)
5756
}

Diff for: limitedreader.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package websocket
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"golang.org/x/xerrors"
8+
)
9+
10+
type limitedReader struct {
11+
c *Conn
12+
r io.Reader
13+
left int64
14+
limit int64
15+
}
16+
17+
func (lr *limitedReader) Read(p []byte) (int, error) {
18+
if lr.limit == 0 {
19+
lr.limit = lr.left
20+
}
21+
22+
if lr.left <= 0 {
23+
msg := fmt.Sprintf("read limited at %v bytes", lr.limit)
24+
lr.c.Close(StatusPolicyViolation, msg)
25+
return 0, xerrors.Errorf(msg)
26+
}
27+
28+
if int64(len(p)) > lr.left {
29+
p = p[:lr.left]
30+
}
31+
n, err := lr.r.Read(p)
32+
lr.left -= int64(n)
33+
return n, err
34+
}

Diff for: messagetype.go

+2
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ const (
1313
// MessageBinary is for binary messages like Protobufs.
1414
MessageBinary MessageType = MessageType(opBinary)
1515
)
16+
17+
// Above I've explicitly included the types of the constants for stringer.

Diff for: statuscode.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func parseClosePayload(p []byte) (CloseError, error) {
6060
}
6161

6262
if len(p) < 2 {
63-
return CloseError{}, xerrors.Errorf("close payload too small, cannot even contain the 2 byte status code")
63+
return CloseError{}, xerrors.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p)
6464
}
6565

6666
ce := CloseError{
@@ -78,13 +78,13 @@ func parseClosePayload(p []byte) (CloseError, error) {
7878
// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
7979
// and https://tools.ietf.org/html/rfc6455#section-7.4.1
8080
func validWireCloseCode(code StatusCode) bool {
81-
if code >= StatusNormalClosure && code <= statusTLSHandshake {
82-
switch code {
83-
case 1004, StatusNoStatusRcvd, statusAbnormalClosure, statusTLSHandshake:
84-
return false
85-
default:
86-
return true
87-
}
81+
switch code {
82+
case 1004, StatusNoStatusRcvd, statusAbnormalClosure, statusTLSHandshake:
83+
return false
84+
}
85+
86+
if code >= StatusNormalClosure && code <= StatusBadGateway {
87+
return true
8888
}
8989
if code >= 3000 && code <= 4999 {
9090
return true

0 commit comments

Comments
 (0)