Laravel Websocket - Echo Server & Ionic
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:
-
Laravel Broadcast (using Redis broadcast driver on a Redis queue)
-
Laravel Echo Server (socket.io server)
-
Client Side
i. Browser (socket.io-client)
ii. Ionic (laravel-echo-client) -
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
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.
Execute the start
command when you're done.
laravel-echo-server start
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'));
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
.
-
Add a sub domain
ws.myblog.io
.
(Recommended) Run certbot (let's encrypt). -
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.
Clap to support the author, help others find it, and make your opinion count.