Create Live Group Chat With PHP, jQuery & WebSocket


With the introduction of HTML5, a new technology was evolved in 2011 called WebSockets. This technology enables live connection with the server even after the page finished loading. It’s a better, replaceable version of AJAX for client to server communication in the background.

With WebSocket technology, its possible to have a direct communication between server and client without any interruption and faster data transmission.

So, to demonstrate this to me and for you, We’re going to create a live group chat with PHPjQuery with the help of WebSockets.

Introduction

Here are the main components of the chat we’re going to create :

You should download the libraries from the hyperlinks of the above components. You should use Composer for downloading the Socketo library.

Directory tree

There are only two directories, “cdn” and “inc”. The client side files like JavaScript and CSS are in the “cdn” directory. The PHP files for the WebSocket server is in the “inc” directory.

Note that, you should download Ratchet library using Composer in the “inc” directory. So, there will be a subfolder in the “inc” directory called “vendor”.

Database

We only need one table for storing messages called “wsMessages” :

CREATE TABLE IF NOT EXISTS `wsMessages` (
    `name` varchar(20) NOT NULL,
    `msg` text NOT NULL,
    `posted` varchar(20) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

The Interface

First, we are going to make the interface ie the HTML. This is the index.php file :

<?php
include "config.php";
?>
<!DOCTYPE html>
<html>
    <head>
        <script src="cdn/jquery.js"></script>
        <script src="cdn/ws.js"></script>
        <script src="cdn/chat.js"></script>
        <link href="cdn/chat.css" rel="stylesheet"/>
        <title>PHP Group Chat With jQuery & WebSocket</title>
    </head>
    <body>
        <div id="content" style="margin-top:10px;height:100%;">
            <center><h1>Live Group Chat In PHP</h1></center>
            <div class="chatWindow">
                <div class="users"></div>
                <div class="chatbox">
                    <div class="status">Offline</div>
                    <div class="chat">
                        <div class="msgs"></div>
                        <form id="msgForm">
                            <input type="text" size="30" />
                            <button>Send</button>
                        </form>
                    </div>
                    <div class="login">
                        <p>Type in your name to start chatting !</p>
                        <form id="loginForm">
                            <input type="text" />
                            <button>Submit</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </body>
</html>

As you can see, there’s a chat box and a login box with online users’ display box. We load jQueryws.jschat.js and chat.css in this page. Also, this is the only page the user needs to see for everything.

jQuery

jQuery must be of version 2.1.1. But, I think it will be compatible with versions 1.7 or more.

ws.js

The jQuery websocket plugin we mentioned in the Introduction of this post must be in this file.

chat.js

This is the file which connect to WebSocket server, login the user, sends messages and all the things that does on the client side.

window.scTop = function(){
    $(".chatWindow .chatbox .msgs").animate({
        scrollTop : $(".chatWindow .chatbox .msgs")[0].scrollHeight
    });
};
window.connect = function(){
    window.ws = $.websocket("ws://127.0.0.1:8080/", {
        open: function() {
            $(".chatWindow .chatbox .status").text("Online");
            ws.send("fetch");
        },
        close: function() {
            $(".chatWindow .chatbox .status").text("Offline");
        },
        events: {
            fetch: function(e) {
                $(".chatWindow .chat .msgs").html('');
                $.each(e.data, function(i, elem){
                    $(".chatWindow .chat .msgs").append("<div class='msg' title='"+ elem.posted +"'><span class='name'>"+ elem.name +"</span> : <span class='msgc'>"+ elem.msg +"</span></div>");
                });
                scTop();
            },
            onliners: function(e){
                $(".chatWindow .users").html('');
                $.each(e.data, function(i, elem){
                    $(".chatWindow .users").append("<div class='user'>"+ elem.name +"</div>");
                });
            },
            single: function(e){
                var elem = e.data;
                $(".chatWindow .chat .msgs").append("<div class='msg' title='"+ elem.posted +"'><span class='name'>"+ elem.name +"</span> : <span class='msgc'>"+ elem.msg +"</span></div>");
                scTop();
            }
        }
    });
};
$(document).ready(function(){
    $(".chatWindow .chat #msgForm").on("submit", function(e){
        e.preventDefault();
        var form = $(this);
        var val  = $(this).find("input[type=text]").val();
        if(val != ""){
            ws.send("send", {"msg": val});
            form[0].reset();
        }
    });
    $(".chatWindow .login #loginForm").on("submit", function(e){
        e.preventDefault();
        var val  = $(this).find("input[type=text]").val();
        if(val != ""){
            ws.send("register", {"name": val});
            ws.send("fetch");
            $(".chatWindow .login").fadeOut(1000, function(){
                $(".chatWindow .chat").fadeIn(1000, function(){
                    scTop();
                    $(".chatWindow .chat #msgForm input[type=text]").focus();
                });
            });
        }
    });
    $(".chatWindow .chatbox .status").on("click", function(){
        if($(this).text() == "Offline"){
            connect();
        }
    });
    setInterval(function(){
        ws.send("onliners");
    }, 4000);
    connect();
});

We connect to server using window.connect() function which can also be called as connect() function and the WebSocket plugin object is stored in window.ws variable.

The URL of the server is mentioned in the above script as “127.0.0.1:8080”. We will get to it later.

There is a function that is called every 4 seconds, to update the user’s online presence and fetching the online users from the server.

But, there might be some errors in the console, because this interval function is sometimes called before the connection to the server is established. So, don’t care about those errors.

There is also a small box which shows the status of the connection between server and client. If it’s “Offline” and the user clicks it, connect() function is called to try again to establish a connection.

This file also has code that responds with the messages the server sends to the client like online status of users, messages sent by other users etc.. This can be seen in the value of events key of JSON array in $.websocket function.

chat.css

Now, let’s style the chat page :

.chatWindow .users, .chatWindow .chatbox{
    display:inline-block;
    vertical-align:top;
    height:330px;
    padding: 0px 15px;
    position:relative;
}
.chatWindow .users{
    background: #6AAEEC;
    color:white;
    width: 100px;
    padding: 10px 15px;
    overflow-y:auto;
}
.chatWindow .chatbox{
    color:black;
    width: 330px;
}
.chatWindow .chatbox .status{
    background: #ABC8F8;
    padding: 5px 15px;
}
.chatWindow .chatbox .chat {
    display: none;
    background:#fff;
}
.chatWindow .chatbox .chat .msgs{
    border-top: 1px solid black;
    border-bottom: 1px solid black;
    overflow-y: auto;
    height: 300px;
}
.chatWindow .chatbox #msgForm{
    padding-top: 1.5px;
}
.chatWindow .msgs .msg, .chat .users .user{
    border-bottom:1px solid black;
    padding:4px 10px;
    white-space:pre-line;
    word-break:break-word;
}

