diff --git a/documentation/network/network.md b/documentation/network/network.md index 5e7ec53eb1..2b127e414a 100644 --- a/documentation/network/network.md +++ b/documentation/network/network.md @@ -1,7 +1,7 @@ # Networking Copyright 2017-2022 Moddable Tech, Inc.
-Revised: February 14, 2022 +Revised: March 8, 2022 **Warning**: These notes are preliminary. Omissions and errors are likely. If you encounter problems, please ask for assistance. @@ -675,7 +675,7 @@ The WebSocket server implementation is designed for sending and receiving small ### `constructor(dictionary)` -A new WebSocket `Server` is configured using a dictionary of properties. The dictionary is a super-set of the `Listener` dictionary. The server is a Socket Listener. If no port is provided in the dictionary, port 80 is used. +A new WebSocket `Server` is configured using a dictionary of properties. The dictionary is a super-set of the `Listener` dictionary. The server is a Socket Listener. If no port is provided in the dictionary, port 80 is used. If port is set to `null`, no listener is created which is useful when sharing a listener with an http server (see `attach` below). At this time, the WebSocket `Server` does not define any additional properties for the dictionary. @@ -697,6 +697,14 @@ ws.close(); *** +### `attach(socket)` + +The `attach` function creates a new incoming WebSockets connection from the provided socket. The server issues the `Server.connect` callback and then performs the WebSockets handshake. The status line has been read from the socket, but none of the HTTP headers have been read as these are required to complete the handshake. + +See the [httpserverwithwebsockets](../../examples/network/http/httpserverwithwebsockets/main.js) for an example of sharing a single listener socket between the HTTP and WebSockets servers. + +*** + ### `callback(message, value)` The WebSocket server callback is the same as the WebSocket client callback with the addition of the "Socket connected" (`1` or `Server.connect`) message. The socket connected message for the server is invoked when the server accepts a new incoming connection. diff --git a/examples/network/http/httpserverwithwebsockets/main.js b/examples/network/http/httpserverwithwebsockets/main.js new file mode 100644 index 0000000000..f0984bb446 --- /dev/null +++ b/examples/network/http/httpserverwithwebsockets/main.js @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-2022 Moddable Tech, Inc. + * + * This file is part of the Moddable SDK. + * + * This work is licensed under the + * Creative Commons Attribution 4.0 International License. + * To view a copy of this license, visit + * . + * or send a letter to Creative Commons, PO Box 1866, + * Mountain View, CA 94042, USA. + * + */ + +import {Server as HTTPServer} from "http" +import {Server as WebsocketsServer} from "websocket" + +const indexHTML = ` + + + + test + + + + + + +`; + +const wsJS = ` +const url = "ws://" + window.location.hostname + "/ws"; +const ws = new WebSocket(url); + +ws.onopen = function() { + console.log("ws open"); + ws.send(JSON.stringify({"hello": "world"})); +} + +ws.onclose = function() { + console.log("ws close"); +} + +ws.onerror = function() { + console.log("ws error"); +} + +ws.onmessage = function(event) { + const data = event.data; + console.log("ws message: ", data); +} +`; + +// WebSockets server without a listener (signaled with port set to null) +// the http server hands-off incoming connections to this server +const websockets = new WebsocketsServer({port: null}); +websockets.callback = function (message, value) { + switch (message) { + case WebsocketsServer.connect: + trace("ws connect\n"); + break; + + case WebsocketsServer.handshake: + trace("ws handshake\n"); + break; + + case WebsocketsServer.receive: + trace(`ws message received: ${value}\n`); + break; + + case WebsocketsServer.disconnect: + trace("ws close\n"); + break; + } +}; + +// HTTP server on port 80 +const http = new HTTPServer; +http.callback = function(message, value, etc) { + if (HTTPServer.status === message) { + this.path = value; + + // request for "/ws" is handed off to the WebSockets server + if ("/ws" === value) { + const socket = http.detach(this); + websockets.attach(socket); + } + + return; + } + + // return "/", "/index.html" and "/ws.js". all other paths are 404 + if (HTTPServer.prepareResponse === message) { + if (("/" === this.path) || ("/index.html" === this.path)) + return {headers: ["Content-type", "text/html"], body: indexHTML}; + if ("/ws.js" === this.path) + return {headers: ["Content-type", "text/javascript"], body: wsJS}; + + return {status: 404}; + } +} diff --git a/examples/network/http/httpserverwithwebsockets/manifest.json b/examples/network/http/httpserverwithwebsockets/manifest.json new file mode 100644 index 0000000000..d09d28bd30 --- /dev/null +++ b/examples/network/http/httpserverwithwebsockets/manifest.json @@ -0,0 +1,13 @@ +{ + "include": [ + "$(MODDABLE)/examples/manifest_base.json", + "$(MODDABLE)/examples/manifest_net.json", + "$(MODULES)/network/http/manifest.json", + "$(MODULES)/network/websocket/manifest.json" + ], + "modules": { + "*": [ + "./main" + ] + } +} diff --git a/modules/network/websocket/websocket.js b/modules/network/websocket/websocket.js index 0b553eeaf5..808f2e64c9 100644 --- a/modules/network/websocket/websocket.js +++ b/modules/network/websocket/websocket.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 Moddable Tech, Inc. + * Copyright (c) 2016-2022 Moddable Tech, Inc. * * This file is part of the Moddable SDK Runtime. * @@ -29,6 +29,7 @@ import {Socket, Listener} from "socket"; import Base64 from "base64"; import Logical from "logical"; import {Digest} from "crypt"; +import Timer from "timer"; /* state: @@ -56,6 +57,7 @@ export class Client { this.headers = dictionary.headers ?? []; this.protocol = dictionary.protocol; this.state = 0; + this.flags = 0; if (dictionary.socket) this.socket = dictionary.socket; @@ -104,6 +106,10 @@ export class Client { close() { this.socket?.close(); delete this.socket; + + if (this.timer) + Timer.clear(this.timer); + delete this.timer; } }; @@ -287,23 +293,38 @@ trace("partial header!!\n"); //@@ untested export class Server { #listener; constructor(dictionary = {}) { + if (null === dictionary.port) + return; + this.#listener = new Listener({port: dictionary.port ?? 80}); this.#listener.callback = () => { - const socket = new Socket({listener: this.#listener}); - const request = new Client({socket}); - request.doMask = false; - socket.callback = server.bind(request); - request.state = 1; // already connected socket - request.callback = this.callback; // transfer server.callback to request.callback + const request = addClient(new Socket({listener: this.#listener}), 1, this.callback); request.callback(Server.connect, this); // tell app we have a new connection }; } close() { - this.#listener.close(); + this.#listener?.close(); this.#listener = undefined; } + attach(socket) { + const request = addClient(socket, 2, this.callback); + request.timer = Timer.set(() => { + delete request.timer; + request.callback(Server.connect, this); // tell app we have a new connection + socket.callback(2, socket.read()); + }); + } }; +function addClient(socket, state, callback) { + const request = new Client({socket}); + delete request.doMask; + socket.callback = server.bind(request); + request.state = state; + request.callback = callback; // transfer server.callback to request.callback + return request; +} + /* callback for server handshake. after that, switches to client callback */