Close search
Hoa

Hack book of Hoa\Websocket

The WebSocket protocol allows a bidirectional and full-duplex communication between a client and a server. The Hoa\Websocket library allows to create WebSocket servers and clients.

Table of contents

  1. Introduction
    1. Handshake and challenge
    2. Frame and opcode
    3. History
  2. Write a server
    1. Listeners
    2. Send messages
    3. Broadcast messages
    4. Closing
  3. Message
    1. Fragmentation
    2. Encoding
    3. Binary
  4. Write a client
    1. Start a connection with the server
    2. Send and broadcast messages
    3. Note about the handshake and the host
  5. Customized node
  6. Conclusion

Introduction

The WebSocket protocol is standardized in the RFC6455. It allows a client and a server to communicate between each other. This communication is bidirectional, it means that the client can send messages to the server, and vice versa. The server does not only send responses, it is able to send messages spontaneously. This changes the classical Web approach with its HTTP protocol. The WebSocket protocol is also full-duplex, it means that data are exchanged simultaneously in both directions: While a data is currently transiting from the server to the client, the latter is able to also send a message to the server. Consequently, the WebSocket protocol allows a high interactivity between both client and server. The client will often be a browser. It is important to notice that the URI schema (see the RFC3986) of the WebSocket is ws://.

We can ask ourselves what are the differences between WebSocket and EventSource. Both solutions are fundamentaly differents: WebSocket allows a bidirectional and full-duplex communication, whereas EventSource is a technology based on the HTTP protocol and provides only a unidirectional communication. For this kind of usage, an EventSource server is more likely to be light, simple and it is designed to be robust regarding disconnections (see the Hoa\Eventsource library).

The WebSocket protocol starts by a handshake in order to start the communication and exchange messages formatted as frames.

Handshake and challenge

To start a communication with the WebSocket protocol, the client must send an HTTP request to the server asking to change the protocol. In this request, the client inserts a challenge, kind of enigma, that the server must solve. If it succeed, the communication will start.

Starting by an HTTP request is not anodyne. It allows the WebSocket protocol to use the same network path as HTTP requests, and so, for example, traverse proxys, firewalls etc. This also eases the deployment of this protocol: No need to open a dedicated port, no need to have a specific configuration on the server etc. Finally, it allows to use a secured connection thought TLS. In this case, we will use the wss:// URI schema.

Frame and opcode

Messages between the client and the server are not exchanged verbatim. Actually, the message is formatted as a frame: A packet of bits with a specific design. In this case, we will find information about the type of the message, its length, some verification codes etc. We will find an explanatory schema in the section 5.2, Base Framing Protocol from the protocol specification for the most curious of you.

The type of the message is called opcode. This is the most important information. We will find this term in the chapter many times. The Hoa\Websocket\Connection class provides constants to represent each opcode in order to simplify their use. This class ensures the protocol support.

History

There is two versions of the WebSocket protocol in the nature: The standard version and the non-standard version. The standard version is the one described in the RFC6455. The last non-standard version has the following nickname: draft-ietf-hybi-thewebsocketprotocol-00 (or draft-hixie-thewebsocketprotocol-76), or Hybi00 for short. This non-standard version has many important security issues but it is in use in some languages such as Flash. Fortunately, this version disappears more and more and is replaced by the RFC6455.

The Hoa\Websocket library supports both versions. It allows clients supporting different versions of the protocol to communicate between each other.

Write a server

The Hoa\Websocket\Server class allows to write a server manipulating the WebSocket protocol. This class inherits from Hoa\Websocket\Connection. The communication is based on a socket server. We will use the Hoa\Socket\Server class (from the Hoa\Socket library) to fill this role.

The WebSocket protocol runs on TCP, thus we will start a local WebSocket server on the 8889 port:

$server = new Hoa\Websocket\Server(
    new Hoa\Socket\Server('tcp://127.0.0.1:8889')
);

However, we can use the ws://127.0.0.1:8889 URI directly instead of tcp://127.0.0.1:8889. This is an advantage when we use wss:// for a secured connection because Hoa\Websocket will know that the connection will need to be secured and everything will be done for you automatically. Manipulating TLS, enabling encryption for certain connections etc. will not be required. Thus:

$server = new Hoa\Websocket\Server(
    new Hoa\Socket\Server('ws://127.0.0.1:8889')
);

Now, let's see how to interact with this server.

Listeners

The Hoa\Websocket\Connection class provides six listeners:

