9. WebSockets

The Hypertext Transfer Protocol (HTTP) is just great. Together with FTP, SMTP, IMAP, and many others, it is part of the large family of text-based protocols executed in the TCP/IP Application Layer. In these protocols, client and server communicate via messages in text form. The following listing demonstrates how easy it is to “speak” HTTP with a web server:

user@host:~> telnet www.google.com 80
Trying 209.85.135.103...
Connected to www.l.google.com.
Escape character is '^]'.
GET /search?q=html5 HTTP/1.0

To run a Google search for the term html5, we first connect to www.google.com on the port reserved for HTTP, port 80. The request has three parts: In the first part GET determines the method for the request; in this case we want to get information from the server. The second part is the URI; here, we call the script search with the parameter q=html5. In the third part we specify the protocol version 1.0.

The server promptly replies with the following information:

HTTP/1.0 200 OK
Cache-Control: private, max-age=0
Date: Fri, 28 Jan 2011 08:29:43 GMT
Expires: -1
Content-Type: text/html; charset=ISO-8859-1
...

<!doctype html><head><title>html5 - Google Search</title>
....

The first block of the message, the header, contains meta information separated by an empty line from the following payload data (note how Google is already using the new DOCTYPE). This gives us almost everything we need to program our very own browser. Joking aside, the simplicity of the protocol is decisive for the quick success and widespread use of HTTP. The header lines are nearly endlessly expandable, making the protocol future-proof.

Each request is a closed issue after it has been answered. So, an HTML page referencing a stylesheet and five images needs seven connections to load. This means that a connection is established seven times, and each time metadata and payload is transmitted. In version 1.1 of HTTP, this behavior was somewhat improved by the keepalive function (new TCP connections do not need to be created every time), but the meta information for each object is transmitted separately. To track a user’s session, you need to resort to other tools (sessions, cookies), because HTTP has not integrated this function.

These considerations provide the starting points for developing a new protocol, which is by no means meant to replace HTTP but can complement it. The WebSockets protocol transports data without meta information in a constant stream, simultaneously from the server to the client and vice versa (full duplex).

Web applications that promptly show small changes in the browser can especially profit from this new method. Examples of such applications are a chat program, the display of stock exchange prices, or online games. What was previously only possible via proprietary plug-ins or unpleasant JavaScript tricks is now codified in a standardized protocol (as an IETF draft) and an associated API (currently as an Editor’s Draft with the W3C). Both were still in a very early stage at the time of this writing; however, both the WebKit engine (and thus Google Chrome and Safari) and the Beta version of Mozilla Firefox contain a functioning implementation.

We do not want to penetrate the depths of the WebSocket protocol, because the communication on the protocol level is taken care of by the browser anyway. However, we will provide a few comments to clarify: Although an HTTP request involves sending several header lines back and forth, WebSockets only use two bytes for this. The first byte shows the start of a message; the second contains the length of the message. This is a saving (of bytes transferred and bandwidth) with dramatic consequences when you have to manage many users accessing the site at short intervals.


Note

image

If you want to know more about the details of the WebSocket protocol, read the relevant Internet Draft on the WHATWG website at http://www.whatwg.org/specs/web-socket-protocol.


Interesting statistics regarding the advantages of WebSockets in different applications can be found in the article at http://soa.sys-con.com/node/1315473: The authors even go so far as to refer to WebSockets as a quantum leap in scalability for the web.

9.1 The WebSocket Server

Client-side support for WebSockets is integrated in modern browsers. But we are still missing one component: the WebSocket server. Although the protocol specification is not set in stone at the moment, there is already a surprising selection of software products available. You can choose a server depending on your preference, be it Java, PHP, Perl, or Python (of course, all products are still in the test stage).

For this book, we chose a special solution. With node.js is a JavaScript interpreter capable of running without a browser. The code developed by Google under the name V8 is working in the background. Because we used JavaScript exclusively for all previous programming in this book, it made sense to write the server using JavaScript as well.

There are currently no finished binary packets of node.js, so the installation requires some manual work. With UNIX-type operating systems, the installation is usually straightforward; for Windows, you still have to resort to the UNIX emulation cygwin.


