Implementing the chat application

After this short introduction in the development with WebSockets, let us now begin implementing the actual chat application. The chat application will consist of the server-side application built in PHP with Ratchet, and an HTML and JavaScript-based client that will run in the user's browser.

Bootstrapping the project server-side

As mentioned in the previous section, applications based on ReactPHP will achieve the best performance when used with an event-loop extension such as libevent or ev. Unfortunately, the libevent extension is not compatible with PHP 7, yet. Luckily, ReactPHP also works with the ev extension, whose latest version already supports PHP 7. Just like in the previous chapter, we'll be working with Docker in order to have a clean software stack to work on. Start by creating a Dockerfile for your application container:

FROM php:7 
RUN pecl install ev-beta &&  
    docker-php-ext-enable ev 
WORKDIR /opt/app 
CMD ["/usr/local/bin/php", "server.php"] 

You will then be able to build an image from this file and start the container using the following CLI command from within your project directory:

$ docker build -t packt-chp6
$ docker run -d --name chat-app -v $PWD:/opt/app -p 8080:8080 
      packt-chp6

Note that this command will not actually work as long as there is no server.php file in your project directory.

Just as in the previous example, we will be using Composer as well for dependency management and for autoloading. Create a new folder for your project and create a composer.json file with the following contents:

{ 
    "name": "packt-php7/chp6-chat", 
    "type": "project", 
    "authors": [{ 
        "name": "Martin Helmich", 
        "email": "[email protected]" 
    }], 
    "require": { 
        "php": ">= 7.0.0", 
        "cboden/ratchet": "^0.3.4" 
    }, 
    "autoload": { 
        "psr-4": { 
            "Packt\Chp6": "src/" 
        } 
    } 
} 

Continue by installing all required packages by running composer install in your project directory and create a provisional server.php file with the following contents:

<?php 
require_once 'vendor/autoload.php'; 
 
$app = new RatchetApp('localhost', 8080, '0.0.0.0'); 
$app->run(); 

You have already used the RatchetApp constructor in the introductory example. A few words concerning this class' constructor parameters:

  • The first parameter, $httpHost is the HTTP hostname at which your application will be available. This value will be used as the allowed origin host. This means that when your server is listening on localhost, only JavaScript running on the localhost domain will be allowed to connect to your WebSocket server.
  • The $port parameter is specified at which port your WebSocket server will listen on. Port 8080 will suffice for now; in a later section, you will learn how you can safely configure your application to be available on the HTTP standard port 80.
  • The $address parameter describes the IP address the WebSocket server will listen on. This parameter's default value is '127.0.0.1', which would allow clients running on the same machine to connect to your WebSocket server. This won't work when you are running your application in a Docker container. The string '0.0.0.0' will instruct the application to listen on all available IP addresses.
  • The fourth parameter, $loop, allows you to inject a custom event loop into the Ratchet application. If you do not pass this parameter, Ratchet will construct its own event loop.

You should now be able to start your application container using the following command:

$ docker run --rm -v $PWD:/opt/app -p 8080:8080 packt-chp6

Tip

As your application is now one single, long-running PHP process, changes to your PHP code base will not become effective until you restart the server. Keep in mind that you stop the server using Ctrl + C and restart it using the same command (or using the docker restart chat-app command) when making changes to your application's PHP code.

Bootstrapping the HTML user interface

The user interface for our chat application will be based on HTML, CSS, and JavaScript. For managing frontend dependencies, we will be using Bower in this example. You can install Bower using npm with the following command (as root or with sudo):

$ npm install -g bower

Continue by creating a new directory public/ in which you can place all your frontend files. In this directory, place a file bower.json with the following contents:

{ 
    "name": "packt-php7/chp6-chat", 
    "authors": [ 
        "Martin Helmich <[email protected]>" 
    ], 
    "private": true, 
    "dependencies": { 
        "bootstrap": "~3.3.6" 
    } 
} 

After creating the bower.json file, you can install the declared dependencies (in this case, the Twitter Bootstrap framework) using the following command:

$ bower install

This will download the Bootstrap framework and all its dependencies (actually, only the jQuery library) into the directory bower_components/, from which you will be able to include them in your HTML frontend files later.

It's also useful to have a web server up and running that can serve your HTML frontend files. This is especially important when your WebSocket application is restricted to a localhost origin, which will only allow requests from JavaScript served from the localhost domain (which does not include local files opened in a browser). One quick and easy way is to use the nginx Docker image. Be sure to run the following command from within your public/ directory:

$ docker run -d --name chat-web -v $PWD:/var/www -p 80:80 nginx

After that, you will be able to open http://localhost in your browser and view the static files from your public/ directory. If you place an empty index.html in that directory, Nginx will use that page as an index page that will not need to be explicitly requested by its path (meaning that http://localhost will serve the contents of the file index.html to the user).

Building a simple chat application

You can now start implementing the actual chat application. As already shown in the previous examples, you need to implement RatchetMessageComponentInterface for this. Start by creating a PacktChp6ChatChatComponent class and implementing all methods that are required by the interface:

