Laravel Websocket from scratch.

Hello there guys.

In my current Laravel project I had to implement Websockets so I started to follow Laravel’s documentation, which is pretty good, but there is just so much to configure and so many packages to add that I started to look into alternatives. Basically I didn’t want to add Supervisor to the project architecture because I just want to keep third party applications to the minimum. Also I didn’t want to go with Ratchet either because its requirements.

With that said I narrowed my known solutions to basically do it all with javascript. In this post I will tell you how I setup the socket and how I configure nginx in order to handle all request through port 80.

As the project is in Laravel I use Homestead for development. All sites are served with nginx and php-fpm on port 80. In addition the project uses Redis for caching and from now on, to publish notifications to subscribed clients (it has this feature too, pretty cool right?).

But lets begin, Homestead already comes with Redis, Nginx, php-fpm and Node so I won’t cover the part of their installation. First, we define all dependencies needed by the project and then we install them:

  1. Ssh into Homestead machine and go to vagrant shared folder, in my case is/home/vagrant/code and run:
    laravel new Socket
  2. Go to Socket folder
    cd Socket
  3. Edit your package.json file at the root level of your Laravel project accordingly
    "scripts": {
      .... 
      "start": "pm2 start --name 'socket' ./websocketServer.js"
     },
    "dependencies": { 
      "express": "^4.12.3",
      "redis": "^0.12.1",
      "socket.io": "^1.3.5"
    }
  4. Go to the project folder in Homestead machine and add Predis package, run:
    composer require predis/predis

    Install all node dependencies, run:

    npm install
  5.  Install pm2 globally:
    sudo npm i -g pm2
  6. Create a file websocketServer.js at the root folder of the project with the following content:
    var app = require('express')();
    var server = require('http').Server(app);
    var io = require('socket.io')(server, {path: '/websocket'});
    var redis = require('redis');
    
    server.listen(8875);
    io.on('connection', function (socket) {
    
     var redisClient = redis.createClient();
     redisClient.subscribe('alerts');
     redisClient.on('message', function(channel, message) {
       socket.emit(data.channel, message);
     });
    
     socket.on('disconnect', function() {
       redisClient.quit();
     });
    });
  7. Now we create an nginx configuration file to rule them all in /etc/nginx/sites-available/socket.local with this content:
    upstream websocket {
     server 127.0.0.1:8875;
    }
    server {
     listen 80;
     server_name local.socket.com;
     index index.html index.php;
     root "/home/vagrant/code/Socket/public";
     charset utf-8;
    
     location / {
     try_files $uri $uri/ /index.php?$query_string;
     }
    
     location = /favicon.ico { access_log off; log_not_found off; }
     location = /robots.txt { access_log off; log_not_found off; }
     location ~ \.php$ {
     fastcgi_split_path_info ^(.+\.php)(/.+)$;
     fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
     fastcgi_index index.php;
     include fastcgi_params;
     fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
     fastcgi_intercept_errors off;
     fastcgi_buffer_size 16k;
     fastcgi_buffers 4 16k;
     fastcgi_connect_timeout 300;
     fastcgi_send_timeout 300;
     fastcgi_read_timeout 300;
     }
    
     location ~* ^/(socket) {
     proxy_set_header Host $http_host;
     proxy_pass http://websocket;
     proxy_http_version 1.1;
     proxy_set_header Upgrade $http_upgrade;
     proxy_set_header Connection "upgrade";
     }
    }
  8. Now lets create the symlink for nginx to serve the new site:
    sudo ln -s /etc/nginx/sites-available/socket.local /etc/nginx/sites-enabled/socket.local
  9. Restart nginx:
    sudo systemctl restart nginx
  10. While on the root project folder, lets run our Socket.io server:
    npm start

    That will tell pm2, as defined in package.json, to start our Socket.io server and listen in port 8875.

  11. Now we need our guest machine to resolve http://local.socket.com, for that you can add in host machine (not Homestead machine) /etc/hosts file
    192.168.10.10  local.socket.com # Homestead IP in ~/.homestead/Homestead.yaml

    After this step, you can browse http://local.socket.com in your browser, it will show homepage of your Laravel project.

Now after aaaaaaalllllll that, we can start coding what we need.

First lets add all routes we will need to create an event, notify the event to the socket server and finally receive the notification. In your Socket/routes/web.php place this piece of code:

Route::get('/', '[email protected]');
Route::post('/notify', '[email protected]');
Route::get('/client', '[email protected]');

Now lets create Socket controller by ssh into Homestead and from Socket folder run:

php artisan make:controller SocketController

Now edit Socket/app/Http/Controllers/SocketController.php and add these three actions:

public function index()
{
    return view('form');
}

public function notify(Request $request)
{
    $data = $request->all();

    $alert = json_encode([
        'text' => $data['text'],
        'type' => $data['type']
    ]);

    $redis = \Redis::connection();
    $redis->publish('alerts', $alert);

    return redirect('/');
}

public function client($type)
{
    return view('client', ['channel' => $type]);
}

