Laravel Websocket - Echo Server & Ionic

JC Tan avatar

JC Tan

Photo by Neven Krcmarek on Unsplash
Photo by Neven Krcmarek on Unsplash

In this article, we will go through how to implement a 'live-update' user interface using websocket.

We will be using Laravel Broadcast to "broadcast" your server side events over a websocket connection to your client-side Javascript application.

Important Note: This guide only covers setting up a workable websocket connection without authentication. Do look at Laravel Presense Channel if you want to have only authenticated users connect and listen to the channel.

At the very least, we would need to setup:

  1. Laravel Broadcast (using Redis broadcast driver on a Redis queue)

  2. Laravel Echo Server (socket.io server)

  3. Client Side
    i. Browser (socket.io-client)
    ii. Ionic (laravel-echo-client)

  4. Nginx as a Websocket Proxy

(1) Setup Laravel Broadcasting

Out of the box, Laravel supports a few driver for broadcasting: Pusher, Redis and a log driver for local development and debugging. We will use Redis driver in this article.

If you are using Laravel Homestead, Redis will be setup and no extra step is needed. You can verify that by typing redis-cli and ping it. If you get PONG back as a result, you are good to proceed. If not, you might need to spend sometime setting up Redis on your local machine or virtual box first.

Install Redis on Mac Using Homebrew
Install Redis on Ubuntu

image
image

Install the predis library to use the Redis driver.

composer require predis/predis

All event broadcasting is done via queued jobs. Instead of using the default QUEUE_DRIVER=sync, we will update it both of the drivers as below:

BROADCAST_DRIVER=redis
QUEUE_DRIVER=redis
(2) Setup Laravel Echo Server

We will broadcast events (along with data) to a socket.io server which will maintain a websocket connection with the clients.

As suggested by the official Laravel documentations, we will be using Laravel Echo Server.

Install the package globally.

npm install -g laravel-echo-server

Run the init command

laravel-echo-server init

The cli tool will help you setup a laravel-echo-server.json file in the root directory of your project. This file will be loaded by the server during start up. You may edit this file later on to manage the configuration of your server.

image
image

Execute the start command when you're done.

laravel-echo-server start
image
image

Let's create a simple Event to test things out. Create a new folder / file in the app directory.

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ChatEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;
    
    private $name;
    private $message;

    /**
     * ChatEvent constructor.
     * @param $name
     * @param $message
     */
    public function __construct($name, $message)
    {
        $this->name = $name;
        $this->message = $message;
    }

    public function broadcastOn()
    {
        return new Channel('chat');
    }

    public function broadcastAs()
    {
        return 'room.general';
    }

    public function broadcastWith()
    {
        return [
            'name' => $this->name,
            'message' => $this->message,
        ];
    }
}

Note that if you implement the ShouldBroadcast interface, Laravel will broadcast the job using queue as specified in your .env file. If you opt to use the sync driver all the time, you can change to implementing the ShouldBroadcastNow interface.

Let's manually trigger the event to ensure that the broadcasting reaches the Laravel Echo Server.

Open up a terminal at your Laravel root:

php artisan queue:listen

Sidenote: Ideally you would want to setup Supervisor to monitor and start working on the queue but now we can manually listen to the queue so we can see the processes in the Terminal.

Open another terminal to manually start laravel-echo-server:

laravel-echo-server start

Sidenote: Ideally we would also want Supervisor to monitor this for us, during development on my localhost I would like to see all the logs first hand on the Terminal without opening the log file. A sample supervisor config file can be found here.

Another terminal:

php artisan tinker
event(new App\Events\ChatEvent('JC', 'Hi'));
image
image

We should use supervisor to run the queue and start laravel-echo-server. For now if you can see the Channel and Event name appear in your Laravel Echo Server terminal after triggering the event in tinker mode, then you have successfully broadcast your event to the Echo server!

Note that if you wonder why the Channel name has been prefixed with laravel_database, it's defined in config/database.php in the redis->options->prefix array, you can set the prefix to an empty string here.

'redis' => [
        'client' => env('REDIS_CLIENT', 'predis'),
        'options' => [
            'cluster' => env('REDIS_CLUSTER', 'predis'),
            'prefix' => '',
        ],
(3) Client Side Setup

Next up, we will setup the client side in order to receive the broadcast.

(3)i. Browser

Install the necessary dependencies:

npm install --save laravel-echo socket.io-client

Setup the connection:

import Echo from "laravel-echo"
window.io = require('socket.io-client');

window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: window.location.hostname + ':6001'
});
npm run dev