All the element selectors in the stylesheet starts with .chatWindow to avoid applying CSS rules to other elements.

config.php

The configuration for connecting to database is in here. Also, this file is used / required by some of the other PHP files. Each time, this file is called, it sends a request to start the WebSocket server if it’s not already started.

<?php
// ini_set("display_errors","on");
$docRoot    = realpath(dirname(__FILE__));
if( !isset($dbh) ){
    session_start();
    date_default_timezone_set("UTC");
    $musername  = "root";
    $mpassword  = "backstreetboys";
    $hostname   = "localhost";
    $dbname     = "test";
    $dbh        = new PDO("mysql:dbname={$dbname};host={$hostname};port={$port}",$musername, $mpassword);
    /* Change The Credentials to connect to database. */
    include_once "$docRoot/inc/startServer.php";
}
?>

The files you’re going to see below should be in the “inc” directory.

serverStatus.txt

A normal text file. This file is used to determine whether the server was started (1) or it isn’t running currently (0). As a default value, set the file content to “0”. It is required for this file to have the permission of Read & Write (0666).

class.chat.php

The operations done when a user connects to the server and all the actions done between the server and client is in this file :

<?php
use RatchetMessageComponentInterface;
use RatchetConnectionInterface;

class ChatServer implements MessageComponentInterface {
    protected $clients;
    private $dbh;
    private $users = array();
    
    public function __construct() {
        global $dbh, $docRoot;
        $this->clients   = new SplObjectStorage;
        $this->dbh       = $dbh;
        $this->root  = $docRoot;
    }
    
    public function onOpen(ConnectionInterface $conn) {
        $this->clients->attach($conn);
        $this->send($conn, "fetch", $this->fetchMessages());
        $this->checkOnliners();
        echo "New connection! ({$conn->resourceId})n";
    }

    public function onMessage(ConnectionInterface $from, $data) {
        $id   = $from->resourceId;
        $data = json_decode($data, true);
        if(isset($data['data']) && count($data['data']) != 0){
            $type = $data['type'];
            $user = isset($this->users[$id]) ? $this->users[$id]['name'] : false;
            if($type == "register"){
                $name = htmlspecialchars($data['data']['name']);
                $this->users[$id] = array(
                    "name"  => $name,
                    "seen"  => time()
                );
            }elseif($type == "send" && $user !== false){
                $msg = htmlspecialchars($data['data']['msg']);
                $sql = $this->dbh->prepare("INSERT INTO `wsMessages` (`name`, `msg`, `posted`) VALUES(?, ?, NOW())");
                $sql->execute(array($user, $msg));
                foreach ($this->clients as $client) {
                    $this->send($client, "single", array("name" => $user, "msg" => $msg, "posted" => date("Y-m-d H:i:s")));
                }
            }elseif($type == "fetch"){
                $this->send($from, "fetch", $this->fetchMessages());
            }
        }
        $this->checkOnliners($from);
    }

