★ Introducing laravel-websockets, an easy to use WebSocket server implemented in PHP
Tags: php, laravel, websockets
Published: 2020-01-02 23:12:04

---
title: ★ Introducing laravel-websockets, an easy to use WebSocket server implemented in PHP
published: true
date: 2018-12-04 21:27:39 UTC
tags: php, laravel, websockets
canonical_url: https://freek.dev/1228-introducing-laravel-websockets-an-easy-to-use-websocket-server-implemented-in-php
---

[laravel-websockets](https://github.com/beyondcode/laravel-websockets) is a Laravel package that can handle the server side of WebSockets entirely. It completely replaces the need for a service like [Pusher](https://pusher.com/) or a JavaScript-based [laravel-echo-server](https://github.com/tlaverdure/laravel-echo-server). It has [extensive documentation](https://docs.beyondco.de/laravel-websockets/) and [a demo application](https://github.com/beyondcode/laravel-websockets-demo) you can play with. [Marcel](https://twitter.com/marcelpociot), developer and co-owner at [beyondcode](https://beyondco.de/), and I have been working on this together for the past couple of weeks. In this blog post, we'd like to introduce the package to you.

![console](https://freek.dev/uploads/media/laravel-websockets/server.png)

## What are WebSockets? #

Simply said, a WebSocket connection is a persistent connection between a browser and the server. It allows two-way communication: the server can send messages to the browser and the browser - the client - can respond back via the same connection. This differs from regular Ajax, which is only one-way communication: only the client can ask stuff from the server.

WebSockets are mainly used for real-time apps, such as chat applications. Such a chat application could work like this.

1. User A points the browser to the chat application. The browser creates a WebSocket connection to the server. Remember that the server keeps the connection open.
2. User B, C, D, ... do the very same thing. The server now has multiple open connections.
3. The first user transmits a message through the open WebSocket connection to the server.
4. The server sees a message coming in via the first connection and send that message to all other connections it has open.
5. The browsers of user B, C, D, ... get an incoming coming message via the WebSockets and can do something with it (in case of chat application: display it).

All this happens in real-time, there's no need for polling. If there are a lot of messages the payload is also a lot smaller. There's no need to send headers and such through the connection.

## What is laravel-websockets? #

Our [laravel-websockets](https://github.com/beyondcode/laravel-websockets) package can handle the serverside of WebSockets for you. It contains a webserver, implemented in PHP, that can handle incoming WebSocket connections.

laravel-websockets has been built on top of [Ratchet](http://socketo.me/), a low-level package to handle WebSockets. Working with Ratchet directly is doable, but there's some research and setup required to make it work. Our package takes all of that away: you'll be able to get a WebSockets server running in a minute.

Marcel and I added a lot of nice features, that would take you quite a while to implement when using Ratchet itself. There is support for multi-tenancy, so you could set up a webSockets server and use it for many different applications. We also added [a real-time debug dashboard](https://docs.beyondco.de/laravel-websockets/1.0/debugging/dashboard.html), so you can inspect what is being transmitted through the webSockets. All this is being powered with a real-time chart that gives you key-insights into your WebSocket application metrics, like the number of peak connections, the amount of messages sent and the amount of API messages received.

We also implemented [the pusher message protocol](https://pusher.com/docs/pusher_protocol). By doing this, all existing packages and applications out there that support Pusher will work with our package as well. Laravel's broadcasting capabilities will just work it. [laravel-echo](https://laravel.com/docs/5.7/broadcasting#receiving-broadcasts), a JavaScript library which handles webSockets clientside, is also 100% compatible. You can use all the features Pusher offers, such as [private channels](https://pusher.com/docs/client_api_guide/client_private_channels), [presence channels](https://pusher.com/docs/client_api_guide/client_presence_channels) and even [the Pusher HTTP API](https://pusher.com/docs/rest_api).

Besides being a free alternative for Pusher, this package also gives Laravel package developers a big advantage. It's now a lot easier to add WebSocket capabilities into your application or packages - since our package completely removes the need for a third-party application or server being installed. Pusher also has a fairly conservatie maximum payload size. In our package you [specify your own maximum payload size](https://github.com/beyondcode/laravel-websockets/blob/7bc6e32a760fb6cdeec61b4e12ddaa8da5c6a814/config/websockets.php#L43).

## How to use it #

Before diving into the details of how this all works under the hood, let's first see how we can actually use it.

First up, require it with Composer

```
composer require beyondcode/laravel-websockets
```

This package comes with a migration to store statistic information while running your WebSocket server. You can publish the migration file using:

```
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"
```

Run the migrations with:

```
php artisan migrate
```

Next, you must publish the config file.

```
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"
```

This is the file that will be published.

```php
return [

    /*
     * This package comes with multi tenancy out of the box. Here you can
     * configure the different apps that can use the webSockets server.
     *
     * Optionally you can disable client events so clients cannot send
     * messages to each other via the webSockets.
     */
    'apps' => [
        [
            'id' => env('PUSHER_APP_ID'),
            'name' => env('APP_NAME'),
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'enable_client_messages' => false,
            'enable_statistics' => true,
        ],
    ],

    /*
     * This class is responsible for finding the apps. The default provider
     * will use the apps defined in this config file.
     *
     * You can create a custom provider by implementing the
     * `AppProvider` interface.
     */
    'app_provider' => BeyondCode\LaravelWebSockets\Apps\ConfigAppProvider::class,

    /*
     * This array contains the hosts of which you want to allow incoming requests.
     * Leave this empty if you want to accept requests from all hosts.
     */
    'allowed_origins' => [
        //
    ],

    /*
     * The maximum request size in kilobytes that is allowed for an incoming WebSocket request.
     */
    'max_request_size_in_kb' => 250,

    /*
     * This path will be used to register the necessary routes for the package.
     */
    'path' => 'laravel-websockets',

    'statistics' => [
        /*
         * This model will be used to store the statistics of the WebSocketsServer.
         * The only requirement is that the model should extend
         * `WebSocketsStatisticsEntry` provided by this package.
         */
        'model' => \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry::class,

        /*
         * Here you can specify the interval in seconds at which statistics should be logged.
         */
        'interval_in_seconds' => 60,

        /*
         * When the clean-command is executed, all recorded statistics older than
         * the number of days specified here will be deleted.
         */
        'delete_statistics_older_than_days' => 60
    ],

    /*
     * Define the optional SSL context for your WebSocket connections.
     * You can see all available options at: http://php.net/manual/en/context.ssl.php
     */
    'ssl' => [
        /*
         * Path to local certificate file on filesystem. It must be a PEM encoded file which
         * contains your certificate and private key. It can optionally contain the
         * certificate chain of issuers. The private key also may be contained
         * in a separate file specified by local_pk.
         */
        'local_cert' => null,

        /*
         * Path to local private key file on filesystem in case of separate files for
         * certificate (local_cert) and private key.
         */
        'local_pk' => null,

        /*
         * Passphrase for your local_cert file.
         */
        'passphrase' => null
    ],
];
```

The last step is to fill some environment variables. Make sure that `APP_NAME`, `PUSHER_APP_ID`, `PUSHER_APP_KEY`, `PUSHER_APP_SECRET` are filled in your `.env`. You might wonder why we're using `PUSHER_` prefixes here. More on that later.

And with that out of the way, you can start the server by executing this command.

```
php artisan websockets:serve
```

Now you can use a library like laravel-echo to connect to the server. I'm not going through the entire process in detail. To get started with the client side check out [our docs](https://docs.beyondco.de/laravel-websockets/1.0/basic-usage/pusher.html#pusher-configuration), [the laravel-echo docs](https://laravel.com/docs/5.7/broadcasting#installing-laravel-echo) and this [demo application](https://github.com/beyondcode/laravel-websockets-demo).

![demo](https://freek.dev/uploads/media/laravel-websockets/demo.png)

## How does it work under the hood? #

If you're not interested in the nitty gritty details of how all of this work, but just want to use it, you can skip ahead to the section on the debug dashboard.

When executing `php artisan websockets:serve` you'll notice that the command never ends. That because inside [that command](https://github.com/beyondcode/laravel-websockets/blob/dc87bfb221540724387712cdeb39b5f783efd6c0/src/Console/StartWebSocketServer.php) we start a Ratchet server that starts listing for connections (by default on port 6001).

```php
protected function startWebSocketServer()
{
    $this->info("Starting the WebSocket server on port {$this->option('port')}...");

    $routes = WebSocketsRouter::getRoutes();

    /**? Start the server ? */
    (new WebSocketServerFactory())
        ->useRoutes($routes)
        ->setHost($this->option('host'))
        ->setPort($this->option('port'))
        ->setConsoleOutput($this->output)
        ->createServer()
        ->run();
}
```

One of the things we give to the server is the `$routes`. That variable is an instance of `\Symfony\Component\Routing\RouteCollection`. That's being created in [our Router class](https://github.com/beyondcode/laravel-websockets/blob/dc87bfb221540724387712cdeb39b5f783efd6c0/src/Server/Router.php).

Because in our `StartWebSocketsCommand` we've [called](https://github.com/beyondcode/laravel-websockets/blob/dc87bfb221540724387712cdeb39b5f783efd6c0/src/Console/StartWebSocketServer.php#L65) the `echo` method, the routes will contain [these routes](https://github.com/beyondcode/laravel-websockets/blob/dc87bfb221540724387712cdeb39b5f783efd6c0/src/Server/Router.php#L32-L40).

```php
public function echo()
{
    $this->get('/app/{appKey}', WebSocketHandler::class);
    
    $this->post('/apps/{appId}/events', gerEventController::class);
    $this->get('/apps/{appId}/channels', hChannelsController::class);
    $this->get('/apps/{appId}/channels/{channelName}', hChannelController::class);
    $this->get('/apps/{appId}/channels/{channelName}/users', hUsersController::class);

}
```

Now here's where it gets interesting. The first route is the route that will handle the incoming webSockets. The route URL `/app/{appKey}` is that specific URL because [that is what Pusher uses](https://pusher.com/docs/pusher_protocol#websocket-connection). Using that URL is important to make all Pusher compatible JavaScript clients work.

The other ones listen for incoming HTTP connections. If you think of it, this is pretty amazing: our little server can listen for both WebSockets and HTTP connections. Isn't PHP awesome?

### Incoming WebSocket connections #

Let's dive a little bit deeper into how WebSocket connections are handled.

```php
$this->get('/app/{appKey}', WebSocketHandler::class);
```

That `get` method in the `Router` will call the `getRoute` method. In that method, we wrap the instantiated `WebSocketHandler` in a `WsServer` class. That `WsServer` class is provided by Ratchet. It provides the code that does the nit gritty work to work with WebSockets.

```php
// inside the `BeyondCode\LaravelWebSockets\Server\Router` class

public function get(string $uri, $action)
{
    $this->addRoute('GET', $uri, $action);
}

protected function getRoute(string $method, string $uri, $action): Route
{
    /**
     * If the given action is a class that handles WebSockets, then it's not a regular
     * controller but a WebSocketHandler that needs to converted to a WsServer.
     *
     * If the given action is a regular controller we'll just instanciate it.
     */
    $action = is_subclass_of($action, MessageComponentInterface::class)
        ? $this->createWebSocketsServer($action)
        : app($action);

    return new Route($uri, ['_controller' => $action], [], [], null, [], [$method]);
}

protected function createWebSocketsServer(string $action): WsServer
{
    $app = app($action);

    if (WebsocketsLogger::isEnabled()) {
        $app = WebsocketsLogger::decorate($app);
    }

    return new WsServer($app);
}
```

Now that you know how the routing is set up for the WebSockets part, let take a look at what the `WebSocketHandler` actually does.

You can think of [the `WebSocketHandler` class](https://github.com/beyondcode/laravel-websockets/blob/d7114a778a7e00758456a09da2c8807498bbd030/src/WebSockets/WebSocketHandler.php) as a controller, but for WebSockets instead of HTTP. It contains various lifecycle methods:

- `onOpen`: will be called when a new client opens up a webSockets connection
- `onMessage`: will be called when the client sends a new message through the webSocket.
- `onClose`: will be called when the client disconnects (usually by navigation to another page, or closing the tab or browser window)

#### onOpen #

This is the code of the `onOpen` method.

```php
public function onOpen(ConnectionInterface $connection)
{
    $this
        ->verifyAppKey($connection)
        ->generateSocketId($connection)
        ->establishConnection($connection);
}
```

Three things happen we a new connection comes in. The first thing is the app verification. Remember that route that's being used to handle webSocket connections?

```php
$this->get('/app/{appKey}', WebSocketHandler::class);
```

The `verifyAppKey` function will check if the given `appKey` is correct. In a default installation `App::findByAppKey` will if there is an app defined with this key in the `websockets.php` config file.

```php
protected function verifyAppKey(ConnectionInterface $connection)
{
    $appKey = QueryParameters::create($connection->httpRequest)->get('appKey');

    if (!$app = App::findByKey($appKey)) {
        throw new UnknownAppKey($appKey);
    }

    $connection->app = $app;

    return $this;
}
```

We store a reference to `app` on the connection itself so we can use this on subsequent events on the connection (for example when a message comes in).

Secondly, we're going to generate a socket id.

```php
protected function generateSocketId(ConnectionInterface $connection)
{
    $socketId = sprintf("%d.%d", random_int(1, 000000), random_int(1, 1000000000));

    $connection->socketId = $socketId;

    return $this;
}
```

The `socketId` has to follow a specific format. Two numbers separated by a dot. We are going to store this number on the connection itself. We'll use that id to identify this particular connection. Remember our server process never ends so this id will stay on the connection as long as it is open.

The last thing that happens on `opOpen` is to send back a response to the browser that the connection has succeeded. Again, we follow the pusher protocol here and send out a `pusher:connection_established` event to be compatible with all existing Pusher packages out there. We'll also log this event on the dashboard.

```php
protected function establishConnection(ConnectionInterface $connection)
{
    $connection->send(json_encode([
        'event' => 'pusher:connection_established',
        'data' => json_encode([
            'socket_id' => $connection->socketId,
            'activity_timeout' => 30,
        ])
    ]));

    DashboardLogger::connection($connection);

    return $this;
}
```

And all that work done, we have an open connection.

Let's now talk a look at what happens when a message is sent over this connection.

#### onMessage #

Let's take a look at [the `onMessage` function in the `WebSocketHandler` class](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/WebSocketHandler.php#L36-L41)

```php
public function onMessage(ConnectionInterface $connection, MessageInterface $message)
{
    $message = PusherMessageFactory::createForMessage($message, $connection, $this->channelManager);

    $message->respond();
}
```

Here we are getting the message and respond to it. Let' dive a deeper in the `createForMessage` method in [the `PusherMessageFactory` class](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/Messages/PusherMessageFactory.php#L11-L21).

```php
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\MessageInterface;

class PusherMessageFactory
{
    public static function createForMessage(
        MessageInterface $message,
        ConnectionInterface $connection,
        ChannelManager $channelManager): PusherMessage
    {
        $payload = json_decode($message->getPayload());

        return starts_with($payload->event, 'pusher:')
            ? new PusherChannelProtocolMessage($payload, $connection, $channelManager)
            : new PusherClientMessage($payload, $connection, $channelManager);
    }
}
```

In the class above we are going to pick the right message type according to the event name in the payload. `PusherChannelProtocolMessage` handles the messages that are mostly focused around the subscription on channels. `PusherClientMessage` will handle messages that are being sent by clients that have subscribed to the channels, more on that later.

Let's see what happens in `PusherChannelProtocolMessage`. More specifically we'll take a look in it's `respond` method since that is [what was called in the `WebSocketHandler` class](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/WebSocketHandler.php#L40)

```php
public function respond()
{
    $eventName = camel_case(str_after($this->payload->event, ':'));

    if (method_exists($this, $eventName)) {
        call_user_func([$this, $eventName], $this->connection, $this->payload->data ?? new stdClass());
    }
}
```

`$this->payload->event` will contains a string like `pusher:subscribe`. So in the code above `$eventName` will contain `subscribe`.

Let's talk a bit about subscribing first. Pusher works with different `channels`. You can think of a channel like a channel on the radio. If you turn your radio to the specific channels you'll hear the messages that are sent on that channel. According [the Pusher protocol](https://pusher.com/docs/pusher_protocol#pusher-subscribe) a client that wants to subscribe to a channel must use the `subscribe` method.

That `call_user_func` call in the `respond` function above will try to call a function named liked the event. So for the event `subscribe`, the `subscribe` method will be called.

```php
protected function subscribe(ConnectionInterface $connection, stdClass $payload)
{
    $channel = $this->channelManager->findOrCreate($connection->app->id, $payload->channel);

    $channel->subscribe($connection, $payload);
}
```

Here we make our first contact with [the `ChannelManager`](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/Channels/ChannelManager.php). This class is responsible for keeping track which connections are subscribed to which channels.

Let's take a look at the called `findOrCreate` on the `ChannelManager`

```php
public function findOrCreate(string $appId, string $channelName): Channel
{
    if (!isset($this->channels[$appId][$channelName])) {
        $channelClass = $this->determineChannelClass($channelName);

        $this->channels[$appId][$channelName] = new $channelClass($channelName);
    }

    return $this->channels[$appId][$channelName];
}
```

In this function, we are going to look up a `Channel` instance for the given `appId` and `channelName`. If it doesn't exist we'll new it up and store it in the `channels` instance variable. Because our server process never ends and is the same for all incoming connections, storing it in memory is enough, we don't need to persist it somewhere else.

Let's step into `determineChannelClass` and discuss the different channel types as defined by Pusher.

```php
protected function determineChannelClass(string $channelName): string
{
    if (starts_with($channelName, 'private-')) {
        return PrivateChannel::class;
    }

    if (starts_with($channelName, 'presence-')) {
        return PresenceChannel::class;
    }

    return Channel::class;
}
```

Pusher has three different channel types:

- [private channels](https://pusher.com/docs/client_api_guide/client_private_channels): an authentication check will be performed before a client can actually subscribe itself to the channels
- [presence channels](https://pusher.com/docs/client_api_guide/client_presence_channels): the same as private channels but with extra functionalities to determine who is on the channel (this is handy if you're building something like a chat room)
- [public channels](https://pusher.com/docs/client_api_guide/client_public_channels): these are channels where anyone can subscribe to, no authentication check is performed.

These three channel types are implemented in our code as [`PrivateChannel`](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/Channels/PrivateChannel.php), [`PresenceChannel`](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/Channels/PresenceChannel.php) and [`Channel`](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/Channels/Channel.php). In `determineChannelClass` we are newing up and return a channel of the right type.

Let's go a bit back to the aforementioned `subscribe` method in [the `PusherChannelProtocolMessage` class](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/Messages/PusherChannelProtocolMessage.php#L51). Here's the code again.

```php
protected function subscribe(ConnectionInterface $connection, stdClass $payload)
{
    $channel = $this->channelManager->findOrCreate($connection->client->appId, $payload->channel);

    $channel->subscribe($connection, $payload);
}
```

So now we know that `$channel` holds an instance of the right channel class, let's look at the implementation of `subscribe`. Let's see what goes on in [the regular `Channel`](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/Channels/Channel.php) class first.

```php
public function subscribe(ConnectionInterface $connection, stdClass $payload)
{
    $this->saveConnection($connection);

    $connection->send(json_encode([
        'event' => 'pusher_internal:subscription_succeeded',
        'channel' => $this->channelName
    ]));
}

protected function saveConnection(ConnectionInterface $connection)
{
    $hadConnectionsPreviously = $this->hasConnections();

    $this->subscriptions[$connection->socketId] = $connection;

    if (! $hadConnectionsPreviously) {
        DashboardLogger::occupied($connection, $this->channelName);
    }

    DashboardLogger::subscribed($connection, $this->channelName);
}
```

Subscribing to a channel is nothing more than just storing the `socketId` of the connection (remember [we set this id in the `onOpen` method?](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/WebSocketHandler.php#L74) in the `subscriptions` instance variable of the channel?). `saveConnection` also sends a message back of the connection to let the client now that the subscription succeeded. That `pusher_internal:subscription_succeeded` event name is [specified by the Pusher protocol](https://pusher.com/docs/pusher_protocol#client-only-events).

And with that, the client is subscribed and the message passed to `onMessage` is handled.

But what would have happened if we were not working with a regular channel put with `PrivateChannel`? Let's take a look at `PrivateChannel`.

```php
namespace BeyondCode\LaravelWebSockets\WebSockets\Channels;

use Ratchet\ConnectionInterface;
use stdClass;

class PrivateChannel extends Channel
{
    public function subscribe(ConnectionInterface $connection, stdClass $payload)
    {
        $this->verifySignature($connection, $payload);

        parent::subscribe($connection, $payload);
    }
}
```

So the `subscribe` method will do exactly the same as in the regular channel, but before actually subscribing the signature will be verified. As this blog post is quite long I’m not going into the actual verification. If you want to know more about that check out [the implementation of the method](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/Channels/Channel.php#L34-L45) and the [Pusher docs on authentication](https://pusher.com/docs/client_api_guide/client_private_channels).

And with that, a client is subscribed to a channel. Let's now review what happens what happens when a client sends a message to the channel.

[According to the pusher protocol](https://pusher.com/docs/pusher_protocol#channel-client-events) clients can send messages to each other using messages that contain an event name starting with `client-`. Let's take another look at the [`PusherMessageFactory`](https://github.com/beyondcode/laravel-websockets/blob/286629fd98011763bb41ee6e47a8a82e9d1f5e90/src/WebSockets/Messages/PusherMessageFactory.php).

```php
return starts_with($payload->event, 'pusher:')
    ? new PusherChannelProtocolMessage($payload, $connection, $channelManager)
    : new PusherClientMessage($payload, $connection, $channelManager);
```

We already covered the `PusherChannelProtocolMessage`, let's now head into the `respond` method of the [`PusherClientMessage`](https://github.com/beyondcode/laravel-websockets/blob/7bc6e32a760fb6cdeec61b4e12ddaa8da5c6a814/src/WebSockets/Messages/PusherClientMessage.php).

```php
public function respond()
{
    if (!starts_with($this->payload->event, 'client-')) {
        return;
    }
    
    if (! $this->connection->app->clientMessagesEnabled) {
        return;
    }
    
    DashboardLogger::clientMessage($this->connection, $this->payload);
    
    $channel = $this->channelManager->find($this->connection->app->id, $this->payload->channel);
    
    optional($channel)->broadcastToOthers($this->connection, $this->payload);
}
```

Let's go through that code If the event name of the message doesn't start with `client-`, it isn't a client to client message and we are going to bail out. The second if checks if client to client messaging is enabled (it [can be disabled on a per-app basis](https://github.com/beyondcode/laravel-websockets/blob/286629fd98011763bb41ee6e47a8a82e9d1f5e90/config/websockets.php#L18)). Next we're going to log the message on our debug dashboard.

With that out of the way the real work can start. We are gonna ask the `ChannelManager` to find the right `Channel` for the message. If it can find the right `Channel` we're going to call `broadcast` on it.

The [implementation of `broadcastToOthers` on `Channel`](https://github.com/beyondcode/laravel-websockets/blob/7bc6e32a760fb6cdeec61b4e12ddaa8da5c6a814/src/WebSockets/Channels/Channel.php#L89-L105) is not that hard. The code below loops through every connection subscribed to the channel and sends the connection the payload. The client that sent to original message does not need to receive it. We filter out the connection of the sender using [the socketId we set earlier](https://github.com/beyondcode/laravel-websockets/blob/7bc6e32a760fb6cdeec61b4e12ddaa8da5c6a814/src/WebSockets/WebSocketHandler.php#L80).

```php
public function broadcastToOthers(ConnectionInterface $connection, $payload)
{
    $this->broadcastToEveryoneExcept($payload, $connection->socketId);
}

public function broadcastToEveryoneExcept($payload, ?string $socketId = null)
{
    if (is_null($socketId)) {
        return $this->broadcast($payload);
    }

    foreach ($this->subscribedConnections as $connection) {
        if ($connection->socketId !== $socketId) {
            $connection->send(json_encode($payload));
        }
    }
}
```

And with that we've send a message to all clients present on the channel.

#### onClose #

The third lifecycle method the [`WebSocketHandler`](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSockets/WebSocketHandler.php#L43-L46) takes care of is `onClose`. It is being called when a client goes away, for example when the tab or browser window is closed. The implementation is pretty easy.

```php
public function onClose(ConnectionInterface $connection)
{
    $this->channelManager->removeFromAllChannels($connection);
}
```

Inside the `removeFromAllChannels` method of `ChannelManager`, we'll remove the connection from all channels. We'll also remove channels and apps that have no connections anymore, so we don't leak memory.

```php
public function removeFromAllChannels(ConnectionInterface $connection)
{
    if (!isset($connection->app)) {
        return;
    }

    /**
     * Remove the connection from all channels.
     */
    collect(array_get($this->channels, $connection->app->id, []))->each->unsubscribe($connection);

    /**
     * Unset all channels that have no connections so we don't leak memory.
     */
    collect(array_get($this->channels, $connection->app->id, []))
        ->reject->hasConnections()
        ->each(function (Channel $channel, string $channelName) use ($connection) {
            unset($this->channels[$connection->app->id][$channelName]);
        });

    if (count(array_get($this->channels, $connection->app->id, [])) === 0) {
        unset($this->channels[$connection->app->id]);
    };
}
```

### Incoming HTTP connections #

Above we already explained that `php artisan websockets:serve ` we start a Ratchet server. If you're reading this post from top to bottom you'll already know how that Ratchet server handles webSockets. Now let's focus on the HTTP part.

Laravel has excellent support for [broadcasting events](https://laravel.com/docs/master/broadcasting). Have you ever wondered what happens under the hood when Laravel broadcasts something? Simply said it will perform an HTTP call with a certain payload to a certain endpoint.

[Our docs specify](https://docs.beyondco.de/laravel-websockets/1.0/basic-usage/pusher.html#pusher-configuration) that you should change `host` of the pusher configuration to '127.0.0.1' (or the hostname of your server where our package runs). That will make Laravel send that HTTP call to our package instead of Pusher.

Remember [those routes](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/Server/Router.php#L34-L39) in our server? Well, when Laravel broadcast an event it will send an POST request to this route:

```php
$this->post('/apps/{appId}/events', TriggerEventController::class);
```

Let's see the code of that [`TriggerEventController`](https://github.com/beyondcode/laravel-websockets/blob/master/src/HttpApi/Controllers/TriggerEventController.php)

```php
class TriggerEventController extends Controller
{
    public function __invoke(Request $request)
    {
        $this->ensureValidSignature($request);

        foreach ($request->json()->get('channels', []) as $channelName) {
            $channel = $this->channelManager->find($request->appId, $channelName);

            optional($channel)->broadcastToEveryoneExcept([
                'channel' => $channelName,
                'event' => $request->json()->get('name'),
                'data' => $request->json()->get('data'),
            ], $request->json()->get('socket_id'));

            DashboardLogger::apiMessage(
                $request->appId,
                $channelName,
                $request->json()->get('name'),
                $request->json()->get('data')
            );
        }

        return $request->json()->all();
    }
}
```

We don't want this endpoint to be callable by just anyone. When sending the request Laravel will add a hashed version of the app signature together with some other parameters to the request. In [`ensureValidSignature` method](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/HttpApi/Controllers/Controller.php#L87-L105) we validate if that signature is correct.

If the signature is correct we are going to let the `ChannelManager` find all the channels the incoming request is intended for. This is interesting. How is this channel manager instantiated? How does it know the channels? We'll our server is a process that never ends. It handles both WebSockets and HTTP requests. The `ChannelManager` is [bound as a singleton](https://github.com/beyondcode/laravel-websockets/blob/89ace081399f197de46c9b5e7466611da62f4add/src/WebSocketsServiceProvider.php#L43-L45), so there can be only one instance of it. So this `ChannelManager` holds all connections of the clients currently subscribed to a channel. So within handling this HTTP request, we can send out messages through the connected WebSockets. This is pretty amazing in our book!

On every channel found by the `ChannelManager` we are going to call the `broadcastToEveryoneExcept` method. Here is the implementation:

```php
public function broadcastToEveryoneExcept($payload, ?string $socketId = null)
{
    if (is_null($socketId)) {
        return $this->broadcast($payload);
    }

    foreach ($this->subscribedConnections as $connection) {
        if ($connection->socketId !== $socketId) {
            $connection->send(json_encode($payload));
        }
    }
}
```

Like seen previously a channel holds all connections subscribed to it. If Laravel wasn't instructed by a specific client to broadcast a message, then the message probably was initiated by the server itself and it should be broadcasted to all subscribed connections.

If Laravel's broadcast was initiated by a specific client, Laravel will have sent the socketId of that connection along with the payload. If that's the case we are going to send a message to all connected clients except that one where the message originated.

And that's how the HTTP side works!

## What if you don't want to use the Pusher protocol? #

While our package's main purpose is to make the usage of either the Pusher Javascript client or Laravel Echo as easy as possible, you are not limited to the Pusher protocol at all. There might be situations where all you need is a simple, bare-bone, websocket server where you want to have full control over the incoming payload and what you want to do with it - without having "channels" in the way.

You can easily create your own custom WebSocketHandler class. All you need to do is implement Ratchets `Ratchet\WebSocket\MessageComponentInterface`.

Once implemented, you will have a class that looks something like this:

```php
namespace App;

use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\MessageInterface;
use Ratchet\WebSocket\MessageComponentInterface;

class MyCustomWebSocketHandler implements MessageComponentInterface
{

    public function onOpen(ConnectionInterface $connection)
    {
        // TODO: Implement onOpen() method.
    }
    
    public function onClose(ConnectionInterface $connection)
    {
        // TODO: Implement onClose() method.
    }

    public function onError(ConnectionInterface $connection, \Exception $e)
    {
        // TODO: Implement onError() method.
    }

    public function onMessage(ConnectionInterface $connection, MessageInterface $msg)
    {
        // TODO: Implement onMessage() method.
    }
}
```

In the class itself you have full control over all the lifecycle events of your WebSocket connections and can intercept the incoming messages and react to them.

The only part missing is, that you will need to tell our WebSocket server to load this handler at a specific route endpoint. This can be achieved using the `WebSocketsRouter` facade.

This class takes care of registering the routes with the actual webSocket server. You can use the `websocket` method to define a custom WebSocket endpoint. The method needs two arguments: the path where the WebSocket handled should be available and the fully qualified classname of the WebSocket handler class.

This could, for example, be done inside your `routes/web.php` file.

```php
WebSocketsRouter::webSocket('/my-websocket', \App\MyCustomWebSocketHandler::class);
```

Once you've added the custom WebSocket route, be sure to restart our WebSockets server for the changes to take place.

## The debug dashboard #

One of the many great features of Pusher is the “Debug Console”. A dashboard that allows you to see all WebSocket connections, events and API requests as they happen in real-time. We wanted to bring this feature to our package as well. That’s why it also contains a debug dashboard with the same features as Pusher.

This is what it looks like: ![The debug dashboard](https://freek.dev/uploads/media/laravel-websockets/dashboard.jpg)

The default location of the WebSocket dashboard is at `/laravel-websockets`. The routes get automatically registered. If you want to change the URL of the dashboard, you can configure it with the `path` setting in your `config/websockets.php` file.

To access the debug dashboard, you can visit the dashboard URL of your Laravel project in the browser. Since your WebSocket server has support for multiple apps, you can select which app you want to connect to and inspect.

By pressing the "Connect" button, you can establish the WebSocket connection and see all events taking place on your WebSocket server from there on in real-time.

By default, access to the WebSocket dashboard is only allowed while your application environment is set to `local`.

However, you can change this behavior by overriding the Laravel Gate being used. A good place for this is the `AuthServiceProvider` that ships with Laravel.

```php
public function boot()
{
    $this->registerPolicies();

    Gate::define('viewWebSocketsDashboard', function ($user = null) {
        return in_array([
        	// 
        ], $user->email);
    });
}
```

If you quickly want to try out an event you can also use the debug dashboard to send out an event to a specific channel.

Simply enter the channel, the event name and provide a valid JSON payload to send it to all connected clients in the given channel.

Another feature that you can find on the debug dashboard is the ability to see key metrics of your WebSocket server in real-time.

Under the hood, the WebSocket server will store a snapshot of the current number of peak connections, the amount of received WebSocket messages and the amount of received API messages defined in a fixed interval. The default setting is to store a snapshot every 60 seconds - but you can change this interval in the configuration file as well.

![Real-Time statistics on the dashboard](https://freek.dev/uploads/media/laravel-websockets/statistics.gif)

## Does it scale? #

This is not a question with an easy answer as your mileage may vary. But with the appropriate server-side configuration your WebSocket server can easily hold a lot of concurrent connections.

This is an example benchmark that was done on the smallest Digital Ocean droplet, that also had a couple of other Laravel projects running. On this specific server, the maximum number of concurrent connections ended up being ~15,000.

![Graph](https://freek.dev/uploads/media/laravel-websockets/simultaneous_users.png)

Here is another benchmark that was run on a 2GB Digital Ocean droplet with 2 CPUs. The maximum number of concurrent connections on this server setup is nearly 60,000.

![Graph](https://freek.dev/uploads/media/laravel-websockets/simultaneous_users_2gb.png)

## Pair programming is fun! #

We really had fun creating this package. The idea was born in a conversion about the Dusk Dashboard the Marcel was making. That dashboard uses webSockets. We both found that there had to be an easier way to work with webSockets. We wanted to make something like `Route::websocket` just work.

Things went fast from there. Marcel already had some experience with Ratchet from building the Dusk dashboard so he created a proof of concept that demonstrated that we could, in theory, replace all functionalities provided by Pusher. Freek did some polishing of the code and implemented some minor features. Meanwhile, Marcel also added the cool debug dashboard and worked on the docs.

On a couple of evenings, we used Slack to share screens and programmed together. The multi-tenancy support and some functionality around the pusher messages was done via pair programming. It was really a good experience for us both.

When the package was feature complete we stayed in touch and daily polished both the package and the docs together. Near the end, a big refactoring was done. A lot of the namespaces and directory structure was changed to make the entire package more clear.

> Blogpost driven development™
> 
> — Freek Van der Herten (@freekmurze) [December 1, 2018](https://twitter.com/freekmurze/status/1068846883018293248?ref_src=twsrc%5Etfw)

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

While writing the blog post we went through all the code again and still polished some code and even added some minor features. We shortly considered creating an organization named `beyond-spatie` to put the package in, but decided it would be better if it was on an existing organization and picked [beyondcode](https://github.com/beyondcode/laravel-websockets), Marcel's company.

We've put a lot of love in the package and we're proud of the work we've done together.

## In closing #

If you read all of the above, congrats, we know that it's quite a lot to take in. Even though a lot happens behind the screen we think the package is easy to use. If you want to know more about the package be sure to read [the extensive documentation](https://docs.beyondco.de/laravel-websockets) we wrote. Head over to [this repo on GitHub](https://github.com/beyondcode/laravel-websockets) to read the source code.

If you're afraid to use it in production, don't be! As a matter of fact, the package is already running a week on [Oh Dear!](https://ohdear.app), Freek's monitoring SaaS. The service makes extensive use of WebSockets to make each screen display real-time info. All of this is backed by our laravel-websockets package, where it handles 50000+ broadcasted events daily.

Even [our own docs](https://docs.beyondco.de/laravel-websockets/1.0/getting-started/introduction.html) uses WebSockets backed by our package. In the right top corner of each page, you can see how many other users are also reading the docs. If you click that bell it'll not only animate on in your browsers, it'll animate for all other readers too. Fun!

If you want to run the package in production, be sure to check out the sections on [how to run it using SSL](https://docs.beyondco.de/laravel-websockets/1.0/basic-usage/ssl.html#server-configuration), and [some deployment gotchas](https://docs.beyondco.de/laravel-websockets/).

We both had a lot of prior experience writing packages. If you like [laravel-websockets](https://github.com/beyondcode/laravel-websockets) be sure to check out our other work as well. You'll find a list of Marcel's previous packages on [Beyondcode's organization on GitHub](https://github.com/beyondcode/laravel-websockets). You'll find the packages that Freek and his team have created in [the open source section of the Spatie website](https://spatie.be/open-source/packages).