We can now listen to the channel / events being broadcasted in the relevant page we want.

<html>
<head>
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <link href="{{ mix('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div class="container">
  <div class="row">
    <div class="col">
      <div class="card">
        <div id="chat-body" class="card-body">
          <div>Message will appear here...</div>
        </div>
      </div>
    </div>
  </div>

  <form class="mt-4">
    <div class="form-group row">
      <label for="name" class="col-sm-2 col-form-label">Name</label>
      <div class="col-sm-10">
        <input type="text" class="form-control" id="name">
      </div>
    </div>
    <div class="form-group row">
      <label for="message" class="col-sm-2 col-form-label">Message</label>
      <div class="col-sm-10">
        <input type="text" class="form-control" id="message">
      </div>
    </div>
    <button id="btn-post" class="btn btn-primary btn-block">Post</button>
  </form>
</div>

<script src=" {{ mix('js/app.js') }}"></script>
<script>
    window.Echo.channel('chat')
        .listen('.room.general', function (data) {
            console.log(data);
            let chatBody = document.getElementById('chat-body');
            chatBody.insertAdjacentHTML('beforeend', `<div>${data.name}: ${data.message}</div>`);
        });

</script>
</body>
</html>

You can manually broadcast an event using php artisan tinker and you should be able to see the message being broadcasted and updated instantaneously on your web browser.

php artisan tinker
event(new App\Events\ChatEvent('JC', 'Hi'));
(3)ii. Ionic

Using it with Ionic is pretty much very straight forward once you have the backend setup correctly. This should works with Ionic 3 and Ionic 4.

Install laravel-echo-ionic. Run this at your ionic root folder:

npm install --save laravel-echo-ionic

The code below shows how you can connect to the websocket server and listen to channels. The host parameter would be the combination of web socket server url and port, which you have initialised and can be found at laravel-echo-server.json.

You can try stopping your web socket server and see it goes the cycle of disconnect -> reconnecting -> connected.

Important Note: You should put this in a service class so that you can access it everywhere in your app.

import {Component} from '@angular/core';
import {Echo} from 'laravel-echo-ionic';

@Component({
    selector: 'page-hello-ionic',
    templateUrl: 'hello-ionic.html'
})
export class HelloIonicPage {
    echo: any = null;

    constructor() {
        this.echo = new Echo({
            broadcaster: 'socket.io',
            host: 'simple-chat.local:6001',
        });

        this.echo.connector.socket.on('connect', function () {
            console.log('CONNECTED');
        });

        this.echo.connector.socket.on('reconnecting', function () {
            console.log('CONNECTING');
        });

        this.echo.connector.socket.on('disconnect', function () {
            console.log('DISCONNECTED');
        });

        this.echo.channel('chat')
            .listen('.room.general', (data) => {
                console.log(data);
            });
    }
}
(4) Nginx as Websocket Proxy

A lot of companies or work places have secutiry protocols in place where only the common ports are open, e.g. 80 (HTTP), 443 (HTTPS). If we were to have our web app try connecting to port 6001 (by default of laravel-echo-server), definitely some of your users will encounter problems as the port is closed. You can setup a sub domain which is accessable with either port 80 or 443, internally it will route to the webserver.

Let's say you have a domain name of myblog.io.

  1. Add a sub domain ws.myblog.io.
    (Recommended) Run certbot (let's encrypt).

  2. Setup your laravel-echo-server as usual, running on port 6001.

The Nginx config should look as below:

server {
    server_name ws.myblog.com;
    location / {
    	proxy_pass             http://127.0.0.1:6001;
        proxy_set_header Host  $host;
        proxy_read_timeout     60;
        proxy_connect_timeout  60;
        proxy_redirect         off;

        # Allow the use of websockets
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/ws.myblog.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/ws.myblog.com.my/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

After this, remember to update your client side code to use the new url:

this.echo = new Echo({
                    broadcaster: 'socket.io',
	            host: 'https://ws.myblog.com', //Port is not needed here as the default port for HTTPS protocol will be 443.
        	});
Wrapping Up

That is it! It may seems daunting initially but once you figured out all the components involved, setting up and using websocket is not as difficult as it seems anymore. You can even swap out laravel-echo-server and use Laravel Websocket should you want a PHP implemtantion.

JC Tan avatar
Written By

JC Tan

Published in PHP
Enjoyed the post?

Clap to support the author, help others find it, and make your opinion count.