For the message and binary-message listeners, there is only one associated datum: message, which contains without any surprise the received message.

For the ping listener, we will also find the message datum. Notice that the pong is automatically sent before the listener is fired.

For the error listener, we will find the exception datum, which contains an exception (not necessary Hoa\Websocket\Exception\Exception, this can be for example Hoa\Socket\Exception). The listener is fired after that the connection has been closed.

The close listener has two data: code for the code and reason explains the reason of this closing with a short message. We will find standard closing codes through the CLOSE_* constants on the Hoa\Websocket\Connection class. For example, Hoa\Websocket\Connection::CLOSE_NORMAL represents a normal connection closing, without error, whereas Hoa\Websocket\Connection::CLOSE_MESSAGE_ERROR represents a connection closing because of a malformed message. This listener is fired after that the connection has been closed.

Send messages

Let's complete our example to, in the message listener, send back to the client all received messages in order to create an echo. We will use the Hoa\Websocket\Connection::send method. Once our listener is registered, we can start the server thanks to the Hoa\Websocket\Connection::run method. Thus:

$server->on('message', function (Hoa\Event\Bucket $bucket) {
    $data = $bucket->getData();

    echo 'message: ', $data['message'], "\n";
    $bucket->getSource()->send($data['message']);

    return;
});

$server->run();

Now, let's test our server by creating a very simple HTML client:

<input type="text" id="input" placeholder="Message…" />
<hr />
<pre id="output"></pre>

<script>
  var host   = 'ws://127.0.0.1:8889';
  var socket = null;
  var input  = document.getElementById('input');
  var output = document.getElementById('output');
  var print  = function (message) {
      var samp       = document.createElement('samp');
      samp.innerHTML = message + '\n';
      output.appendChild(samp);

      return;
  };

  input.addEventListener('keyup', function (evt) {
      if (13 === evt.keyCode) {
          var msg = input.value;

          if (!msg) {
              return;
          }

          try {
              socket.send(msg);
              input.value = '';
              input.focus();
          } catch (e) {
              console.log(e);
          }

          return;
      }
  });

  try {
      socket = new WebSocket(host);
      socket.onopen = function () {
          print('connection is opened');
          input.focus();

          return;
      };
      socket.onmessage = function (msg) {
          print(msg.data);

          return;
      };
      socket.onclose = function () {
          print('connection is closed');

          return;
      };
  } catch (e) {
      console.log(e);
  }
</script>

At line 6, we declare the address of the WebSocket server by using the ws protocol. At line 45, we use the WebSocket object and we attach listeners, strongly similar to those of Hoa\Websocket\Connection!

To test, we just have to start the server:

$ php Server.php

Then, open the client in your favorite browser. Each message sent to the server returns identically, we have an echo.

Broadcast messages

So far, the client speaks with the server and the server replies, but this is only a dialog. However, the server has all connections in memory. We are able to broadcast a message to all connected clients. So, we will use the Hoa\Websocket\Connection::broadcast method that will send a message to all other connected clients. Thus:

$server->on('message', function (Hoa\Event\Bucket $bucket) {
    $data = $bucket->getData();

    echo 'message: ', $data['message'], "\n";
    $bucket->getSource()->broadcast($data['message']);

    return;
});

Et voilà ! Restart the server and open the client many times. Each message we send is broadcasted to all other clients! Our example is now an instant messaging tool.

We have to understand that the Hoa\Socket\Server socket server works with nodes, it means an object that represents an opened connection. In this listener, to know the current node that has fired it, we have to call the Hoa\Websocket\Connection::getConnection method to get the socket server, and then, the Hoa\Socket\Server::getCurrentNode method. Similarly, we have the Hoa\Socket\Server::getNodes to get a list of all nodes. The Hoa\Websocket\Connection::broadcast method is in fact implemented in the Hoa\Socket library and hides this complexity. It is preferable to use this method for performance and compatibility reasons.

Closing

To close a connection with a client, we use the Hoa\Websocket\Connection::close method. This latter is very similar to Hoa\Websocket\Connection::send. Its arguments are:

For example, when we will receive the message I love you, we will close the connection by explaining why, else we will only do an echo of the message:

$server->on('message', function (Hoa\Event\Bucket $bucket) {
    $data = $bucket->getData();

    if ('I love you' === $data['message']) {
        $bucket->getSource()->close(
            Hoa\Websocket\Connection::CLOSE_NORMAL,
            'Thank you but my heart is already taken, bye bye!'
        );

        return;
    }

    $bucket->getSource()->send($data['message']);

    return;
});