namespace PacktChp6Chat; 
 
use RatchetMessageComponentInterface; 
use RatchetConnectionInterface; 
 
class ChatComponent implements MessageComponentInterface 
{ 
    public function onOpen(ConnectionInterface $conn) {} 
    public function onClose(ConnectionInterface $conn) {} 
    public function onMessage(ConnectionInterface $from, $msg) {} 
    public function onError(ConnectionInterface $conn, Exception $err) {} 
} 

The first thing that the chat application needs to do is to keep track of connected users. For this, you will need to maintain a collection of all open connections, add new connections when a new user connects, and remove them when a user disconnects. For this, initialize an instance of the SplObjectStorage class in the constructor:

private $users; 
 
public function __construct() 
{ 
    $this->users = new SplObjectStorage(); 
} 

You can then attach new connections to this storage in the onOpen event and remove them in the onClose event:

public function onOpen(ConnectionInterface $conn) 
{ 
    echo "user {$conn->remoteAddress} connected.
";

    $this->users->attach($conn); 
} 
 
public function onClose(ConnectionInterface $conn) 
{ 
    echo "user {$conn->remoteAddress} disconnected.
";

    $this->users->detach($conn);} 

Each connected user can now send messages to the server. For each received message, the component's onMessage method will be called. To implement a real chat application, each received message needs to be relayed to the other users-conveniently, you already have a list of all connected users in your $this->users collection to whom you can then send the received message:

public function onMessage(ConnectionInterface $from, $msg) 
{ 
    echo "received message '$msg' from user {$from->remoteAddress}
";
    foreach($this->users as $user) {

        if ($user != $from) {

            $user->send($msg);

        }

    }} 

You can then register your chat component at the Ratchet application in your server.php file:

$app = new RatchetApp('localhost', 8080, '0.0.0.0'); 
$app->route('/chat', new PacktChp6ChatChatComponent); 
$app->run(); 

After restarting your application, test the chat functionality by opening two WebSocket connections with wscat in two separate terminals. Each message that you send in one connection should pop up in the other.

Building a simple chat application

Testing the rudimentary chat application using two wscat connections

Now that you have an (admittedly, still rudimentary) chat server running, we can start building the HTML frontend for the chat application. For the beginning, a static HTML file will be completely sufficient for this. Begin by creating an empty index.html file in your public/ directory:

<!DOCTYPE html>
<html> 
  <head> 
    <title>Chat application</title> 
    <script src="bower_components/jquery/dist/jquery.min.js"></script>

    <script src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/> 
  </head> 
  <body> 
  </body> 
</html> 

In this file, we are already including the frontend libraries that we'll use for this example; the Bootstrap framework (with one JavaScript and one CSS file) and the jQuery library (with one other JavaScript file).

As you will be writing a fair amount of JavaScript for this application, it is also useful to add another instance of a js/app.js file in which you can place your own JavaScript code to the <head> section of the HTML page:

<head> 
  <title>Chat application</title> 
  <script src="bower_components/jquery/dist/jquery.min.js"></script> 
  <script src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script> 
  <script src="js/app.js"></script> 
  <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/> 
</head> 

You can then continue by building a minimalist chat window in the <body> section of your index.html file. All you need to get started is an input field for writing messages, a button for sending them, and an area for displaying other user's messages:

<div class="container"> 
  <div class="row"> 
    <div class="col-md-12"> 
      <div class="input-group"> 
        <input class="form-control" type="text" id="message"  placeholder="Your message..." /> 
        <span class="input-group-btn"> 
          <button id="submit" class="btn btn-primary">Send</button> 
        </span> 
      </div> 
    </div> 
  </div> 
  <div class="row"> 
    <div id="messages"></div> 
  </div> 
</div> 

The HTML file contains an input field (id="message") in which a user can enter new chat messages, a button (id="submit") to submit the message, and a (currently still empty) section (id="messages") in which the messages received from other users can be displayed. The following screenshot shows how this page will be displayed in the browser:

Building a simple chat application

Of course, all of this will not be any good without the appropriate JavaScript to actually make the chat work. In JavaScript, you can open a WebSocket connection by using the WebSocket class.

Note

On browser support WebSockets are supported in all modern browsers and have been for quite some time. You may run into issues where you need to support older Internet Explorer versions (9 and below), which do not support WebSockets. In this case, you can use the web-socket-js library, which internally uses a fallback using Flash, which is also well supported by Ratchet.

In this example, we will be placing all our JavaScript code in the file js/app.js in the public/ directory. You can open a new WebSocket connection by instantiating the WebSocket class with the WebSocket server's URL as the first parameter:

var connection = new WebSocket('ws://localhost:8080/chat'); 

Just like the server-side component, the client-side WebSocket offers several events that you can listen on. Conveniently, these events are named similarly to the methods used by Ratchet, onopen, onclose, and onmessage, all of which you can (and should) implement in your own code:

connection.onopen = function() { 
    console.log('connection established'); 
} 
 
connection.onclose = function() { 
    console.log('connection closed'); 
} 
 
connection.onmessage = function(event) { 
    console.log('message received: ' + event.data); 
} 