Note

image

For a more detailed description regarding the installation of node.js, see the project’s website at http://nodejs.org.


node.js does not yet contain a WebSocket server, but help is available on the Internet. At http://github.com/miksago/node-websocket-server, you will find a small library implementing the current specification of the WebSocket protocol for the server. The three JavaScript files of the node-websocket-server are simply copied into a subdirectory and loaded with the following lines:

var ws = require(__dirname + '/lib/ws'),
    server = ws.createServer();

From this point on, the variable server contains a reference to the WebSocket server object. We still need to specify a port for the server:

server.listen(8887);

To start the server, we call the JavaScript file with the node.js interpreter:

node mini_server.js

Our minimal WebSocket server is now running and accepts connections at port 8887. But that is all our server can do for the moment. A more sensible application is developed in the following example, which we will use to investigate the individual components in more detail.

9.2 Example: A Broadcast Server

For our first little example, we want to communicate with a WebSocket that transmits entered text to all clients with an active connection to the socket. This is not a real Internet chat application but is well suited to the purpose of testing the interactivity of WebSockets. Figure 9.1 shows how four interconnected clients exchange messages with each other.

Figure 9.1 Four connections to the WebSocket broadcast server

image

9.2.1 The Broadcast Client

In the HTML code we just need a text field for entering the message to be sent. To demonstrate the capabilities of WebSocket, we want every character to be sent to all connected users immediately. To achieve this, we use the oninput event of the text field and call the sendmsg() function for each keystroke, which we will analyze later on:

<h2>Broadcast messages</h2>
<textarea accesskey=t oninput="sendmsg();"
  onfocus="select()" rows=5 cols=40 id=ta
  placeholder="Please insert your message">
</textarea>
<div id=broadcast></div>
<p id=status><p id=debug>

The JavaScript section starts with a definition of the previously encountered $() function, loaned from the jQuery library. As soon as the whole document is loaded, the WebSocket is initialized and assigned to the variable ws. In our example, we use the server html5.komplett.cc on the special port 8887. The server is specified via a URL, and the protocol is abbreviated with ws://. Similar to the SSL encoded HTTPS, there is also an encoded channel for WebSockets, called with wss:// as the protocol. For our example, we stick with the uncoded variation. The path specified in the URL (/bc) is not relevant for our WebSocket server, because the server on this port has the sole purpose of serving this example (more about the server in section 9.2.2, The Broadcast Server):

function $(a) { return document.getElementById(a); }

