WebSockets + Node.js server
This is part 2 in my article series about how I created “Pizza Peers”.
Haven’t read the other entries? Go to the devlog overview.
So, first things first, we need:
A server that hosts our game (and serves the game files)
A server that receives connections from both the players and the computer, so that it can connect them directly. (Once connected, the whole game goes via peer-to-peer.)
Obviously, we’ll use the same server for both these things.
IMPORTANT REMARK: In these articles, I will not show the full code (as it’s too complicated and specific, thus not so good for teaching or explaining). Instead, I give a template and some pseudo-code where needed. If you want to implement these things yourself, check out the source code for all the details and exceptions.
Node.js
For the server side, we’ll use Node.js. It is simple and small, it uses JavaScript (which we’ll also use in the game itself), and I have some experience with it.
Below is the template for this server. It simply sets up a server that servers both static files (which are the game files) and accepts websocket connections (which are needed to connect players with the computer).
1process.title = 'pizza-peers';
2var webSocketsServerPort = process.env.PORT || 8888;
3
4var WebSocketServer = require('websocket').server;
5var http = require('http');
6
7// stuff for creating an app that is a WEBSOCKET and also serves STATIC FILES
8var WebSocketServer = require('websocket').server;
9var express = require('express');
10var app = express();
11var server = app.listen(webSocketsServerPort, function() {
12 console.log((new Date()) + " Server is listening on port " + webSocketsServerPort);
13});
14
15// create the web socket server (using the HTTP server as a basis)
16var wsServer = new WebSocketServer({ httpServer : server });
17
18// this will make Express serve your static files (from the folder /public)
19app.use(express.static(__dirname + '/public'));
20
21// Global variable that contains all games currently being played!
22var rooms = {}
23
24// WebSocket server
25wsServer.on('request', function(request) {
26 // accept the connection
27 var connection = request.accept(null, request.origin);
28
29 // The most important callback: we'll handle all messages from users here.
30 connection.on('message', function(message) {
31 // @TODO: Any message sent to the server is handled here
32 });
33
34 // user connection closed
35 connection.on('close', function(connection) {
36 // if you want, you can use this space to delete games and free up space once a game has ended
37 });
38});
Of course, all the magic is going to happen in that “on(message)” block.
Peer to Peer
Which messages do we need to send? For that, we need to understand how peer-to-peer works.
Instead of creating a connection with the server, a peer connection is a direct connection between two devices. So, once established, your smartphone has a direct link with the computer (that hosts the game), and vice versa. This is what makes it so incredibly quick and easy.
However, we cannot allow devices to just establish connections as they please. That would not be very secure.
Instead, there’s a handshake protocol we need to follow:
Device A wants to connect with device B
A creates a peer (on their side) and sends out an offer
B receives the offer, creates its own peer, and formulates a response.
Once A has received and validated the response, both are officially connected.
For generating the offer and response signals, I use the simple-peer library. I don’t need to understand what it’s doing with those signals or what all the information means, and neither do you. I just pass the signals along.
Speaking about that: we’ve encountered an issue. A needs to send an offer to B. But … they are not connected yet. How is A going to find B?
That’s where our WebSockets come in!
Signaling server
In order to exchange signals, we turn our server into a so-called “signaling server”.
The idea is very simple:
A generates a signal and sends it to the server.
The server determines the intended recipient and relays the signal.
B receives it, creates a response, sends it back.
The server determines that the response must return to A.
And voila, peer to peer connection!
So, at our server, we need a way to receive and pass on messages.
In my case, this is even more simplified:
The player (with the smartphone) is always the initiator of the connection.
The computer (that hosts the game) is always the one responding.
See the code below for the basic structure of this system. (It will make even more sense once we’ve created the client side.)
1// WebSocket server
2wsServer.on('request', function(request) {
3 // accept the connection
4 var connection = request.accept(null, request.origin);
5
6 // create a persistent variable for our roomID; means we don't need to look it up all the time, which is faster
7 var roomID = -1;
8 var isServer = false;
9
10 // The most important callback: we'll handle all messages from users here.
11 connection.on('message', function(message) {
12
13 //
14 // if this message is a CREATE ROOM message
15 //
16 if(message.action == "createRoom") {
17 // generate a (non-existing) ID
18 var id = generateID();
19
20 // insert it into rooms (effectively creating a new room)
21 // each room simply knows which socket is the server, and which are the players
22 rooms[id] = {
23 "server": connection,
24 "players": [],
25 "gameStarted": false
26 };
27
28 // remember our roomID
29 roomID = id;
30
31 // rember we function as a server
32 isServer = true;
33
34 // beam the room number back to the connection
35 connection.sendUTF(JSON.stringify({ "type": "confirmRoom", "room": roomID }));
36
37 //
38 // if this message is a JOIN ROOM message
39 //
40 } else if(message.action == "joinRoom") {
41 // which room should we join?
42 var roomToJoin = message.room
43
44 // what is the player username?
45 var usn = message.username
46
47 // add this player to that room
48 rooms[roomToJoin].players[usn] = connection;
49
50 // remember we joined the room
51 roomID = roomToJoin;
52
53 // we want to connect with the server (peer-to-peer)
54 // for this, the player generates an "invitation" or "offer"
55 // it sends this along with the message
56 var offer = message.offer;
57
58 // append the USERNAME of the client extending the offer
59 // (otherwise, the server doesn't know to whom the response needs to be send)
60 offer.clientUsername = usn;
61
62 // now relay this offer to the server
63 rooms[roomToJoin].server.sendUTF(JSON.stringify(offer));
64
65 //
66 // if this message is an OFFER RESPONSE
67 //
68 } else if(message.action == "offerResponse") {
69 // get the client that should receive it
70 var receivingClient = message.clientUsername
71
72 // get the response
73 var offerResponse = message.response;
74
75 // send the response
76 rooms[roomID].players[receivingClient].sendUTF(JSON.stringify(offerResponse));
77 });
78});
In fact, this is almost all the code on the server. The only thing I added in the real game is more robust error handling and handling some extra cases/exceptions.
Client side - Websockets
So far, we’ve created the server only. It accepts web sockets and passes along signals … but we still need a client side to actually create those web sockets and signals.
I assume you know how to set up a basic HTML page. If not, check out index.html in my source code.
Then, make sure the JavaScript below is run (once the page has finished loading):
1// if user is running mozilla then use it's built-in WebSocket
2window.WebSocket = window.WebSocket || window.MozWebSocket;
3
4// assuming a https connection here, otherwise use "ws://"
5host = 'wss://' + location.host;
6
7// this creates the connection and keeps a reference to it
8var connection = new WebSocket(host);
9
10var curPeer = null;
11
12// if browser doesn't support WebSocket, just show some notification and exit
13if (!window.WebSocket) {
14 updateStatus('Sorry, but your browser doesn\'t support WebSocket.');
15 return;
16}
17
18connection.onopen = function () {
19 // connection is opened and ready to use
20};
21
22connection.onerror = function (error) {
23 // an error occurred when sending/receiving data, display error message
24};
25
26connection.onmessage = function (message) {
27 // parse message
28 message = JSON.parse(message.data);
29
30 // if it's a confirmation of the created game (room) ...
31 if(message.type == 'confirmRoom') {
32 // start the actual game (Phaser.io)
33 }
34
35 // if it's an OFFER ...
36 if(message.type == 'offer') {
37 // Create new peer; initiator = false (you'll learn about this later)
38 curPeer = createPeer(...);
39
40 // delete some info from the message (to save bandwidth)
41 delete message.clientUsername;
42
43 // put this signal into our peer (should be the last one we created)
44 // (when it has formulated an answer, it will automatically send that back)
45 curPeer.signal(message);
46 }
47
48 // if it's a RESPONSE to an offer
49 if(message.type == 'answer') {
50 // simply relay it to the peer
51 // we should be a player, who only has a single peer active
52 // (NOTE: if accepted, ALL communication henceforth should be peer-to-peer)
53 curPeer.signal(message);
54 }
55};
56
57// Listen to button for CREATING games
58document.getElementById("createGameBtn").addEventListener('click', function(ev) {
59 // Send a message to the websocket server, creating a game and opening ourselves to connections
60 var msg = { "action": 'createRoom' }
61 connection.send( JSON.stringify(msg) );
62});
63
64// Listen to button for JOINING games
65document.getElementById("joinGameBtn").addEventListener('click', function(ev) {
66 // Create peer; initiator = true (again, you'll learn about this soon)
67 // NOTE: Once the peer is done and it can start pairing, it will inform the websocket server
68 curPeer = createPeer(...);
69});
This bit of code opens the web socket connection, then listens for responses from the server. When necessary, it creates a new peer.
The “peer.signal(…)” bit will be explained soon. Essentially, we just pass the “signal message” directly into the peer and let it formulate its response. (Remember when I told you that I don’t know what’s inside those signals? Those things are put in here.)
NOTE: All messages are JSON. However, we cannot (and don’t want to) send objects over the internet. So, before sending, we must always stringify the object. At the receiving end, we always parse it, so it returns to the original JSON object.
Almost done …
All the code above still does not complete our system for connecting – so don’t try to run out – but we’re very close.
This is what we’ve achieved so far:
A server that servers our game files.
A server that accepts socket connections, gets messages, and then relays them to the right connection.
A client side that connects with the server, and also sends/receives the right messages, and creates the proper peers when needed.
All that’s left to do, is actually create the peers. For that, see you at part 3!