Home > HTML 5 > HTML 5 – Websocket Chat Demo

HTML 5 – Websocket Chat Demo

The HTML 5 initiative contains a draft spec for WebSockets which allow a browser to establish a full-duplex, bi-directional communication channel over a single TCP socket. This allows web developers to establish real time two way communications with a server using simple javascript without resorting to Flash, Java, ajax long polling, comet, forever iframe, or other current workarounds. Here I’ve written a simple browser chat using WebSockets for the client and PHP for the server.

WebSocket Handshake

To establish a WebSocket connection, the browser makes a HTTP GET request to the server with an offer to upgrade to WebSocket. In the current websocket spec, that request looks like this:

Client Request

GET /resource HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: example.com:8080
Origin: http://www.example.com
Sec-WebSocket-Key1: W  1529l9 6i4b{{a 9 z55
Sec-WebSocket-Key2: F13 O05   q  q6 - 4k7D   97

-#&nD[fU

The first line is a HTTP/1.1 GET request. The next two lines offer to upgrade the connection to the WebSocket protocol. The Host header lists the server name and port the client wishes to connect to (necessary when the server hosts multiple domains on the same IP address). The Origin header indicates where the request originates from, not unlike the HTTP Referer header. The last two headers and the 8 bytes at the end are random tokens which the server will use to construct a challenge response.

If the server accepts the upgrade offer (based on the resource, host, and origin), it returns a response like this:

Server Response

HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://www.example.com
Sec-WebSocket-Location: ws://example.com:8080/resource

8jKS'y:G*Co,Wxa-

The first three lines indicate that the server is upgrading the connection to the WebSocket protocol. The Sec-WebSocket-Origin header echoes the value the client sent, and the Sec-WebSocket-Location header returns the full URI for the websocket connection. The 16 bytes at the end form the challenge response. It is calculated by extracting the numeric digits from the client's first Sec-WebSocket-Key header, concatenating them to form a number, and dividing by the number of spaces. The same process is done with the second client key, the two numbers are then concatenated together along with the final 8 bytes of the client request, and the MD5 sum of the result becomes the server challenge response. Whew.

Here's a snippet from the server code which calculates the security response:

private function handleSecurityKey($key) {
    // extract the numeric digits to form a number
    preg_match_all('/[0-9]/', $key, $number);
    // extract the number of spaces
    preg_match_all('/ /', $key, $space);
    if ($number && $space) {
        // perform the calculation per the websocket spec
        return implode('', $number[0]) / count($space[0]);
    }
    return '';
}

private function getHandshakeSecurityKey($key1, $key2, $code) {
    return md5(
        pack('N', $this->handleSecurityKey($key1)).
        pack('N', $this->handleSecurityKey($key2)).
        $code,
        true
    );
}

At any point during this handshake process, if the client or the server does not receive the expected response, it will refuse the upgrade simply by closing the connection.

WebSocket API

Inside a compatible browser, you create and interact with WebSockets via a WebSocket object. According to the WebSocket API, creating a new WebSocket object causes the browser to attempt a connection to the server. The server address is specified in the same manner as a HTTP URI except “ws” replaces “http” and “wss” replaces “https”. Once created, the WebSocket object exposes four callback functions for handling events, a function for sending messages, another for closing the connection and a readyState property for determining the status of the connection.

// connect to server
var websocket = new WebSocket("ws://example.com:8080/resource");

// called when the connection is established
websocket.onopen = function(evt) { ... };

// called when a message is received from the server
websocket.onmessage = function(evt) { ... };

// called when an error occurs
websocket.onerror = function(evt) { ... };

// called when the connection is closed (by either side)
websocket.onclose = function() { ... };

// send a message to the server
websocket.send(data);

// close the connection
websocket.close();

// status of the connection
// 0 == CONNECTING
// 1 == OPEN
// 2 == CLOSING
// 3 == CLOSED
var state = websocket.readyState;

Data Transport

Once the connection between client and server has been established successfully, it is held open and becomes a two way TCP socket, allowing either side to send messages at will.

Each message consists of a 0x00 byte followed by UTF-8 data followed by a 0xFF byte. Any deviation from this, for example missing start or end byte markers or the data is not UTF-8, results in an error and the termination of the connection. The WebSocket protocol currently does not support binary data streams.

On the client side, each message received triggers the onmessage handler function, allowing client code to react appropriately.

The Client

Now it’s time to dive into the client code. The html is really simple:

<html>
<head>
  <title>HTML5 Chat Demo</title>
<body>

<h3>HTML 5 Chat Demo</h3>

<h4 id="error" style="color:#ff0000"></h4>
<div id="console" style="height:300px; overflow:scroll; border:1px solid black"></div>
<div style="padding-top:10px">
    <label>Handle:</label><input id="handle" size="25" value="">
    <button id="connect">Connect</button>
    <button id="disconnect">Disconnect</button><br>
    <label>Message:</label><input id="message" size="25" value="">
    <button id="send">Send</button>
</div>


</body>
</html>

We have a large div to represent the chat window. Below it is a text box to hold the user’s chat handle, two buttons for connecting and disconnecting to the server, followed by another text box for the user’s current message, and a send button. The actual work is done via javascript.

First we need to see if the browser supports WebSockets, if not, we display an error message:

// test for websocket support
if ("WebSocket" in window) {

    // websockets supported!

} else { // no websocket support
    $('error').innerHTML = "Your browser does not appear to support WebSockets";
}

Once compatibility has been determined, we can attempt to make a connection and hook up the handler functions.

// creating a WebSocket object causes it
// to connect to the server
websocket = new WebSocket(server);

// called once the connection is established
websocket.onopen = function(evt) {
    $('console').innerHTML += "CONNECTED<br>";
};

// called upon receipt of a message
websocket.onmessage = function(evt) {
    $('console').innerHTML += "RECEIVED: "+evt.data+"<br>";
};

// called when an error occurs
websocket.onerror = function(evt) {
    $('console').innerHTML += "ERROR: "+evt.data +"<br>";
};

// called when the connection is closed (by either side)
websocket.onclose = function() {
    $('console').innerHTML += "CLOSED<br>";
};

The Server

The server application needs to listen on a given port for incoming connections, offer to upgrade them from HTTP to WebSockets, perform the handshake correctly, and finally perform any logic specific to your application. In this case, it forwards messages between connected clients. In PHP, this requires a number of calls to the various socket_* functions. This example is pretty light on proper error handling, and uses code adapted from here and here

First we need to create a socket to listen on. This initial socket needs to be saved as it is required for getting the socket objects for any clients that connect.

protected function connectMaster() {
    // create a server socket.  AF_INET = IPv4, SOCK_STREAM + SOL_TCP = TCP/IP
    $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("socket_create() failed");
    // reuse local addresses
    socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1) or die("socket_option() failed");
    // bind the IP address to the socket
    socket_bind($this->master, $this->address, $this->port) or die("socket_bind() failed");
    // listen for incoming connections (max of 20 pending)
    socket_listen($this->master, 20) or die("socket_listen() failed");
    // save the socket object
    $this->sockets[] = $this->master;

    $this->log("Websocket server started : ". date('Y-m-d H:i:s'));
    $this->log("Listening on             : ". $this->address ." port ". $this->port);
    $this->log("Master socket            : ". $this->master);
    $this->log("CTRL-C to quit");

    return $this->master;
}

Now we enter the main server run loop. It waits around for any of its open sockets to have data. If it’s the master socket, it means we have a new connection to process. If it’s any other socket, we send the handshake if necessary, otherwise, we pass the message to our callback function.

public function run() {
    // main run loop
    while(true){
        $changed = $this->sockets;
        // wait for any of the sockets to change (have data to read)
        // see http://www.php.net/manual/en/function.socket-select.php
        if (false === socket_select($changed, $write=NULL, $except=NULL,NULL)) {
            die("socket_select() failed: ". socket_strerror(socket_last_error()));
        }
        foreach($changed as $socket){
            if ($socket == $this->master) {
                // new incoming connection, time to accept
                $this->log("Incoming connection request");
                $client = socket_accept($this->master);
                if($client === false){ $this->log("socket_accept() failed: ". socket_strerror(socket_last_error())); continue; }
                else{ $this->connect($client); }
            } else {
                // existing connection has data to read
                $bytes = @socket_recv($socket, $buffer, 2048, 0);
                // close the connection if there was an error or no data to read
                if($bytes == 0) {
                    $this->disconnect($socket);
                } else {
                    $user = $this->getUserBySocket($socket);
                    // perform the WebSocket upgrade HTTP handshake if needed
                    if(! $user->handshake) {
                        $this->doHandshake($buffer, $socket);
                        $user->handshake = true;
                    } else {
                        $this->log("< ". $this->unwrap($buffer));
                        $user->lastAction = time();
                        // call the callback function
                        if ($this->callback) {
                            call_user_func($this->callback, $user, $this->unwrap($buffer), $this);
                        }
                    }
                }
            }
        }
    }
}

Here’s the simple callback function which passes the message along to the other clients so that they can display our chat messages:

// callback function
function process($sourceUser, $msg, $server){

    // echo the data back to all connected users
    foreach($server->getUsers() as $user){
        $server->send($user->socket, $msg);
    }
}

Because PHP is not normally used as an infinitely running process, we have to adjust the time limit config as well as turn off the buffering subsystem to get this to work smoothly without timing out.

// show us all errors
error_reporting(E_ALL | E_NOTICE);
// keep running until script aborts or we're killed
set_time_limit(0);
// flush output buffering
ob_implicit_flush();

And there you have the basics of a simple websocket server and client.

Limitations of WebSockets

Currently, browsers based on recent versions of webkit (Safari & Chrome but not iPhone) support draft76[the latest draft] of the WebSocket spec, but as a draft, it’s a bit of a moving target.

As Greg Wilkins discusses, there are some fundamental difficulties writing a robust application based on the current WebSocket draft specification. In particular, since any problem encountered by client or server results in the connection closing, it is impossible for the client to distinguish errors from failed connections from time outs or transient network issues as all result in a single call to onclose().

Also, as discussed here, even when using port 80, non-WebSocket aware proxies often terminate WebSocket handshakes or connections preventing clients behind the proxy from communicating with the WebSocket server.

Hopefully, future drafts of the spec will address these issues.

Putting It All Together

To see the chat client in action, click here.

The full client and server source code is available as a download:

phpwebsocket.zip[5.4k]

To launch the server, use something like this:

php server/server.php 127.0.0.1 8080

Edit the server address in the client/index.html code to match your own server.

// the websocket object
var websocket;

// our chat handle
var handle = "";

// the address of the websocket server
var server = "ws://chat.dev.weevilgenius.net:1212/chat_demo";


function init()
{
Categories: HTML 5 Tags: , , ,
  1. testman
    November 9th, 2010 at 08:04 | #1

    Hi, great tutorial!

    Can’t get the code to run though. I can get the client to run if I use your server, but if I edit index.html and change the info to my server, which at least seems to be running fine, I can’t get a connection, weird.

  2. Barry Stump
    November 9th, 2010 at 08:51 | #2

    @testman yeah, debugging connection issues is a pain. Are your client and server configured with the same port? My snippet showing the server start script has port 8080, but my demo is actually using port 1212. They both have to be the same.

    You can use telnet to see if the server is accepting connections as expected, like this: (this is my server, you’ll have to change the host and port to check your setup)

    $ telnet chat.dev.weevilgenius.net 1212
    (wait for it to connect then type the following followed by enter)
    GET /chat HTTP/1.1

    If the server is active on that port, it should attempt to send you the websocket upgrade handshake, like this:

    HTTP/1.1 101 Web Socket Protocol Handshake
    Upgrade: WebSocket
    Connection: Upgrade
    WebSocket-Origin:
    WebSocket-Location: ws:///chat

    As you can see, it blindly responds even though the request was incomplete.

  3. steven
    November 22nd, 2010 at 07:05 | #3

    Hi great stuff! how would i go about allowing only certain type of user to post a message serverside

  4. Barry Stump
    November 22nd, 2010 at 22:10 | #4

    @steven The most basic type of control you can implement is on the server by looking at the host, origin, and resource values in the client’s request. My example code will accept connections from anywhere, but it could easily be modified to only accept requests from a particular website, or for a particular resource URL.

    However, that only provides basic source and destination control. To have user-level control, you will need to authenticate your users . The client will need to send their credentials to the server, probably after requesting them from the user. The server can then decide what each client can do on behalf of its user.

  5. Spiteful
    December 1st, 2010 at 12:11 | #5

    I agree this is good stuff. I’m just wondering about your wrap and unwrap functions. I’ve been working on trying to build a PBBG (persistent browser based game) and your example looks like a great place. I’m just wondering what purpose the wraps serve and if it would be ok to replace them with json_encode and decodes…

  6. Barry Stump
    December 1st, 2010 at 14:33 | #6

    @Spiteful You’ll have to keep the server side wrap and unwrap functions as they wrap the data in 0×00 and 0xFF bytes as required by the WebSocket spec. However, you can use json_encode/decode on the data itself before it gets wrapped.

    Glad to hear that this post was useful to you. PHP is easy to program and fairly ubiquitous, but I would advise against using it for this kind of socket work, other than as a proof-of-concept or prototype. For something a little more robust, you might wish to consider Java or Erlang or node.js for the server side.

  7. Spiteful
    December 1st, 2010 at 22:41 | #7

    Sadly PHP is the only language I’m familiar with :( If I knew another language to do it in I gladly and easily would. I’m just tired of working on this project for 6 years and not getting anywhere (my initial setup was constant AJAX calls).

  8. Daniel Flores
    December 13th, 2010 at 20:38 | #8

    Only working with the websocket.onmessage you can create amazing things like me . Websockets + canvas is so much win. Greetings.

  9. Matt
    December 31st, 2010 at 07:44 | #9

    @Barry Stump

    Nice article.

    What if you get the following over Telnet:

    HTTP/1.1 101 Web Socket Protocol Handshake
    Upgrade: WebSocket
    Connection: Upgrade
    WebSocket-Origin:
    WebSocket-Location: ws://

    (nothing after ws://). My implantation works fine connecting to your server but keeps disconnecting after trying to change the server string to 127.0.0.1.

    Any ideas?

  10. Matt
    December 31st, 2010 at 13:55 | #10

    Sorry that should be *implementation ;)

  11. Seba
    April 16th, 2011 at 09:44 | #11

    hi,

    great demo.
    I have some questions if you could help.
    Can you please send me an e-mail if you could help?

    thanks,
    Sebastian

  12. Barry Stump
    April 18th, 2011 at 11:22 | #12

    @Matt The WebSocket-Location header in my example is constructed from the combination of the received Host header (which my telnet example didn’t specify) plus the requested resource (/chat in my example). If you’re seeing nothing after ws:// then you didn’t tell the server what you wanted when making the connection.

  13. April 19th, 2011 at 21:08 | #13

    Hi, Great Tutorial !
    Btw, i wanna do the handshaking in Java.. Maybe do u have any link or information about that? Thx..

  14. Barry Stump
    April 20th, 2011 at 08:24 | #14

    @eve the Jetty project now has built in WebSocket support. You might start there to avoid having to write it all from scratch.

  15. nemo
    February 10th, 2012 at 13:56 | #15

    hi,

    I have uploaded this chat on my server to
    myserver.com/websocket/server/server.php

    and I´m pointing safari to
    myserver.com/websocket/client/index.html

    What is the exact url for var server ?

    the server.php is running already:
    Listening on : 127.0.0.1 port 8080

    I´ve tried different urls like
    var server = “ws://127.0.0.1:8080″;
    or
    ws://127.0.0.1:8080/websocket/server/server.php

    but I can´t get a connection.
    Firewall on server is turned off, also on my OS.
    Strange thing is, on a second shell window,
    telnet 127.0.0.1 8080
    works but
    telnet SERVER_IP 8080
    doesn´t work..

    Can you help me, please ?
    Thanks :)

  16. Stig
    February 20th, 2012 at 06:56 | #16

    I am having the same problem as @nemo.
    Did you find a solution?

    Regards.

  17. alice
    March 15th, 2012 at 01:36 | #17

    Hi

    As far as I know,

    ws://localhost:8000/socket/server/startDaemon.php
    ==> Scheme://Host:Port/WebSocketServer

    I hope this information helps you.

    Regards.

  18. alice
    March 15th, 2012 at 01:40 | #18

    me, too

  19. alice
    March 15th, 2012 at 01:41 | #19

    @alice

    Did you find a solution?

    Regards.

  20. alice
    March 15th, 2012 at 02:01 | #20

    do you used chrome?

  21. March 26th, 2012 at 03:18 | #21

    Author: Thanks for the post! I learned a lot from it.
    Others: The post is old and the example not working with the actual browsers.

  22. March 26th, 2012 at 03:25 | #22

    @nemo

    in the “php server/server.php 127.0.0.1 8080″ the 127.0.0.1 means the 8080 port will only response to localhost (!!!!!!!!) requests.

    ’127.0.0.1′
    accepts clients from localhost (eg. 127.0.0.1)

    ’0.0.0.0′
    accepts clients from localhost, and the server’s network (eg. 127.0.0.1, 192.168.2.5, 10.20.30.40)

    ’0′ or 0
    accepts clients from localhost, the server’s network, and external networks (eg. 127.0.0.1, 192.168.2.5, 10.20.30.40, 209.85.169.99)

    So, start this way:
    php server/server.php 0 8080

    Now you can open it with telnet, because the port is open. However the example won’t work as new Google Chrome browsers using a newer draft specification (version 13), so you have to overwrite the old server code or try another newer websockets server implementation.

    Now I try to implement this server: http://code.google.com/p/phpws/
    If hope this will work fine.

  23. lilzz
    March 28th, 2012 at 23:32 | #23

    Can PHPwebsocket used for server and Mobile devices instead of just web browsers and servers?

  1. No trackbacks yet.