We can modify our client in order to show the code and the reason of the closing:

      socket.onclose = function (e) {
          print(
              'connection is closed (' + e.code + ' ' +
              (e.reason || '—no reason—') + ')'
          );

          return;
      };

It is highly recommended to always use this method to close a connection instead of closing the TCP connection directly.

Message

There is two ways to send a message: Either in one piece if we have the message entirely, or in several pieces. Our message can also contain other things than text, for example binary data. In this case, we speak about a binary message.

Fragmentation

To send a message in one block, we use the Hoa\Websocket\Connection::send method as seen in previous sections:

$server->on('message', function (Hoa\Event\Bucket $bucket) {
    $bucket->getSource()->send('foobar');

    return;
});

In fact, this method has four arguments:

We will use all these arguments by trying to send a fragmented message.

In our example, we have sent a message entirely, in one piece, which is the most usual way to do. If we send a very long message, we will also use this method. However, it is possible that we have the message piece by piece, and then, we are unable to send it entirely. For example, if a message, with an undetermined length, is read on a stream, and we would like to send it to a client, we won't wait to get the full message: We will send each piece directly to the client. In this case, we speak about fragmented messages.

We will use the next opcodes: OPCODE_TEXT_FRAME for the first fragment, and then OPCODE_CONTINUATION_FRAME for the next ones. Each time, we will specify that the message is not over thanks to the fin argument which will be set to false, except for the last fragment where fin will be true.

The final user behind the client won't receive fragmented messages, but one message in one block once the last fragment will be received. From the server side, this will avoid to overload the memory with “transit” data, and also to overload the network with a heavy message. We send data as soon as we get them, and it is part to the client to reconstitute the message. The server acts in the same way when it receives a fragment message. Between both fragments, the server is able to compute other tasks. So, it is more interesting to use fragments instead of bufferizing the message.

Let's see an example. We will send the foobarbaz fragmented message in three pieces. We can imagine we read these data on a socket for example, and that the data are coming as soon as they are available. Thus:

$server->on('message', function (Hoa\Event\Bucket $bucket) {
    $self = $bucket->getSource();

    $self->send(
        'foo',
        null,
        Hoa\Websocket\Connection::OPCODE_TEXT_FRAME,
        false // not the end…
    );
    echo 'sent foo', "\n";
    sleep(1);

    $self->send(
        'bar',
        null,
        Hoa\Websocket\Connection::OPCODE_CONTINUATION_FRAME,
        false // not the end…
    );
    echo 'sent bar', "\n";
    sleep(1);

    $self->send(
        'baz',
        null,
        Hoa\Websocket\Connection::OPCODE_CONTINUATION_FRAME,
        true // the end!
    );
    echo 'sent baz, over', "\n";

    return;
});

The sleep instructions allow to emulate a network latency or something equivalent. Each time the send method is called, data are effectively sent to the client, there is no buffer on the server side.

Encoding

All exchanged messages must be in the UTF-8 format (see the RFC3629). If a message from the client is not consistent with this format, Hoa\Websocket\Connection will close the connection appropriately with the Hoa\Websocket\Connection::CLOSE_MESSAGE_ERROR code, we have nothing special to do. Consequently, all the received messages in our listeners respect this encoding.

However, Hoa\Websocket\Connection verifies that messages going to the client are consistent with this format. If the encoding is not appropriated, then a Hoa\Websocket\Exception\InvalidMessage exception will be thrown, which will close the connection and fire the error listener if it is not captured earlier.

Binary

It is also possible to send binary data, more compacted than textual data. We speak about binary messages. We will still use the Hoa\Websocket\Connection::send method but with the OPCODE_BINARY_FRAME opcode. This makes sense only in the binary-message listener, it means in a “binary communication” between the client and the server. We can imagine the client sending coordinates and the server giving others (for example for a board game):

$server->on('binary-message', function (Hoa\Event\Bucket $bucket) {
    $data                          = $bucket->getData();
    $message                       = $data['message'];
    $point                         = [];
    list($point['x'], $point['y']) = array_values(unpack('nx/ny', $message));

    // compute a next point.

    $bucket->getSource()->send(
        pack('nn', $point['x'], $point['y']),
        null,
        Hoa\Websocket\Connection::OPCODE_BINARY_FRAME
    );

    return;
});

