browser-to-browser websocket tunnels with Caffeine and livecoded NodeJS
In our previous look at livecoding NodeJS from Caffeine, we implemented tweetcoding. Now let’s try another exercise, creating WebSockets that tunnel between web browsers. This gives us a very simple version of peer-to-peer networking, similar to WebRTC.
Once again we’ll start with Caffeine running in a web browser, and a NodeJS server running the node-livecode package. Our approach will be to use the NodeJS server as a relay. Web browsers that want to establish a publicly-available server can register there, and browser that want to use such a server can connect there. We’ll implement the following node-livecode instructions:
- initialize, to initialize the structures we’ll need for the other instructions
- create server credential, which creates a credential that a server browser can use to register a WebSocket as a server
- install server, which registers a WebSocket as a server
- connect to server, which a client browser can use to connect to a registered server
- forward to client, which forwards data from a server to a client
- forward to server, which forwards data from a client to a server
In Smalltalk, we’ll make a subclass of NodeJSLivecodingClient called NodeJSTunnelingClient, and give it an overriding implementation of configureServerAt:withCredential:, for injecting new instructions into our NodeJS server:
configureServerAt: url withCredential: credential "Add JavaScript functions as protocol instructions to the node-livecoding server at url, using the given credential." ^(super configureServerAt: url withCredential: credential) addInstruction: 'initialize' from: ' function () { global.servers = [] global.clients = [] global.serverCredentials = [] global.delimiter = ''', Delimiter, ''' return ''initialized tunnel relay''}'; invoke: 'initialize'; addInstruction: 'create server credential' from: ' function () { var credential = Math.floor(Math.random() * 10000) serverCredentials.push(credential) this.send((serverCredentials.length - 1) + '' '' + credential) return ''created server credential''}'; addInstruction: 'install server' from: ' function (serverID, credential) { if (serverCredentials[serverID] == credential) { servers[serverID] = this this.send(''1'') return ''installed server''} else { debugger; this.send(''0'') return ''bad credential''}}'; addInstruction: 'connect to server' from: ' function (serverID, port, req) { if (servers[serverID]) { clients.push(this) servers[serverID].send(''connected:atPort:for: '' + (clients.length - 1) + delimiter + port + delimiter + req.connection.remoteAddress.toString()) this.send(''1'') return ''connected client''} else { this.send(''0'') return ''server not connected''}}'; addInstruction: 'forward to client' from: ' function (channel, data) { if (clients[channel]) { clients[channel].send(''from:data: '' + servers.indexOf(this) + delimiter + data) this.send(''1'') return ''sent data to client''} else { this.send(''0'') return ''no such client channel''}}'; addInstruction: 'forward to server' from: ' function (channel, data) { if (servers[channel]) { servers[channel].send(''from:data: '' + clients.indexOf(this) + delimiter + data) this.send(''1'') return (''sent data to server'')} else { this.send(''0'') return ''no such server channel''}}'
We’ll send that message immediately, configuring our NodeJS server:
NodeJSTunnelingClient configureServerAt: 'wss://yourserver:8087' withCredential: 'shared secret'; closeConfigurator
On the NodeJS console, we see the following messages:
server: received command 'add instruction' server: adding instruction 'initialize' server: received command 'initialize' server: evaluating added instruction 'initialize' server: initialized tunnel relay server: received command 'add instruction' server: adding instruction 'create server credential' server: received command 'add instruction' server: adding instruction 'install server' server: received command 'add instruction' server: adding instruction 'connect to server' server: received command 'add instruction' server: adding instruction 'forward to client' server: received command 'add instruction' server: adding instruction 'forward to server'
Now our NodeJS server is a tunneling relay, and we can connect servers and clients through it. We’ll make a new ForwardingWebSocket class hierarchy:
Object ForwardingWebSocket ForwardingClientWebSocket ForwardingServerWebSocket
Instances of ForwardingClientWebSocket and ForwardingServerWebSocket use a NodeJSTunnelingClient to invoke our tunneling instructions.
We create a new ForwardingServerWebSocket with newThrough:, which requests new server credentials from the tunneling relay, and uses them to install a new server. Another new class, PeerToPeerWebSocket, provides the public message interface for the framework. There are two instantiation messages:
- toPort:atServerWithID:throughURL: creates an outgoing client that uses a ForwardingClientWebSocket to connect to a server and exchange data
- throughChannel:of: creates an incoming client that uses a ForwardingServerWebSocket to exchange data with a remote outgoing client.
Incoming clients are used by ForwardingServerWebSockets to represent their incoming connections. Each ForwardingServerWebSocket can provide services over a range of ports, as a normal IP server would. To connect, a client needs the websocket URL of the tunneling relay, a port, and the server ID assigned by the relay.
As usual, you can examine and try out this code by clearing your browser’s caches for caffeine.js.org (including IndexedDB), and visiting https://caffeine.js.org/. With browsers able to communicate directly, there are many interesting things we can build, including games, chat applications, and team development tools. What would you like to build?
6 July 2017 at 6:17 pm
Is this significantly simpler than webrtc? I can see that it might be a better avenue to a more or less hub and spoke mechanism for more than two users, but I do lean toward the P2P aspect of webrtc.
LikeLike
6 July 2017 at 7:27 pm
I think it’s much simpler than WebRTC to implement and to use (just open a websocket and start sending instructions, no ICE, no STUN, no TURN). It’s also livecoded, which isn’t so easy with WebRTC. I don’t mean for it to be a replacement, though. It’s just another exercise in livecoding NodeJS. Thanks for checking it out!
LikeLike
24 July 2017 at 9:41 pm
[…] to deliver. With generalized messaging connectivity to the DOM of every page in a web browser, and with other web browsers, we have a far more powerful editing medium. Web applications are dynamic media when people are […]
LikeLike