From 450200a3e30436b3268f3cce7cec24b4025ac559 Mon Sep 17 00:00:00 2001 From: Tylor Arndt Date: Thu, 26 Dec 2019 14:55:45 -0600 Subject: [PATCH] Improve documentation. --- README.md | 8 +++--- arrayreader_js.go | 9 +++++++ dial_js.go | 10 ++++++++ doc.go | 9 +++++++ helpertypes_js.go | 4 +++ sockettype_js.go | 7 ++++++ streamreader_js.go | 5 ++++ websock_js.go | 62 ++++++++++++++++++++++++++++++++++++++++------ wslistener.go | 10 ++++++++ 9 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 doc.go diff --git a/README.md b/README.md index a8aeb76..613cd08 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# wasmws: Webassembly Websocket (for Go) +# wasmws: Web-Assembly Websocket (for Go) -## What is wasms? Why would I want to use this? +## What is wasmws? Why would I want to use this? [wasmws](https://github.com/tarndt/wasmws) was written primarily to allow [Go](https://golang.org/) applications targeting [WASM](https://en.wikipedia.org/wiki/WebAssembly) to communicate with a [gRPC](https://grpc.io/) server. This is normally challenging for two reasons: @@ -69,7 +69,7 @@ If you do not have Go installed, you will of course need to [install it](https:/ ## Alternatives 1. Use [gRPC-Web](https://github.com/grpc/grpc-web) as a HTTP to gRPC gateway/proxy. (If you don't mind a TCP connection per request, running extra middleware which are also extra points of failure...) -2. Use "[nhooyr.io/websocket](https://github.com/nhooyr/websocket)"'s implemtation which unlike "wasmws" does not use the browser provided websocket functionality. Test and bench your own use-case! +2. Use "[nhooyr.io/websocket](https://github.com/nhooyr/websocket)"'s implementation which unlike "wasmws" does not use the browser provided websocket functionality. Test and bench your own use-case! ## Future @@ -81,6 +81,6 @@ wasmws is actively being maintained, but that does not mean there are not things ## Contributing -[Issues](https://github.com/tarndt/wasmws/issues), and espcially issues with [pull requests](https://github.com/tarndt/wasmws/pulls) are welcome! +[Issues](https://github.com/tarndt/wasmws/issues), and especially issues with [pull requests](https://github.com/tarndt/wasmws/pulls) are welcome! This code is licensed under [MPL 2.0](https://en.wikipedia.org/wiki/Mozilla_Public_License). diff --git a/arrayreader_js.go b/arrayreader_js.go index 8a10b3b..57b9fc9 100644 --- a/arrayreader_js.go +++ b/arrayreader_js.go @@ -23,24 +23,30 @@ var arrayReaderPool = sync.Pool{ }, } +//newReaderArrayPromise returns a arrayReader from a JavaScript promise for +// an array buffer: See https://developer.mozilla.org/en-US/docs/Web/API/Blob/arrayBuffer func newReaderArrayPromise(arrayPromise js.Value) *arrayReader { ar := arrayReaderPool.Get().(*arrayReader) ar.jsPromise = arrayPromise return ar } +//newReaderArrayPromise returns a arrayReader from a JavaScript array buffer: +// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer func newReaderArrayBuffer(arrayBuffer js.Value) (*arrayReader, int) { ar := arrayReaderPool.Get().(*arrayReader) ar.remaining, ar.read = ar.fromArray(arrayBuffer), true return ar, len(ar.remaining) } +//Close closes the arrayReader and returns it to a pool. DO NOT USE FURTHER! func (ar *arrayReader) Close() error { ar.Reset() arrayReaderPool.Put(ar) return nil } +//Reset makes this arrayReader ready for reuse func (ar *arrayReader) Reset() { const bufMax = socketStreamThresholdBytes ar.jsPromise, ar.read, ar.err = js.Value{}, false, nil @@ -51,6 +57,7 @@ func (ar *arrayReader) Reset() { } } +//Read implements the standard io.Reader interface func (ar *arrayReader) Read(buf []byte) (n int, err error) { if ar.err != nil { return 0, ar.err @@ -89,6 +96,8 @@ func (ar *arrayReader) Read(buf []byte) (n int, err error) { return n, nil } +//fromArray is a helper that that copies a JavaScript ArrayBuffer into go-space +// and uses an existing go buffer if possible. func (ar *arrayReader) fromArray(arrayBuffer js.Value) []byte { jsBuf := uint8Array.New(arrayBuffer) count := jsBuf.Get("byteLength").Int() diff --git a/dial_js.go b/dial_js.go index 24767f3..f34fc2d 100644 --- a/dial_js.go +++ b/dial_js.go @@ -8,10 +8,17 @@ import ( "strings" ) +//Dial is a standard legacy network dialer that returns a websocket-based connection. +//See: DialContext for details on the network and address. func Dial(network, address string) (net.Conn, error) { return DialContext(context.Background(), network, address) } +//DialContext is a standard context-aware network dialer that returns a websocket-based connection. +// The address is a URL that should be in the form of "ws://host/path..." for unsecured websockets +// and "wss://host/path..." for secured websockets. If tunnel a TLS based protocol +// over a "wss://..." websocket you will get TLS twice, once on the websocket using +// the browsers TLS stack and another using the Go (or other compiled) TLS stack. func DialContext(ctx context.Context, network, address string) (net.Conn, error) { if network != "websocket" { return nil, fmt.Errorf("Invalid network: %q; Details: Only \"websocket\" network is supported", network) @@ -22,6 +29,9 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error) return New(ctx, address) } +//GRPCDialer is a helper that can be used with grpc.WithContextDialer to call DialContext. +//The address provided to the calling grpc.Dial should be in the form "passthrough:///"+websocketURL +// where websocketURL matches the description in DialContext. func GRPCDialer(ctx context.Context, address string) (net.Conn, error) { return DialContext(ctx, "websocket", address) } diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..9ac6af1 --- /dev/null +++ b/doc.go @@ -0,0 +1,9 @@ +package wasmws + +/* +Package wasm is a library allows you to use a websocket as net.Conn for arbitrary traffic (this includes tunneling protocols like HTTP, gRPC or any other TCP protocol over it). This is most useful for protocols that are not normally exposed to client side web applications. The specific motivation of this package was to allow Go applications targeting WASM to communicate with a gRPC server. + +wasmws.WebSocket is the provided net.Conn implementation intended to be used from within Go WASM applications and wasmws.WebSockListener is the provided net.Listener intended to be used from server side native Go applications to accept client connections. + +For extended details and examples please see: https://github.com/tarndt/wasmws/blob/master/README.md +*/ \ No newline at end of file diff --git a/helpertypes_js.go b/helpertypes_js.go index 4be5ed8..5f50dc8 100644 --- a/helpertypes_js.go +++ b/helpertypes_js.go @@ -9,6 +9,8 @@ var ( uint8Array = js.Global().Get("Uint8Array") ) +//timeoutErr is a net.Addr implementation for the websocket to use when fufilling +// the net.Conn interface type timeoutError struct{} func (timeoutError) Error() string { return "deadline exceeded" } @@ -17,6 +19,8 @@ func (timeoutError) Timeout() bool { return true } func (timeoutError) Temporary() bool { return true } +//wsAddr is a net.Addr implementation for the websocket to use when fufilling +// the net.Conn interface type wsAddr string func (wsAddr) Network() string { return "websocket" } diff --git a/sockettype_js.go b/sockettype_js.go index 15d4a7b..1501a1f 100644 --- a/sockettype_js.go +++ b/sockettype_js.go @@ -12,10 +12,14 @@ const ( type socketType uint8 +//newSocketType returns the socket type of the provided JavaScript websocket object func newSocketType(websocket js.Value) socketType { return newSocketTypeString(websocket.Get("binaryType").String()) } +//newSocketTypeString returns a socketType from a string of the socket type that +// matches the JavaScript websock.binaryType property: +// See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType func newSocketTypeString(wsTypeStr string) socketType { switch wsTypeStr { case "blob": @@ -27,6 +31,8 @@ func newSocketTypeString(wsTypeStr string) socketType { } } +//String returns the a string of the socket type that matches the JavaScript +// websock.binaryType property: See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType func (st socketType) String() string { switch st { case socketTypeArrayBuffer: @@ -38,6 +44,7 @@ func (st socketType) String() string { } } +//Set sets the type of the provided JavaScript websocket to itself func (st socketType) Set(websocket js.Value) { websocket.Set("binaryType", st.String()) if debugVerbose { diff --git a/streamreader_js.go b/streamreader_js.go index ad79668..de86453 100644 --- a/streamreader_js.go +++ b/streamreader_js.go @@ -21,18 +21,22 @@ var streamReaderPool = sync.Pool{ }, } +//newStreamReaderPromise returns a streamReader from a JavaScript promise for +// a stream reader: See https://developer.mozilla.org/en-US/docs/Web/API/Blob/stream func newStreamReaderPromise(streamPromise js.Value) *streamReader { sr := streamReaderPool.Get().(*streamReader) sr.jsPromise = streamPromise return sr } +//Close closes the streamReader and returns it to a pool. DO NOT USE FURTHER! func (sr *streamReader) Close() error { sr.Reset() streamReaderPool.Put(sr) return nil } +//Reset makes this streamReader ready for reuse func (sr *streamReader) Reset() { const bufMax = socketStreamThresholdBytes sr.jsPromise, sr.err = js.Value{}, nil @@ -43,6 +47,7 @@ func (sr *streamReader) Reset() { } } +//Read implements the standard io.Reader interface func (sr *streamReader) Read(p []byte) (n int, err error) { if sr.err != nil { return 0, sr.err diff --git a/websock_js.go b/websock_js.go index 98a9658..08b66c2 100644 --- a/websock_js.go +++ b/websock_js.go @@ -12,16 +12,22 @@ import ( ) const ( - socketStreamThresholdBytes = 1024 - debugVerbose = false + socketStreamThresholdBytes = 1024 //If enabled, the Blob interface will be used when consecutive messages exceed this threshold + debugVerbose = false //Set to true if you are debugging issues, this gates many prints that would kill performance ) var ( + //EnableBlobStreaming allows the browser provided Websocket's streaming + // interface to be used, if supported. EnableBlobStreaming bool = true - blobSupported bool - ErrWebsocketClosed = errors.New("WebSocket: Web socket is closed") + + //ErrWebsocketClosed is returned when operations are performed on a closed Websocket + ErrWebsocketClosed = errors.New("WebSocket: Web socket is closed") + + blobSupported bool //set to true by init if browser supports the Blob interface ) +//init checks to see if the browser hosting this application support the Websocket Blob interface func init() { newBlob := js.Global().Get("Blob") if newBlob == jsUndefined { @@ -37,6 +43,7 @@ func init() { } } +//WebSocket is a Go struct that wraps the web browser's JavaScript websocket object and provides a net.Conn interface type WebSocket struct { ctx context.Context ctxCancel context.CancelFunc @@ -63,6 +70,11 @@ type WebSocket struct { cleanup []func() } +//New returns a new WebSocket using the provided dial context and websocket URL. +// The URL should be in the form of "ws://host/path..." for unsecured websockets +// and "wss://host/path..." for secured websockets. If tunnel a TLS based protocol +// over a "wss://..." websocket you will get TLS twice, once on the websocket using +// the browsers TLS stack and another using the Go (or other compiled) TLS stack. func New(dialCtx context.Context, URL string) (*WebSocket, error) { ctx, cancel := context.WithCancel(context.Background()) ws := &WebSocket{ @@ -104,6 +116,19 @@ func New(dialCtx context.Context, URL string) (*WebSocket, error) { for _, cleanup := range ws.cleanup { cleanup() } + + for { + select { + case pending := <-ws.readCh: + if closer, hasClose := pending.(io.Closer); hasClose { + closer.Close() + } + continue + + default: + } + break + } }() //Wait for connection or failure @@ -136,6 +161,7 @@ func New(dialCtx context.Context, URL string) (*WebSocket, error) { return ws, nil } +//Close shuts the websocket down func (ws *WebSocket) Close() error { if debugVerbose { println("Websocket: Internal close") @@ -144,14 +170,19 @@ func (ws *WebSocket) Close() error { return nil } +//LocalAddr returns a dummy websocket address to satisfy net.Conn, see: wsAddr func (ws *WebSocket) LocalAddr() net.Addr { return wsAddr(ws.URL) } +//RemoteAddr returns a dummy websocket address to satisfy net.Conn, see: wsAddr func (ws *WebSocket) RemoteAddr() net.Addr { return wsAddr(ws.URL) } +//Write implements the standard io.Writer interface. Due to the JavaScript writes +// being internally buffered it will never block and a write timeout from a +// previous write may not surface until a subsequent write. func (ws *WebSocket) Write(buf []byte) (n int, err error) { //Check for noop writeCount := len(buf) @@ -190,7 +221,7 @@ func (ws *WebSocket) Write(buf []byte) (n int, err error) { ws.setDeadline(ws.writeDeadlineTimer, newWriteDeadline) case <-ws.writeDeadlineTimer.C: - if reamining := ws.ws.Get("bufferedAmount").Int(); reamining > 0 { + if remaining := ws.ws.Get("bufferedAmount").Int(); remaining > 0 { return 0, timeoutError{} } @@ -222,6 +253,7 @@ func (ws *WebSocket) Write(buf []byte) (n int, err error) { return writeCount, nil } +//Read implements the standard io.Reader interface (typical semantics) func (ws *WebSocket) Read(buf []byte) (int, error) { //Check for noop if len(buf) < 1 { @@ -303,6 +335,7 @@ func (ws *WebSocket) SetDeadline(future time.Time) (err error) { return nil } +//SetWriteDeadline implements the Conn SetWriteDeadline method func (ws *WebSocket) SetWriteDeadline(future time.Time) error { if debugVerbose { println("Websocket: Set write deadline for", future.String()) @@ -311,6 +344,7 @@ func (ws *WebSocket) SetWriteDeadline(future time.Time) error { return nil } +//SetReadDeadline implements the Conn SetReadDeadline method func (ws *WebSocket) SetReadDeadline(future time.Time) error { if debugVerbose { println("Websocket: Set read deadline for", future.String()) @@ -319,7 +353,7 @@ func (ws *WebSocket) SetReadDeadline(future time.Time) error { return nil } -//Only call from New or Read! +//setDeadline is used internally; Only call from New or Read! func (ws *WebSocket) setDeadline(timer *time.Timer, future time.Time) error { if !timer.Stop() { select { @@ -333,6 +367,7 @@ func (ws *WebSocket) setDeadline(timer *time.Timer, future time.Time) error { return nil } +//addHandler is used internall by the WebSocket constructor func (ws *WebSocket) addHandler(handler func(this js.Value, args []js.Value), event string) { jsHandler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { handler(this, args) @@ -346,6 +381,8 @@ func (ws *WebSocket) addHandler(handler func(this js.Value, args []js.Value), ev ws.cleanup = append(ws.cleanup, cleanup) } +//handleOpen is a callback for JavaScript to notify Go when the websocket is open: +// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onopen func (ws *WebSocket) handleOpen(_ js.Value, _ []js.Value) { if debugVerbose { println("Websocket: Open JS callback!") @@ -353,6 +390,8 @@ func (ws *WebSocket) handleOpen(_ js.Value, _ []js.Value) { close(ws.openCh) } +//handleClose is a callback for JavaScript to notify Go when the websocket is closed: +// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onclose func (ws *WebSocket) handleClose(_ js.Value, _ []js.Value) { if debugVerbose { println("Websocket: Close JS callback!") @@ -360,6 +399,8 @@ func (ws *WebSocket) handleClose(_ js.Value, _ []js.Value) { ws.ctxCancel() } +//handleError is a callback for JavaScript to notify Go when the websocket is in an error state: +// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onerror func (ws *WebSocket) handleError(_ js.Value, args []js.Value) { if debugVerbose { println("Websocket: Error JS Callback") @@ -375,11 +416,18 @@ func (ws *WebSocket) handleError(_ js.Value, args []js.Value) { } } +//handleMessage is a callback for JavaScript to notify Go when the websocket has a new message: +// See: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/onmessage func (ws *WebSocket) handleMessage(_ js.Value, args []js.Value) { if debugVerbose { println("Websocket: New Message JS Callback") } + select { + case <-ws.ctx.Done(): + default: + } + var rdr io.Reader var size int @@ -408,7 +456,7 @@ func (ws *WebSocket) handleMessage(_ js.Value, args []js.Value) { } select { - case ws.readCh <- rdr: + case ws.readCh <- rdr: //Try non-blocking queue first... if debugVerbose { println("Websocket: JS read callback sync enqueue") } diff --git a/wslistener.go b/wslistener.go index 9eedfdf..d0562f5 100644 --- a/wslistener.go +++ b/wslistener.go @@ -12,6 +12,8 @@ import ( "nhooyr.io/websocket" ) +//WebSockListener implements net.Listener and provides connections that are +//incoming websocket connections type WebSockListener struct { ctx context.Context ctxCancel context.CancelFunc @@ -21,6 +23,8 @@ type WebSockListener struct { var _ net.Listener = (*WebSockListener)(nil) +//NewWebSocketListener constructs a new WebSockListener, the provided context +//is for the lifetime of the listener. func NewWebSocketListener(ctx context.Context) *WebSockListener { ctx, cancel := context.WithCancel(ctx) wsl := &WebSockListener{ @@ -43,6 +47,8 @@ func NewWebSocketListener(ctx context.Context) *WebSockListener { return wsl } +//HTTPAccept is a method that is mean to be used as http.HandlerFunc to accept inbound HTTP requests +// that are websocket connections func (wsl *WebSockListener) HTTPAccept(wtr http.ResponseWriter, req *http.Request) { select { case <-wsl.ctx.Done(): @@ -67,6 +73,8 @@ func (wsl *WebSockListener) HTTPAccept(wtr http.ResponseWriter, req *http.Reques } } +//Accept fulfills the net.Listener interface and returns net.Conn that are incoming +// websockets func (wsl *WebSockListener) Accept() (net.Conn, error) { select { case conn := <-wsl.acceptCh: @@ -76,11 +84,13 @@ func (wsl *WebSockListener) Accept() (net.Conn, error) { } } +//Close closes the listener func (wsl *WebSockListener) Close() error { wsl.ctxCancel() return nil } +//RemoteAddr returns a dummy websocket address to satisfy net.Listener func (wsl *WebSockListener) Addr() net.Addr { return wsAddr{} }