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?