Now lets create the 2 views above actions use:

form.blade.php

@extends('welcome')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-offset-2 col-md-6">
                <form class="form-horizontal" action="/notify" method="POST">
                    {{ csrf_field() }}
                    <div class="form-group">
                        <label class="col-sm-2  control-label" for="message">Text</label>
                        <div class="col-md-10">
                            <input type="text" class="form-control" name="text" placeholder="text">
                        </div>
                    </div>
                    <div class="form-group">
                        <label class="col-sm-2 control-label" for="type">Password</label>
                        <div class="col-md-10">
                            <div class="radio">
                                <label>
                                    <input type="radio" name="type" value="success" checked>Success
                                </label>
                            </div>
                            <div class="radio">
                                <label>
                                    <input type="radio" name="type" value="info">Info
                                </label>
                            </div>
                            <div class="radio">
                                <label>
                                    <input type="radio" name="type" value="warning">Warning
                                </label>
                            </div>
                            <div class="radio">
                                <label>
                                    <input type="radio" name="type" value="danger">Danger
                                </label>
                            </div>
                        </div>
                    </div>
                    <div class="form-group">
                        <div class="col-sm-offset-2 col-sm-10">
                            <button type="submit" class="btn btn-default">Submit</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
@endsection

client.blade.php

@extends('welcome')

@section('content')
    <script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
    <script src="//code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
    <script src="https://cdn.socket.io/socket.io-1.3.4.js"></script>

    <div class="container">
        <div class="row text-center">
            <div class="col-md-6 col-lg-6">
                <div id="alerts" ></div>
            </div>
        </div>
    </div>
    <script>
      var socket = io.connect('http://local.socket.com', { path: '/socket'});

      socket.on('{{ $channel }}', function (data) {
        var alert = JSON.parse(data);
        console.log(alert);
        $( "#alerts" ).append('<div class="alert alert-'+ alert.type +'" role="alert">' + alert.text + '</div>');
      });
    </script>


@endsection

Edit welcome.blade.php and replace its content with the following

<!doctype html>
<html lang="{{ config('app.locale') }}">
    <head>
        <meta charset="utf-8">
        <title>Socket</title>
        <link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css' rel='stylesheet' type='text/css'>
    </head>
    <body>
        @yield('content')
    </body>
</html>

We are one step away from our goal, now we need to tell Laravel to connect to Redis. In order to do that just edit .env and add this line (or edit if it already in there).

BROADCAST_DRIVER=redis

If your project already connects to Redis because it uses for cache or db, then this last step is optional.

You now have 2 urls available in your browser.

  1. http://local.socket.com/
  2. http://local.socket.com/client/info . You can replace “info” with any word here, but you’ll only see results are the ones in the form displayed at the homepage: success, info, danger and warning

When you type something in the input and choose the alert type you want to publish, that message is sent to all socket clients listening for that type of messages.

Here is the list of technologies that we used to build all this:

  1. Laravel Homestead: it already has all applications installed but pm2.
  2. Nginx: serves php pages and handle socket requests and responses.
  3. Socket.io: server and client. The server is a standalone script that listens all requests coming from a given port (this actually is done by express) and when a connection is establish, it subscribes to the specified Redis channel. This channel must be the same one specified in [email protected]
  4. Laravel Predis to handle Redis connection.

The workflow of the entire process is like this:

First, submit the information you want to broadcast, that is done by the form. Then /notify action takes all that info, process it and publish it to Redis channel. Basically, it does that with these 2 lines:

$redis = \Redis::connection();
$redis->publish('alerts', $alert);

Then Redis notifies all clients subscribed to that channel, in this case, our websocketServer script which subscribes to the channel once the connection is done (when the /client/info url is loaded).

The /client url only connects to the websocket url (http://local.socket.com/socket) all socket communication under this protocol is done on this url. Notice that /socket is the same rule we added in nginx configuration file. All request coming from /socket will be handle by the websocket Upstream.

In order to make each alert type to only “listen” for its own messages what I did was to tell which kind of messages the client has to take care about. It was pretty easy defining that variable in the route.

PM2 is only to keep websocketServer script running all the time.

There must better ways to do this I know but I haven’t research all this very deep as it is the first time I implement all this.

I hope this would be helpful for you if you need implement something similar.

Here are some links I use to build all this up:

Here is the link to our GitLab repo

Cya on my next post.

4 comments on “Laravel Websocket from scratch.Add yours →

  1. Está excelente, no se me ocurrió. Tengo un proyecto donde los usuarios generan notificaciones con un stack similar (Javascript/NodeJS + PHP).
    Terminé haciendo 2 peticiones desde el browser, hacia PHP y Socket.IO, para guardar el evento en la DB y para distribuir el mensaje el tiempo real.
    Ahora voy a cambiar a hacer algo similar y hacer un sólo viaje hacia el server PHP, y que PHP envíe el mensaje a Socket.IO a través de Redis!
    Gracias por compartir!

Leave a Reply

Your email address will not be published. Required fields are marked *