Skip to content

Commit edb073b

Browse files
author
Michael Johnson
committed
Merge pull request #3 from gopherjs/dev
Implement net.Conn high-level interface, improve low-level interface.
2 parents ce4237a + d9ebf98 commit edb073b

File tree

14 files changed

+3529
-76
lines changed

14 files changed

+3529
-76
lines changed

LICENSE

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Copyright (c) 2014-2015, GopherJS Team
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5+
6+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7+
8+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9+
10+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11+
12+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,42 @@
11
websocket
22
=========
33

4-
Package websocket will provide GopherJS bindings for the WebSocket API.
4+
Package websocket provides high- and low-level bindings for the browser's WebSocket API.
55

6-
It is currently in development and should not be considered stable. The public API is unfinished and will change.
6+
The high-level bindings act like a regular net.Conn. They can be used as such. For example:
7+
8+
```Go
9+
c, err := websocket.Dial("ws://localhost/socket") // Blocks until connection is established
10+
if err != nil { handleError() }
11+
12+
buf := make([]byte, 1024)
13+
n, err = c.Read(buf) // Blocks until a WebSocket frame is received
14+
if err != nil { handleError() }
15+
doSomethingWithData(buf[:n])
16+
17+
_, err = c.Write([]byte("Hello!"))
18+
if err != nil { handleError() }
19+
20+
err = c.Close()
21+
if err != nil { handleError() }
22+
```
23+
24+
The low-level bindings use the typical JavaScript idioms.
25+
26+
```Go
27+
ws, err := websocket.New("ws://localhost/socket") // Does not block.
28+
if err != nil { handleError() }
29+
30+
onOpen := func(ev js.Object) {
31+
err := ws.Send([]byte("Hello!")) // Send as a binary frame
32+
err := ws.Send("Hello!") // Send a text frame
33+
}
34+
35+
ws.AddEventListener("open", false, onOpen)
36+
ws.AddEventListener("message", false, onMessage)
37+
ws.AddEventListener("close", false, onClose)
38+
ws.AddEventListener("error", false, onError)
39+
40+
err = ws.Close()
41+
if err != nil { handleError() }
42+
```

addr.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2014-2015 GopherJS Team. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be found
3+
// in the LICENSE file.
4+
5+
package websocket
6+
7+
import "net/url"
8+
9+
// Addr represents the address of a WebSocket connection.
10+
type Addr struct {
11+
*url.URL
12+
}
13+
14+
// Network returns the network type for a WebSocket, "websocket".
15+
func (addr *Addr) Network() string { return "websocket" }