The pack and unpack functions are precious friends here.

Notice that binary messages can also be fragmented. We have to use the OPCODE_BINARY_FRAME opcode instead of OPCODE_TEXT_FRAME and then continue with OPCODE_CONTINUATION_FRAME as we have learned.

Write a client

The Hoa\Websocket\Client class allows to write a client manipulating the WebSocket protocol. This class inherits from Hoa\Websocket\Connection. The communication is based on a socket client. We will use the Hoa\Socket\Client class (from the Hoa\Socket library) to fill this role.

Much as Hoa\Websocket\Server is able to communicate with clients supporting different versions of the WebSocket protocol, Hoa\Websocket\Client uses only the RFC6455 protocol. It means that the client is only able to communicate with a server supporting the RFC6455 protocol.

Start a connection with the server

Like the server, the client inherits from Hoa\Websocket\Connection. We find the same send, close, run and other methods, and also the same listeners. And like the server, listeners must be registered on the client.

Thus, to start a client, we will write:

$client = new Hoa\Websocket\Client(
    new Hoa\Socket\Client('ws://127.0.0.1:8889')
);
$client->on('message', function (Hoa\Event\Bucket $bucket) {
    $data = $bucket->getData();
    echo 'received message: ', $data['message'], "\n";

    return;
});

The client can work in loop mode, like the server, with the run method. In this case, we will write:

$client->run();

Or, for a sequential exchange, we have to manually call the Hoa\Websocket\Client::connect method:

$client->connect();

If the server does not support the right protocol, a Hoa\Websocket\Exception\BadProtocol exception will be thrown.

Send and broadcast messages

To send a message, we will use the Hoa\Websocket\Connection::send method. Its behavior has been described previously for the server. It is identical.

A contrario, to receive a message, we will use the Hoa\Websocket\Client::receive method. The received messages from the server will fire listeners. Thus, we will send a message to the server, and then we will wait a response, twice:

$client->send('foobar');
$client->receive();

$client->send('bazqux');
$client->receive();

$client->close();

The Hoa\Websocket\Client::receive method has no argument.

Note about the handshake and the host

In order the handshake to be complete, it is necessary to send a Host HTTP header, representing the name of the host. When the client is executed through an HTTP server, the host of the server will be used if available. Else, if it is not available, or if we execute the client in a command line for example, we have to specify a host with the Hoa\Websocket\Client::setHost method, before connecting the client (before the calls of Hoa\Websocket\Connection::run or Hoa\Websocket\Client::connect); thus:

$client->setHost('hoa-project.net');
$client->connect();

// …

To know if the host is known, we can use the Hoa\Websocket\Client::getHost method. It will return null if the host is unknown. Or, during the handshake, an exception of type Hoa\Websocket\Exception\Exception will be thrown.

Customized node

The Hoa\Socket\Server and Hoa\Socket\Client classes work with nodes: Objects representing an opened connection. The main class representing a node is Hoa\Socket\Node. The Hoa\Websocket library provides its own node: Hoa\Websocket\Node. We can extend this class in order to add and manipulate some information about a connection.

For example, in the case of an instant messaging, we could store the pseudo of the client:

class ChatNode extends Hoa\Websocket\Node
{
    protected $_pseudo = null;

    public function setPseudo ($pseudo)
    {
        $old           = $this->_pseudo;
        $this->_pseudo = $pseudo;

        return $old;
    }

    public function getPseudo()
    {
        return $this->_pseudo;
    }
}

The Hoa\Socket\Server::setNodeName or Hoa\Socket\Client::setNodeName methods allow to specify what node class the server or the client will use:

$server = new Hoa\Websocket\Server(
    new Hoa\Socket\Server('ws://127.0.0.1:8889')
);
$server->getConnection()->setNodeName('ChatNode');

Next, in our listeners for example, we will be able to use our getPseudo method:

$server->on('message', function (Hoa\Event\Bucket $bucket) {
    $node = $bucket->getSource()->getConnection()->getCurrentNode();

    var_dump($node->getPseudo());

    // …
});

If you set up a protocol by using WebSockets between your clients and your server, customized nodes will be very helpful to store extra recurrent information.

Conclusion

The Hoa\Websocket library allows to create WebSocket servers and clients for more interactivity in your applications. The server is easily extensible thanks to the notion of node which eases the storage and manipulation of useful data to create its own protocol.

An error or a suggestion about the documentation? Contributions are welcome!

Comments

menu