Search code examples
phplaravelwebsocketlaravel-websockets

Laravel not responding to WebSocket events correctly


As a Laravel beginning I'm having hard times grasping on how to create a simple "hello world" -like WebSocket interface, it seems as if authentication must be implemented, database logging must exist, and a certain WebSocket structure must be implemented in order for it to work. The problem is, the client cannot be modified to fit the server. How should this simple server be implemented in Laravel (preferably with beyondcode/laravel-websockets)? The websocket should be available at ws://{location}:3000/clock, whereas other already existing REST interfaces in other paths, such as http://{location}:3000/some-rest-thing. It's vital for the application to work with either commonly used Laravel frameworks, or default methods provided by Laravel.

Different parts of the application have also been attempted to create using this youtube tutorial, but it seems in many cases it's not possible to even access the /laravel-websockets dashboard and when it is accessible, pressing the connect button does nothing as the server returns HTTP status code 404.

The goal for the server is to collect WebSocket clients, and once receiving a "requestTime" request, broadcast the server time to all connected clients. The whole server application can be seen in the fully working NodeJS app below; this is the same structure the Laravel app should also have:

const wsServer = new ws.WebSocketServer({ server: server, path: "/clock" });

wsServer.on('connection', socket => {
    socket.on('error', err => {
        console.error(err);
    });

    socket.on('message', data => {
        if(data.toString() === "requestTime") {
            // broadcast time on requestTime event to all clients
            wsServer.clients.forEach(client => {
                if(client.readyState === ws.OPEN) {
                    client.send((new Date()).getMilliseconds());
                }
            });
        }
    });
});

Here's what has been implemented thus far:

Connecting a client to the implementation below the client connects, but almost directly disconnects as it receives a HTTP response code 200, which probably shouldn't happen in a WebSocket api. No events seem to be thrown.

SendTimeToClientEvent.php, the event that gets broadcasted to connected clients assuming that it only sends "<system time in ms>" with no other data

<?php

namespace App\Events;

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

class SendTimeToClientEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    public function broadcastWith() {
        return round(microtime(true) * 1000));
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new Channel('clock');
    }
}

Route in api.php

Route::get('/clock', function(Request $request) {
    $message = $request->input("message", null);
    if($message == "requestTime") {
        SendTimeToClientEvent::dispatch();
    }
    return null;
});

Pusher set to .env

BROADCAST_DRIVER=pusher

Laravel CMD output Note that the client works with some other provided frameworks

[Fri Mar 10 00:40:07 2023] 127.0.0.1:3617 Accepted
[Fri Mar 10 00:40:07 2023] 127.0.0.1:3609 Closing
[Fri Mar 10 00:40:07 2023] 127.0.0.1:3611 Invalid request (An existing connection was forcibly closed by the remote host.)

I've also attempted some alternative methods to do this, such as:

The first alternative method

WebSocketsRouter::webSocket('/clock', function ($webSocket) {
    $webSocket->onMessage('requestTime', function ($client, $data) use ($webSocket) {
        $webSocket->broadcast()->emit('systemTime', round(microtime(true) * 1000));
    });
});

which will throw "invalid websocket controller provided". after running php artisan cache:clear. Moving the function to an actual class that implements MessageComponentInterface seems to throw a similar error as well.

The second alternative method

create a new socket controller:

namespace App\Http\Controllers;

use Ratchet\ConnectionInterface;
use Ratchet\WebSocket\MessageComponentInterface;

class ClockWebSocketController implements MessageComponentInterface
{
    protected $clients;

    public function __construct()
    {
        $this->clients = new \SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients->attach($conn);
    }

    public function onMessage(ConnectionInterface $from, $msg)
    {
        if ($msg === 'requestTime') {
            $now = round(microtime(true) * 1000);
            $this->broadcast($now);
        }
    }

    public function onClose(ConnectionInterface $conn)
    {
        $this->clients->detach($conn);
    }

    public function onError(ConnectionInterface $conn, \Exception $e)
    {
        $conn->close();
    }

    protected function broadcast($msg)
    {
        foreach ($this->clients as $client) {
            $client->send($msg);
        }
    }
}

bind it in AppServiceProvider.register()

App::bind(MessageComponentInterface::class, ClockWebSocketController::class);

and resolve and take the controller into use in api.php

$webSocketRouter = resolve(Router::class);
$webSocketRouter->webSocket('/clock', MessageComponentInterface::class);

unfortunately this throws the following peculiar error:

   BeyondCode\LaravelWebSockets\Exceptions\InvalidWebSocketController 

  Invalid WebSocket Controller provided. Expected instance of `Ratchet\WebSocket\MessageComponentInterface`, but received `Ratchet\WebSocket\MessageComponentInterface`.

How to replicate with a fresh project

After trying to implement this in a whole new project, I still can't get this to work. I've added the steps I've taken below, which can be used to replicate the issue:

  1. Install Composer (2.5.4)
  2. Create a new Laravel Project composer create-project laravel/laravel stresstest2
  3. Require laravel-websockets composer require beyondcode/laravel-websockets
  4. Publish websockets config etc. packages php artisan vendor:publish and select the laravel-websockets package, should be one of the first ones
  5. Run migration php artisan migrate
  6. Require pusher-php-server composer require pusher/pusher-php-server
  7. in config/websockets.php add '*' to allowed origins
  8. in .env modify the following values:
BROADCAST_DRIVER=pusher
QUEUE_CONNECTION=sync
PUSHER_APP_ID=dummy
PUSHER_APP_KEY=dummy
PUSHER_APP_SECRET=dummy
# \/ are new
LARAVEL_WEBSOCKETS_PORT=3000
LARAVEL_WEBSOCKETS_HOST=127.0.0.1
  1. now launch laravel with php artisan serve --port=3000. websockets:serve is not used as in the future some REST API:s need to also be in the app.
  2. Open http://localhost:3000/laravel-websockets and click on the dashboard connect button -> nothing happens
  3. Alternatively try launching the app with php artisan websockets:serve --port=3000 and go to the dashboard -> dashboard now doesn't open and network log on browser displays error 404

Interestingly if you first serve the website with php artisan serve --port=3000, then stop the server (do not refresh the website!) and open the websocket server with php artisan websockets:serve you can now press the Connect button, but the browser still displays error 404, even though the PHP server displays a new successful connection


Solution

  • When you run php artisan serve --port=3000 this starts up the development webserver. This runs all your normal web and api routes, including the websockets dashboard which in itself is not using websockets.

    When you run php artisan websockets:serve --port=3000 this starts the websockets server, but this only server websockets over ws:// or wss:// but not the dashboard.

    The websocket dashboard however does connect to the websocket server from the client/browser.

    You should open 2 terminals and run bith servers on different ports. For example, in the first one run php artisan serve --port=8000 and on the second one php artisan serve --port=3000. Then you can access the websockets dashboard on http://localhost:8000/laravel-websockets but you have to make sure that you set the websockets url to use port 3000. By default it uses port 6001 for the websockets, so it might be easier to use that port instead of 3000 to make sure it works before you switch ports to 3000.