conn.go

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
// Copyright 2014-2015 GopherJS Team. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be found
3+
// in the LICENSE file.
4+
5+
package websocket
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
"io"
11+
"net/url"
12+
"time"
13+
14+
"github.com/gopherjs/gopherjs/js"
15+
"honnef.co/go/js/dom"
16+
)
17+
18+
func beginHandlerOpen(ch chan error, removeHandlers func()) func(ev js.Object) {
19+
return func(ev js.Object) {
20+
removeHandlers()
21+
close(ch)
22+
}
23+
}
24+
25+
// closeError allows a CloseEvent to be used as an error.
26+
type closeError struct {
27+
*dom.CloseEvent
28+
}
29+
30+
func (e *closeError) Error() string {
31+
var cleanStmt string
32+
if e.WasClean {
33+
cleanStmt = "clean"
34+
} else {
35+
cleanStmt = "unclean"
36+
}
37+
return fmt.Sprintf("CloseEvent: (%s) (%d) %s", cleanStmt, e.Code, e.Reason)
38+
}
39+
40+
func beginHandlerClose(ch chan error, removeHandlers func()) func(ev js.Object) {
41+
return func(ev js.Object) {
42+
removeHandlers()
43+
go func() {
44+
ce := dom.WrapEvent(ev).(*dom.CloseEvent)
45+
ch <- &closeError{CloseEvent: ce}
46+
close(ch)
47+
}()
48+
}
49+
}
50+
51+
type deadlineErr struct{}
52+
53+
func (e *deadlineErr) Error() string { return "i/o timeout: deadline reached" }
54+
func (e *deadlineErr) Timeout() bool { return true }
55+
func (e *deadlineErr) Temporary() bool { return true }
56+
57+
var errDeadlineReached = &deadlineErr{}
58+
59+
// TODO(nightexcessive): Add a Dial function that allows a deadline to be
60+
// specified.
61+
62+
// Dial opens a new WebSocket connection. It will block until the connection is
63+
// established or fails to connect.
64+
func Dial(url string) (*Conn, error) {
65+
ws, err := New(url)
66+
if err != nil {
67+
return nil, err
68+
}
69+
conn := &Conn{
70+
WebSocket: ws,
71+
ch: make(chan *dom.MessageEvent, 1),
72+
}
73+
conn.initialize()
74+
75+
openCh := make(chan error, 1)
76+
77+
var (
78+
openHandler func(ev js.Object)
79+
closeHandler func(ev js.Object)
80+
)
81+
82+
// Handlers need to be removed to prevent a panic when the WebSocket closes
83+
// immediately and fires both open and close before they can be removed.
84+
// This way, handlers are removed before the channel is closed.
85+
removeHandlers := func() {
86+
ws.RemoveEventListener("open", false, openHandler)
87+
ws.RemoveEventListener("close", false, closeHandler)
88+
}
89+
90+
// We have to use variables for the functions so that we can remove the
91+
// event handlers afterwards.
92+
openHandler = beginHandlerOpen(openCh, removeHandlers)
93+
closeHandler = beginHandlerClose(openCh, removeHandlers)
94+
95+
ws.AddEventListener("open", false, openHandler)
96+
ws.AddEventListener("close", false, closeHandler)
97+
98+
err, ok := <-openCh
99+
if ok && err != nil {
100+
return nil, err
101+
}
102+
103+
return conn, nil
104+
}
105+
106+
// Conn is a high-level wrapper around WebSocket. It is intended to satisfy the
107+
// net.Conn interface.
108+
//
109+
// To create a Conn, use Dial. Instantiating Conn without Dial will not work.
110+
type Conn struct {
111+
*WebSocket
112+
113+
ch chan *dom.MessageEvent
114+
readBuf *bytes.Reader
115+
116+
readDeadline time.Time
117+
}
118+
119+
func (c *Conn) onMessage(event js.Object) {
120+
go func() {
121+
c.ch <- dom.WrapEvent(event).(*dom.MessageEvent)
122+
}()
123+
}
124+
125+
func (c *Conn) onClose(event js.Object) {
126+
go func() {
127+
// We queue nil to the end so that any messages received prior to
128+
// closing get handled first.
129+
c.ch <- nil
130+
}()
131+
}
132+
133+
// initialize adds all of the event handlers necessary for a Conn to function.
134+
// It should never be called more than once and is already called if Dial was
135+
// used to create the Conn.
136+
func (c *Conn) initialize() {
137+
// We need this so that received binary data is in ArrayBufferView format so
138+
// that it can easily be read.
139+
c.BinaryType = "arraybuffer"
140+
141+
c.AddEventListener("message", false, c.onMessage)
142+
c.AddEventListener("close", false, c.onClose)
143+
}
144+
145+
// handleFrame handles a single frame received from the channel. This is a
146+
// convenience funciton to dedupe code for the multiple deadline cases.
147+
func (c *Conn) handleFrame(item *dom.MessageEvent, ok bool) (*dom.MessageEvent, error) {
148+
if !ok { // The channel has been closed
149+
return nil, io.EOF
150+
} else if item == nil {
151+
// See onClose for the explanation about sending a nil item.
152+
close(c.ch)
153+
return nil, io.EOF
154+
}
155+
156+
return item, nil
157+
}
158+
159+
// receiveFrame receives one full frame from the WebSocket. It blocks until the
160+
// frame is received.
161+
func (c *Conn) receiveFrame(observeDeadline bool) (*dom.MessageEvent, error) {
162+
var deadlineChan <-chan time.Time // Receiving on a nil channel always blocks indefinitely
163+
164+
if observeDeadline && !c.readDeadline.IsZero() {
165+
now := time.Now()
166+
if now.After(c.readDeadline) {
167+
select {
168+
case item, ok := <-c.ch:
169+
return c.handleFrame(item, ok)
170+
default:
171+
return nil, errDeadlineReached
172+
}
173+
}
174+
175+
timer := time.NewTimer(c.readDeadline.Sub(now))
176+
defer timer.Stop()
177+
178+
deadlineChan = timer.C
179+
}
180+
181+
select {
182+
case item, ok := <-c.ch:
183+
return c.handleFrame(item, ok)
184+
case <-deadlineChan:
185+
return nil, errDeadlineReached
186+
}
187+
}
188+
189+
func getFrameData(obj js.Object) []byte {
190+
// Check if it's an array buffer. If so, convert it to a Go byte slice.
191+
if constructor := obj.Get("constructor"); constructor == js.Global.Get("ArrayBuffer") {
192+
int8Array := js.Global.Get("Uint8Array").New(obj)
193+
return int8Array.Interface().([]byte)
194+
}
195+
196+
return []byte(obj.Str())
197+
}
198+
199+
func (c *Conn) Read(b []byte) (n int, err error) {
200+
if c.readBuf != nil {
201+
n, err = c.readBuf.Read(b)
202+
if err == io.EOF {
203+
c.readBuf = nil
204+
err = nil
205+
}
206+
// If we read nothing from the buffer, continue to trying to receive.
207+
// This saves us when the last Read call emptied the buffer and this
208+
// call triggers the EOF. There's probably a better way of doing this,
209+
// but I'm really tired.
210+
if n > 0 {
211+
return
212+
}
213+
}
214+
215+
frame, err := c.receiveFrame(true)
216+
if err != nil {
217+
return 0, err
218+
}
219+
220+
receivedBytes := getFrameData(frame.Data)
221+
222+
n = copy(b, receivedBytes)
223+
// Fast path: The entire frame's contents have been copied into b.
224+
if n >= len(receivedBytes) {
225+
return
226+
}
227+
228+
c.readBuf = bytes.NewReader(receivedBytes[n:])
229+
return
230+
}
231+
232+
// Write writes the contents of b to the WebSocket using a binary opcode.
233+
func (c *Conn) Write(b []byte) (n int, err error) {
234+
// []byte is converted to an (U)Int8Array by GopherJS, which fullfils the
235+
// ArrayBufferView definition.
236+
err = c.Send(b)
237+
if err != nil {
238+
return
239+
}
240+
n = len(b)
241+
return
242+
}
243+
244+
// WriteString writes the contents of s to the WebSocket using a text frame
245+
// opcode.
246+
func (c *Conn) WriteString(s string) (n int, err error) {
247+
err = c.Send(s)
248+
if err != nil {
249+
return
250+
}
251+
n = len(s)
252+
return
253+
}
254+
255+
// BUG(nightexcessive): Conn doesn't currently fulfill the net.Conn interface
256+
// because we can't return net.Addr from Conn.LocalAddr and Conn.RemoteAddr
257+
// because net.init() causes a panic due to attempts to make syscalls.
258+
//
259+
// See: https://github.com/gopherjs/gopherjs/issues/123
260+
261+
// LocalAddr would typically return the local network address, but due to
262+
// limitations in the JavaScript API, it is unable to. Calling this method will
263+
// cause a panic.
264+
func (c *Conn) LocalAddr() *Addr {
265+
// BUG(nightexcessive): Conn.LocalAddr() panics because the underlying
266+
// JavaScript API has no way of figuring out the local address.
267+
268+
// TODO(nightexcessive): Find a more graceful way to handle this
269+
panic("we are unable to implement websocket.Conn.LocalAddr() due to limitations in the underlying JavaScript API")
270+
}
271+
272+
// RemoteAddr returns the remote network address, based on
273+
// websocket.WebSocket.URL.
274+
func (c *Conn) RemoteAddr() *Addr {
275+
wsURL, err := url.Parse(c.URL)
276+
if err != nil {
277+
// TODO(nightexcessive): Should we be panicking for this?
278+
panic(err)
279+
}
280+
return &Addr{wsURL}
281+
}
282+
283+
// SetDeadline sets the read and write deadlines associated with the connection.
284+
// It is equivalent to calling both SetReadDeadline and SetWriteDeadline.
285+
//
286+
// A zero value for t means that I/O operations will not time out.
287+
func (c *Conn) SetDeadline(t time.Time) error {
288+
c.readDeadline = t
289+
return nil
290+
}
291+
292+
// SetReadDeadline sets the deadline for future Read calls. A zero value for t
293+
// means Read will not time out.
294+
func (c *Conn) SetReadDeadline(t time.Time) error {
295+
c.readDeadline = t
296+
return nil
297+
}
298+
299+
// SetWriteDeadline sets the deadline for future Write calls. Because our writes
300+
// do not block, this function is a no-op.
301+
func (c *Conn) SetWriteDeadline(t time.Time) error {
302+
return nil
303+
}

0 commit comments

Comments
 (0)