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.
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:
$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.$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
.$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.$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
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.
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).
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.
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:
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.
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); }
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.
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.
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.
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:
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); } } }
3.15.182.62