Receiving messages

Each client connection will have a corresponding ConnectionInterface instance in the Ratchet server application. When you call a connection's send() method on the server, this will trigger the onmessage event on the client side.

Each time a new message is received; this message should be displayed in the chat window. For this, you can implement a new JavaScript method appendMessage that will display a new message in the previously created message container:

var appendMessage = function(message, sentByMe) { 
    var text = sentByMe ? 'Sent at' : 'Received at'; 
     var html = $('<div class="msg">' + text + ' <span class="date"></span>: <span 
    class="text"></span></div>'); 
 
    html.find('.date').text(new Date().toLocaleTimeString()); 
    html.find('.text').text(message); 
 
    $('#messages').prepend(html); 
} 

In this example, we are using a simple jQuery construct to create a new HTML element and populate it with the current date and time and the actual message text received. Be aware that a single message currently only consists of the raw message text and does not yet contain any kind of meta data, such as an author or other information. We'll get to that later.

Tip

While creating HTML elements with jQuery is sufficient in this case, you might want to consider using a dedicated templating engine such as Mustache or Handlebars in a real-world scenario. Since this is not a JavaScript book, we will be sticking to the basics here.

You can then call the appendMessage method when a message is received:

connection.onmessage = function(event) { 
    console.log('message received: ' + event.data); 
    appendMessage(event.data, false); 
} 

The event's data property contains the entire received message as a string and you can use it as you see fit. Currently, our chat application is only equipped to handle plain text chat messages; whenever you need to transport more or structured data, using JSON encoding is probably a good option.

Sending messages

To send messages, you can (unsurprisingly) use the connection's send() method. Since you already have the respective user input fields in your HTML file, all it needs now to get the first version of our chat working is a little more jQuery:

$(document).ready(function() { 
    $('#submit').click(function() { 
        var message = $('#message').val(); 
 
        if (message) { 
            console.log('sending message: "' + message + '"'); 
            connection.send(message); 
 
            appendMessage(message, true); 
        } 
    }); 
}); 

As soon as the HTML page is loaded completely, we begin listening on the submit button's click event. When the button is clicked, the message from the input field is sent to the server using the connection's send() method. Each time a message is sent, Ratchet will call the onMessage event in the server-side component, allowing the server to react to that message and to dispatch it to other connected users.

Usually, a user will want to see messages that they sent themselves in the chat window, too. That is why we are calling the appendMessage that was implemented previously, which will insert the sent message into the message container, just as if it was received from a remote user.

Testing the application

When both containers (web server and WebSocket application) are running, you can now test the first version of your chat by opening the URL http://localhost in your browser (better yet, open the page twice in two different windows so that you can actually use the application to chat with yourself).

The following screenshot shows an example of the result that you should get when testing the application:

Testing the application

Testing the first version of the chat application with two browser windows

Keeping the connection from timing out

When you keep the test site open for more than a few minutes, you might notice that eventually the WebSocket connection will be closed. This is because most browsers will close a WebSocket connection when no messages were sent or received in a certain time frame (usually five minutes). As you are working with long-running connections, you will also need to consider connectivity issues-what if one of your users uses a mobile connection and temporarily disconnects while using your application?

The easiest way to mitigate this is to implement a simple re-connect mechanism-whenever the connection is closed, wait a few seconds and then try again. For this, you can start a timeout in the onclose event in which you open a new connection:

connection.onclose = function(event) { 
    console.error(e); 
    setTimeout(function() { 
        connection = new WebSocket('ws://localhost:8080/chat'); 
    }, 5000); 
} 

This way, each time the connection is closed (due to a timeout, network connectivity problems, or any other reason); the application will try to re-establish the connection after a grace time of five seconds.

If you want to proactively prevent disconnects, you can also periodically send messages through the connection in order to keep the connection alive. This can be done by registering an interval function that periodically (in intervals smaller than the timeout) sends messages to the server:

var interval; 
 
connection.onopen = function() { 
    console.log('connection established'); 
    interval = setInterval(function() {

        connection.send('ping');

    }, 120000); 
} 
 
connection.onclose = function() { 
    console.error(e); 
    clearInterval(interval); 
    setTimeout(function() { 
        connection = new WebSocket('ws://localhost:8080/chat'); 
    }, 5000); 
} 

There are a few caveats to consider here: first of all, you should only start sending keep-alive messages after the connection was actually established (that is why we are registering the interval in the onopen event), and you should also stop sending keep-alives when the connection was closed (which can still happen, for example, when the network is not available), which is why the interval needs to be cleared in the onclose event.

Furthermore, you probably do not want keep-alive messages to be broadcast to the other connected clients; this means that these messages also need a special handling in the server-side component:

public function onMessage(ConnectionInterface $from, $msg) 
{ 
    if ($msg == 'ping') {

        return;

    } 
 
    echo "received message '$msg' from user {$from->remoteAddress}
"; 
    foreach($this->users as $user) { 
        if ($user != $from) { 
            $user->send($msg); 
        } 
    } 
} 
..................Content has been hidden....................

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