Skip to content

Commit

Permalink
WebSocket server creates new connection from existing socket #755
Browse files Browse the repository at this point in the history
  • Loading branch information
phoddie committed Mar 8, 2022
1 parent 90ee606 commit 72c9f34
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 10 deletions.
12 changes: 10 additions & 2 deletions documentation/network/network.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Networking

Copyright 2017-2022 Moddable Tech, Inc.<BR>
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.

Expand Down Expand Up @@ -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.

Expand All @@ -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.
Expand Down
101 changes: 101 additions & 0 deletions examples/network/http/httpserverwithwebsockets/main.js
Original file line number Diff line number Diff line change
@@ -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
* <http://creativecommons.org/licenses/by/4.0>.
* 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 = `
<html>
<head>
<meta charset="utf-8">
<title>test</title>
</head>
<body>
<script type="module" src="./ws.js"></script>
</body>
</html>
`;

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};
}
}
13 changes: 13 additions & 0 deletions examples/network/http/httpserverwithwebsockets/manifest.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
37 changes: 29 additions & 8 deletions modules/network/websocket/websocket.js
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -104,6 +106,10 @@ export class Client {
close() {
this.socket?.close();
delete this.socket;

if (this.timer)
Timer.clear(this.timer);
delete this.timer;
}
};

Expand Down Expand Up @@ -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
*/
Expand Down

0 comments on commit 72c9f34

Please sign in to comment.