var ws, currentUser, ele;
window.onload = function() {
ws = new WebSocket("ws://html5.komplett.cc:8887/bc");
ws.onopen = function() {
  $("status").innerHTML = 'online';
  $("status").style.color = 'green';
  ws.onmessage = function(e) {
    var msg;
    try {
      msg = JSON.parse(e.data);
    } catch (SyntaxError) {
      $("debug").innerHTML = "invalid message";
      return false;
    }

If the connection is successfully established, the WebSocket’s onopen event is activated. The anonymous function in our example writes the string online in green into a status line at the end of the HTML document. For each message received by the WebSocket, it activates the onmessage event. The data attribute of the variable e available for this function contains the payload sent by the server. In our example, the data is converted into a JavaScript object via JSON.parse, which means that the server has to send a JSON string (details on this will follow later in this section). If the conversion is unsuccessful, the function is terminated and an appropriate error message appears on the HTML page.

A valid message contains a JavaScript object with user name (user), message text (text), and the color in which the message is to be displayed (color). As you can see in Figure 9.1, each user is writing on his own line and in his own color. The server assigns colors to users; assigning a new line is implemented on the client. The subsequent if query checks if the last message is from the same user as the previously received message. If that is the case, the innerHTML value of the variable ele is assigned the received text. If it is a different user or this is the first message, a new paragraph with the name ele is created and added to the div element with the ID broadcast. The variable currentUser is then set to the value of the current user:

    if (currentUser == msg.user) {
      ele.innerHTML = msg.text;
    } else {
      ele = document.createElement("p");
      $("broadcast").appendChild(ele);
      ele.style.color = msg.color;
      ele.innerHTML = msg.text;
      currentUser = msg.user;
    }
  };
};
function sendmsg() {
  ws.send($("ta").value);
}
ws.onclose = function(e){
  $("status").innerHTML = 'offline';
  $("status").style.color = 'red';
};
window.onunload = function(){
  ws.close();
};

The sendmsg() function fired with every keystroke within the text field sends the entire content of the text field to the WebSocket.

If the connection to the WebSocket is terminated for any reason (for example, due to an absent network connection or server problems), the WebSocket object fires the close event and consequently the onclose function. In our example, we set the status line to offline in red. When leaving the site (window.onunload), we explicitly close the WebSocket, logging out of the server.

9.2.2 The Broadcast Server

To complete the example, we still need the server component. As mentioned earlier, we use the node.js runtime and the node-websocket-server for the WebSocket examples in this book. This makes sense didactically because we do not need to switch to another programming language. After all, the server code is meant to be easily understandable for you as well.

Similar to the client, the server works based on events. Each established connection and each received message fires a connection or message event, respectively, to which we react in the JavaScript code. At the beginning of the script, we load the node-websocket-server library, located in the directory lib/ under the name ws.js. A new WebSocket object is assigned to the variable server:

var ws = require(__dirname + '/lib/ws'),
    server = ws.createServer();
var user_cols = {};
server.addListener("connection", function(conn) {
  var h = conn._server.manager.length*70;
  user_cols[conn.id] = "hsl("+h+",100%,30%)";
  var msg = {};
  msg.user = conn.id;
  msg.color = user_cols[conn.id];
  msg.text = "<em>A new user has entered the chat</em>";
  conn.broadcast(JSON.stringify(msg));

The first event handler (connection) handles the new connections. As in Chapter 8 in the section 8.5, Example: Click to tick!, we assign the color for the user step by step in HSL, jumping ahead for each new user by 70 degrees (the number of users can be retrieved via the array conn._server.manager). The colors are saved in the variable user_cols with the connection ID (conn.id) as an index. The variable msg is furnished with the created color and the notification that a new user has entered; then it is sent as a JSON string via the method conn.broadcast. This method is a function of the node-websocket-server and broadcasts messages to all clients except the one who fired the current event, which is exactly what we want in this case: All users are informed that a new user has entered the chat:

  conn.addListener("message", function(message) {
    var msg = {};
    message = message.replace(/</g, "&lt;");
    message = message.replace(/>/g, "&gt;");
    msg.text = message;
    msg.user = conn.id;
    msg.color = user_cols[conn.id];
    conn.write(JSON.stringify(msg));
    conn.broadcast(JSON.stringify(msg));
  });
});

The second function reacting to the message event replaces the start and end characters for HTML tags in the passed string (message) to ensure that no script code or similar tricks can be smuggled in. A reliable application would have to check input even more thoroughly to protect against possible attacks. After all, the message is broadcast to all clients and displayed in their browsers, a nearly ideal attack scenario. As in the connection event, a local variable msg is filled with the desired content and sent as a JSON string. But here, it happens twice: first with the write() method to the actual user and then with the broadcast() method to all other users.

The WebSocket server is almost finished. We are still missing an event handler for closed connections and the actual start of the server:

server.addListener("close", function(conn) {
  var msg = {};
  msg.user = conn.id;
  msg.color = user_cols[conn.id];
  msg.text = "<em>A user has left the chat</em>";
  conn.broadcast(JSON.stringify(msg));
});
server.listen(8887);

As with the connection event, all users receive a message in the close event as well. In this case, they are told that a user has left the conference. Then the server is bound to port 8887 and receives queries from that point on.

That was an initial, very brief example. In the next section we will develop a game that makes even better use of the advantages of WebSockets.

9.3 Example: Battleships!

A more detailed websocket example is devoted to a popular strategy game for which you would normally only need paper and pencil—Battleships! The rules are easy to explain: Each player places ten ships of different sizes on a play area sized ten-by-ten spaces. The ships are not allowed to be touching, can be arranged horizontally or vertically, and are two to five spaces long. You distribute them following the rule: 1×5, 2×4, 3×3, and 4×2 spaces per ship. The player who is first to finish arranging his or her ships can start the game by choosing one of the opponent’s spaces. If the space chosen contains only water, it’s the opponent’s turn next; if that space contains a ship or part of a ship, the player can keep guessing. You continue in this way until all parts of all ships have been hit, and the ships have been sunk.

For converting Battleships! to HTML5, we require an HTML file on the client with a JavaScript library and a CSS stylesheet; on the server we use node-websocket-server, which we already mentioned in section 9.1, The WebSocket Server. All files relevant for the application can be found on the companion website at these links:

http://html5.komplett.cc/code/chap_websockets/game_en.html

http://html5.komplett.cc/code/chap_websockets/game_en.js

http://html5.komplett.cc/code/chap_websockets/game.css

http://html5.komplett.cc/code/chap_websockets/ws/game_server.js

The game is shown in Figure 9.2.

Figure 9.2 The game “Battleships!” in action

image

In the HTML code, control elements and game dialogs are defined as form elements, visible or hidden depending on the game phase. Four of them are message windows, displayed centered with position:fixed as an invitation to play, rejection of the invitation, and congratulations or commiserations at the end of the game. The other forms contain the login mask, two game areas for the player’s own and the opponent’s ships, a digitalization component for placing the ships in the desired orientation, plus a list of currently logged-in users and their status.

On loading the page, the login mask appears and you are asked to enter your nickname (see Figure 9.3). Two special users are available for testing the application, test1 and test2, for which the ships are positioned automatically, and player test1 can always start the game. The auxiliary page test_game.html is a good way to observe the game from both players’ viewpoints. Here you can log in under two different user names via embedded iframe elements, so you can play against yourself, as it were. The advantage is that you always win and are able to follow the application’s game logic more easily. This testing page can be found at http://html5.komplett.cc/code/chap_websockets/game_test_en.html.

Figure 9.3 Start page of the “Battleships!” game

image

If you click OK after logging in, the connection to the WebSocket server is created. Its tasks are limited to exchanging messages between players and updating the user list. The user list shows each user with a connection ID, nickname, and current game status.

All messages are sent as JSON strings and fall roughly into two categories: The first one comprises messages sent to all users, concerning changes in the game status of individual players. The second category comprises private messages exchanged only between users currently playing a game together. For this purpose, we had to add the additional method writeclient() to the connection library of the node-websocket-server, passing messages only to the desired user.

Immediately after login, your own game area appears. Just as the opponent’s game area, it consists of ten-by-ten button elements whose value attributes reflect the grid position and have values between 1,1 (top left) and 10,10 (bottom right). Each button has a class attribute that can be changed several times in the course of the game. The CSS stylesheet contains the classes presented in Table 9.1 (relevant to the gameplay).

Table 9.1 Gameplay-related CSS classes in the game area

image

Before we can start digitalizing the ships, we need to find a partner so we can play. This is done by selecting a player from the list of logged-in users and clicking the button Invite Player to send that user an invitation to play. The callback function of this button locates the player ID and sends an invitation message to the WebSocket server:

this.invitePlayer = function() {
  var opts = document.forms.loggedin.users.options;
  if (opts.selectedIndex != -1) {
    wsMessage({
      task : 'private',
      request : 'invite',
      client : opts[opts.selectedIndex].value
    });
  }
};

The called function wsMessage() directs the message in JSON format to the server. It can also contain additional steps, such as checking the validity of the message or similar steps:

var wsMessage = function(msg) {
  game.websocket.send(JSON.stringify(msg));
};

The variable game in this code listing represents the central game object, and contains all variables relevant to the game. On the server, the invitation is identified as a private message, the sender’s data is added, and then the message is sent to the selected player.

With the server in game_server.js, it would look like this:

else if (msg.task == 'private') {
  msg.from = USERS[conn.id];
  conn.writeclient(JSON.stringify(msg),msg.client);
}

This user is presented with a little window asking to play a game with you (see Figure 9.4). If the user declines, you receive the answer No thanks, not now; if the user accepts, the user list is hidden and the digitalization component for placing the ships is displayed. Let’s first look at the code for inviting someone to play. On the client, we see it as part of the onmessage callback function for all server messages:

game.websocket.onmessage = function(e) {
  var msg = JSON.parse(e.data);
  if (msg.request == 'invite') {
    var frm = document.forms.inviteConfirm;
    var txt = '<strong>'+msg.from.nick+'</strong>';
    txt += 'wants to play a game with you.';
    txt += 'Accept?';
    frm.sender.previousSibling.innerHTML = txt;
    frm.sender.value = msg.from.id;
    frm.sendernick.value = msg.from.nick;
    frm.style.display = 'inline';
  }
};

Figure 9.4 The dialog box inviting you to a new game

image

So the ID and nickname of the person who sends the invite are contained in the form inviteConfirm, and the message window can be displayed. When the other player clicks on Yes or No, the appropriate response is sent back to the inviter via the server and once again lands in the onmessage callback:

else if (msg.request == 'confirm') {
  if (msg.choice == true) {
    wsMessage({
      task : 'setPlaying',
      client : msg.from.id
    });
    prepareGame(msg.from.id,msg.from.nick);
    document.forms.loggedin.style.display = 'none';
  }
  else {
    show('nothanks'),
    window.setTimeout(function() {
      hide('nothanks'),
      document.forms.users.style.display = 'inline';
    }, 2000);
  }
}

If the invitation to play was answered with Yes, the server is informed that the two players are now playing together, the game is prepared, and the selection list of logged-in players is hidden. If the answer was No, only the message No thanks, not now is displayed for two seconds.

As a direct consequence of the server message We are now playing together, other steps follow, such as the update of the player status object on the server, which then informs all users that the two players involved are not currently available for other games.

For the server in game_server.js, it would look as follows:

  var setBusy = function(id) {.
    USERS[id].busy = true;
    var msg = {task:'isPlaying',user:USERS[id]};
    conn.broadcast(JSON.stringify(msg));
    conn.write(JSON.stringify(msg));
  };
...
else if (msg.task == 'setPlaying') {
  setBusy(conn.id);
  setBusy(msg.client);
}

Back in the client, this message is caught in the onmessage callback and the locally kept list of logged-in players is updated. The result of this update is that both players can no longer be selected, because their option elements are deactivated via a disabled attribute:

else if (msg.task == 'isPlaying') {
  var opts = document.forms.loggedin.users.options;
  for (var i=0; i<opts.length; i++) {
    if (opts[i].value == msg.user.id) {
      opts[i].disabled = 'disabled';
    }
  }
}

If both players agree that they want to play together, they can start placing the ships. If you are logged in as user test1 or test2, your ships are already prepared for you; if not, a pull-down menu pops up, allowing you to digitalize your flotilla via five buttons. Select whether to arrange each ship in a horizontal or vertical orientation, and then click on the desired ship type and place the ship onto the play area in the desired place by clicking once more.

The relevant fields are formatted to represent ships via a CSS class ship and are recorded in three JavaScript variables. The variable game.ships.isShip remembers the designated positions, and the variable game.ships.parts records the fields belonging to each ship as an array of arrays. A copy of these arrays is worked through successively in the variable game.ships.partsTodo during the game and only contains ten empty arrays at the end of the game for the losing player, because the relevant position is deleted for each hit.

With each newly placed ship, the label of the relevant button is updated as well, showing how many ships of this type are still available. It disappears once all ships of this type have been placed. Once all ships are placed, the entire form disappears and a message is sent to the opponent: Ready to start the game!

if (game.ships.parts.length == 10) {
  document.forms.digitize.style.display = 'none';
  game.me.grid['1-1'].parentNode.style.pointerEvents =
    'none';
  wsMessage({
    task : 'private',
    request : 'ready',
    client : game.you.id
  });
  game.me.ready = true;
}

Who comes first, goes first is the motto, so the player who is the quickest to place all his ships can begin the game. The slower player has to bite the bullet and suffer the first attack on his fleet. To allow each player to attack the opponent’s ships, a second play area is displayed after both players have placed their ships.

The game logic for attacking and sinking ships is implemented fully on the client side. The server only distributes the game moves as private messages to both players involved. Each click on an active play field calls the reveal function:

this.reveal = function(evt) {
  wsMessage({
    task : 'private',
    request : 'challenge',
    field : evt.target.value,
    client : game.you.id
  });
};

The server transmits the message to the opponent’s side, which then checks whether the field the other player clicked on contains part of a ship or not:

else if (msg.request == 'challenge') {
  var destroyed = 0;
  if (game.ships.isShip[msg.field]) {
    game.me.grid[msg.field].setAttribute("class","hit");

Figure 9.5 shows the game in demo mode.

Figure 9.5 “Battleships!” in demo mode

image

In case of a hit (isShip is true), the relevant button on the player’s own play area is assigned the class hit, coloring it red according to the stylesheet instruction. If a ship is hit but is not yet completely destroyed, the opponent receives an appropriate message:

wsMessage({
  task : 'private',
  request : 'thisFieldIs',
  result : 'hit',
  field : msg.field,
  client : game.you.id
});

If the request part of the message is thisFieldIs, the field is treated accordingly for the opponent:

else if (msg.request == 'thisFieldIs') {
  if (msg.result == 'water') {
    game.you.grid[msg.field].setAttribute("class",
        msg.result);
    deactivateField();
  }
  else if (msg.result == 'hit') {
    game.you.grid[msg.field].setAttribute("class",
        msg.result);
  }
...

From the attacker’s point of view, the answer hit marks the field he clicked on in red. If the answer is destroyed, all fields belonging to that ship are turned from red to green to show it was hit and destroyed. At the same time, in the play area of the attacked player, his hit position is marked in red. In the case of destroyed, all previously red ship sections are turned blue to show that the attacked ship was destroyed and has been replaced by blue blocks, or water. So, the more blue you can see on your play area, the worse the situation; the more green in the opponent’s play area, the better the chance of victory.

If the answer is water, it is now the attacked player’s turn to retaliate (the deactivateField() function prevents further actions). The game continues back and forth until one of the two players has destroyed all of the opponent’s ships and is declared the winner. Marking the status of your own and enemy ships is done via CSS formats for each button element, as mentioned previously. The turn-taking between players is possible because the opposing game area for the currently inactive player is deactivated with pointer-events:none and opacity:0.2.

After the end of the game, both players are separated again; their status is reset to Available to play, and the next invitation can be issued. In the current version, Battleships! does not yet allow for playing several consecutive games with the same player; perhaps you would like to try implementing this new feature? Another good idea might be a Logout button, and if you are feeling really brave, you could implement a multiplayer mode. There are many options for developing this application further. You are only limited by your imagination!

The example demonstrates in an impressive way the new options offered by the WebSocket protocol for developing interactive applications. Our examples dealt with interaction between users. But a feature you could easily implement would be the WebSocket server getting information from the Internet, processing it, and then sending it to the connected users. The previously mentioned application for broadcasting current stock market prices would be a good example of this. Another possible scenario would be displaying new messages received in Twitter. The advantages are obvious: The client is notified of news via the message event, and the data stream between client and server is very lean, conserving network bandwidth.

Summary

With WebSockets, a new protocol has stepped onto the WWW stage. By no means does this spell the end of the Hypertext Transfer Protocol. The WebSocket protocol was developed for special applications where bidirectional communication between client and server with little overhead is required.

Both the server-side and the client APIs are very easy to program, as you can see from our first example of a rudimentary chat application. A full-blown, multiuser, online game is presented in our final example, Battleships!. Here, too, the communication between client and server can be programmed with a few lines of JavaScript code, and less code always means less risk of errors.

The introduction of WebSockets makes it easy to program web applications that previously could only be realized very laboriously via XMLHttpRequests or by constantly reloading web pages. Large amounts of rapidly changing data can then be monitored through one website; stock exchange data is just one example.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset