Building multiplayer HTML5 games – Part 2: WebSockets
In the last post we discussed how digital multiplayer games have evolved, various strategies for getting different game clients to keep their games in sync, and why the core HTTP request/response architecture of the web was fundamentally flawed when it came to multiplayer games. In this post, we’ll dig into the HTML5 solution to this problem and how to use it in desktop or mobile web applications.
WebSockets are a new technology that was written in response to JavaScript developers using AJAX calls to keep checking back with the server to see if anything new had come in. It was clear that the current design was not meeting the needs of modern Web 2.0 applications, which wanted to leave a connection with the server open for as long as the web page was up so that any new information from the server could instantly be read by the JavaScript running on the client and displayed.
At the lowest levels, WebSockets do piggyback on top of the World Wide Web infrastructure (hence the name). They follow a URL scheme, similar to the ubiquitous HTTP definition. Since the underlying protocol is different from HTTP, however, the URL scheme begins instead with a new label: ‘ws‘ (for unencrypted streams) and ‘wss‘ (for encrypted streams). For example:
ws://yourserver.com/yourwebsocketresource
If you’re running your own web servers, you’ll need to have a program running on the backend to handle WebSocket requests such as these. Apache’s ActiveMQ project is one example of WebSocket-aware server software. A full discussion of the pros and cons of various server configurations and software packages to handle WebSockets is beyond the scope of this post.
WebSocket technology is new enough that browser support when running your client HTML5 application should not be assumed. To assist in this, there are several JavaScript libraries that wrap the WebSocket calls in their own API and thus can downgrade the connection to an HTTP polling connection if the client application is running on a browser that does not support WebSockets, and we’ll dig into one of those wrappers momentarily. In the HTML5 mobile space, WebSockets were added to Mobile Safari in iOS 4.2 (though the WebSocket spec supported is an out-of-date specification, so make sure your servers are compatible with the old spec if you want to support iOS devices), but as of this writing are not supported on the default Android browser. WebSockets are supported in the newly-announced Chrome for Android beta, which only runs on Android 4.0.
If you are running in an environment that supports WebSockets, interacting with one on the client side is delightfully simple. The code below shows how to set up a socket and use that socket to both read and write messages:
// Create the connection to the server var socket = new WebSocket("ws://mywebserver.com/mysocketresource"); // Set up a callback to run when a message comes in from the server socket.onmessage = function(msg){ alert('Socket received a message: ' + msg.data); }; // This function will send a message to the server function send(msg){ if(socket.readyState === 1) { // 'open' socket.send(msg); } else { alert("Socket can't send the message right now, state is: " + socket.readyState); } } // Closing the socket is trivial function close(){ socket.close(); }
I should warn you, I’ve never tested the above code. The reason why is a little convoluted, but a necessary story. To begin with, my company, 10×10 Room, loves cloud technology. We love the fact that we don’t have to manage our own web servers, database hardware, load balancers, etc etc. We use Heroku as our polyglot server, and they have been awesome.
Here’s the thing, though. Heroku doesn’t support WebSockets (yet). Neither does, near as I could tell, any other cloud service–directly. There are several cloud services that support WebSockets indirectly, though. The one that I’ve been using for this research as been a company called Pusher. If you subscribe to Pusher, they will handle the WebSocket hardware for you, and as you’ll see in the next post, it works great. But, Pusher is not simply a WebSocket server. They’ve wrapped themselves in a whole architecture, and if you’re in for part of it, you’re in for all of it:
The Pusher API expects new messages primarily to be coming from your web server, and Pusher will dutifully broadcast those messages to any other HTML5 apps that have a Pusher connection open to the same channel. The idea is that if your game client wants to let the rest of the world know about a game update, your client is not the true source of that message. Remember from last time that the game client itself is untrusted and–particularly in JavaScript–easily hackable. Instead, the client sends its message back to the game server via a traditional HTTP message. The server verifies that the message is reasonable and doesn’t break any rules, then the server tells Pusher it’s okay to broadcast the message along to the other clients.
The Pusher API is slightly different from the raw WebSocket API:
// Create a Pusher object, using the application ID assigned to you by Pusher var pusher = new Pusher([application id], {encrypted: true}); // Tell Pusher to subscribe your client app to a channel you've defined var channel = pusher.subscribe('test-channel'); // The 'pusher:subscription_succeeded' or 'pusher:subscription_error' event // callbacks will be called to notify your app if the subscription succeeded // or failed. channel.bind('pusher:subscription_succeeded', function() { alert("Got a subscription_succeeded event from the server"); }); channel.bind('pusher:subscription_error', function(value) { alert("Got a subscription_error event from the server: " + value); }); // Bind to a certain type of event you've defined that can fire on this // channel. The callback will be called whenever this event comes in from // the Pusher server. channel.bind('testEvent', function(data) { alert("Got a testEvent from test-channel! Data is: "+data.value); });
Note that there is no code to send a message in the above. To send a message, rather than sending it directly from the client to Pusher over the Pusher socket, you are generally expected to use traditional AJAX (or an HTTP request to a new web page) as the mechanism to push a message up to your server, then use the server API to send that message along to Pusher. Pusher does provide direct client-to-client messaging, but that API is in beta and (at least currently) still requires you to have a server involved to authorize any client subscription to a particular channel.
A great bonus feature of using a WebSocket wrapper like Pusher, as briefly touched on above, is that if your app is running on a web browser that does not support WebSockets, Pusher will fall back to HTTP polling under the hood to fetch any message updates waiting for your client. You don’t need to worry about whether Pusher is actually using WebSockets or not as the message delivery behavior (at least on the client side) will be the same.
So! Congratulations, you now have a path directly from one HTML5 web application to any other client running your web application, albeit with a proxy server or two to help out. How much do those proxy servers slow you down? We’ll show off responsiveness in the next update with the blog’s first ever vlog entry. This is so cool, I can’t wait to show it off.
-
May 9, 2012 at 8:38 AMWebSocket Latency Unveiled, News at 11 « archmagus