    public function onClose(ConnectionInterface $conn) {
        if( isset($this->users[$conn->resourceId]) ){
            unset($this->users[$conn->resourceId]);
        }
        $this->clients->detach($conn);
    }

    public function onError(ConnectionInterface $conn, Exception $e) {
        $conn->close();
    }
    
    /* My custom functions */
    public function fetchMessages(){
        $sql = $this->dbh->query("SELECT * FROM `wsMessages`");
        $msgs = $sql->fetchAll();
        return $msgs;
    }
    
    public function checkOnliners($curUser = ""){
        date_default_timezone_set("UTC");
        if( $curUser != "" && isset($this->users[$curUser->resourceId]) ){
            $this->users[$curUser->resourceId]['seen'] = time();
        }
        
        $curtime    = strtotime(date("Y-m-d H:i:s", strtotime('-5 seconds', time())));
        foreach($this->users as $id => $user){
            $usertime   = $user['seen'];
            if($usertime < $curtime){
                unset($this->users[$id]);
            }
        }
        
        /* Send online users to evryone */
        $data = $this->users;
        foreach ($this->clients as $client) {
            $this->send($client, "onliners", $data);
        }
    }
    
    public function send($client, $type, $data){
        $send = array(
            "type" => $type,
            "data" => $data
        );
        $send = json_encode($send, true);
        $client->send($send);
    }
}
?>

Since the server is always running, we store the online users in an array variable inside the ChatServer class called “users”. Each user that connects to the server has a unique connection code. This is also added within the array of variable. But, a new item is only added when the registration data is sent to server. An example item of ChatServer::users array :

15 => array(
 "name" => "Subin",
 "seen" => 1410624788
)

The “seen” key is for storing the timestamp of the users’ last online presence. If this time is less than that of 5 seconds of current time, the user is removed as he is offline. See ChatServer::checkOnliners() function for seeing this.

The user registration, message sending and message fetching are all taken care by the ChatServer::onMessage() function.

The messages sent by the user and the name of user is filtered for HTML code to prevent XSS injection.

There are no cookie or session in this. If the user closes the browser window, then he/she is eliminated from the online users and have to re register for chatting again.

ChatServer::checkOnliners() also updates the “seen” timestamp value of the current user each time it’s called.

server.php

This is the script that starts the WebSocket server :

<?php
use RatchetServerIoServer;
use RatchetHttpHttpServer;
use RatchetWebSocketWsServer;

function shutdown(){
    global $docRoot;
    file_put_contents("$docRoot/inc/serverStatus.txt", "0");
    require_once "$docRoot/inc/startServer.php";
}
register_shutdown_function('shutdown');
if( isset($startNow) ){
    require_once "$docRoot/inc/vendor/autoload.php";
    require_once "$docRoot/inc/class.chat.php";
    $server = IoServer::factory(
          new HttpServer( 
            new WsServer(
              new ChatServer()
            )
          ), 
          8080,
          "127.0.0.1"
        );
    $server->run();
}
?>

The above script listens requests to the port 8080 of IP 127.0.0.1 or localhost. So, any request to 127.0.0.1:8080 is processed by the above script. If you want to change the port and URL, change it in this file and also in the cdn/chat.js file.

In any case the above script was terminated, it automatically starts back due to the callback used in the register_shutdown_function(). This file can’t operate on it’s own, because it needs config.php which has been not yet included in this file, because actually this file is used by another one called bg.php.

bg.php

To run the server, this file should be ran :

<?php
require_once realpath(__DIR__) . "/../config.php";
$startNow = 1;
include_once "$docRoot/inc/server.php";
?>

You can run this file by the following command in Linux :

php bg.php

startServer.php

The server is automatically started when the user visits the chat page if it’s not already started. This file starts the server if it’s not started in the background. This file is called by the config.php file.

<?php
require_once realpath(dirname(__DIR__)) . "/config.php";
$statusFile = "$docRoot/inc/serverStatus.txt";
$status = file_get_contents($statusFile);
if($status == "0"){
    /* This means, the WebSocket server is not started. So we, start it */
    function execInbg($cmd) { 
        if (substr(php_uname(), 0, 7) == "Windows"){ 
            pclose(popen("start /B ". $cmd, "r"));  
        } else { 
            exec($cmd . " > /dev/null &");   
        } 
    }
    execInbg("php $docRoot/inc/bg.php");
    file_put_contents($statusFile, 1);
}
?>

If the content of the “serverStatus.txt” file is “0”, then a command is executed by this file to run the bg.php file in the background. This means that the server started running in the background. If it started running, then the file’s contents is changed to “1” and this file won’t do anything if the value is “1”.

The command executed by this script is different for both Linux and Windows.

Thus the server is started and the client can start communicating with the server and the chatting works. This tutorial is completed. If you encountered any problems, please comment and I’ll be here (most of the times) to help you. Cheers ! 🙂