Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P3)

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (Part 3): Background WebSocket Server Implementation

Today we continue the development of the background function of the chat room project. Now that the back-end database is ready and the user authentication function based on API Token has been implemented, next, let’s implement the core of the chat room function-the realization of the business logic of the WebSocket server.

Create WebSocketHandler

First we create WebSocketHandler.php in the app/services directory to handle WebSocket communication, and initialize the WebSocketHandler class code as follows:

<? php
namespace App \ Services;

use Hhxsv5 \ LaravelS \ Swoole \ WebSocketHandlerInterface;
use Illuminate \ Support \ Facades \ Log;
use Swoole \ Http \ Request;
use Swoole \ WebSocket \ Frame;
use Swoole \ WebSocket \ Server;

class WebSocketHandler implements WebSocketHandlerInterface
{
    public function __construct ()
    {
        // The constructor cannot be omitted even if it is empty
    }

    // Triggered when the connection is established
    public function onOpen (Server $server, Request $request)
    {
        Log :: info ('WebSocket connection established:'. $Request-> fd);
    }

    // Triggered when a message is received
    public function onMessage (Server $server, Frame $frame)
    {
        // $frame-> fd is the client id and $frame-> data is the data sent by the client
        Log :: info ("Data received from {$frame-> fd}: {$frame-> data}");
        foreach ($server-> connections as $fd) {
            if (! $server-> isEstablished ($fd)) {
                // Ignore if connection is not available
                continue;
            }
            $server-> push ($fd, $frame-> data); // The server broadcasts messages to all clients through the push method
        }
    }

    // Triggered when the connection is closed
    public function onClose (Server $server, $fd, $reactorId)
    {
        Log :: info ('WebSocket connection closed:'. $Fd);
    }
}

This class implements WebSocketHandlerInterface an interface, it must implement the interface conventions of constructors onOpen, onMessage and onClose methods, specific features of each method in front of our integrated Swoole achieve the WebSocket server Laravel has been introduced, in establishing connections and disconnections, we only print a log record, upon receipt of the message from the client, we will record the message and broadcast to each client connected to the server WebSocket (logically defined in the corresponding onMessage method).

Asynchronous event monitoring and processing

Of course, when we build a chat room project, the business functions we implement are more complex than the barrage functions we implemented before . In addition to broadcasting the message to all clients, we also need to save the message to the database and verify whether the user Logged in, unlogged users cannot send messages. Let’s first handle the implementation of saving messages to the database.

Since operating the database is a time-consuming operation involving network IO, here we transfer it to the Task Worker for processing through the asynchronous event monitoring mechanism provided by Swoole , thereby improving the communication performance of the WebSocket server.

First, create a message receive event with the help of Laravel’s Artisan command MessageReceived:

php artisan make:event MessageReceived

Then modify the generated app/Events/MessageReceived.php code is as follows:

<?php
namespace App\Events;

use App\Message;
use Hhxsv5\LaravelS\Swoole\Task\Event;

class MessageReceived extends Event
{
    private $message;
    private $userId;

    /**
     * Create a new event instance.
     */
    public function __construct($message, $userId = 0)
    {
        $this->message = $message;
        $this->userId = $userId;
    }

    /**
     * Get the message data
     * 
     * return App\Message
     */
    public function getData()
    {
        $model = new Message();
        $model->room_id = $this->message->room_id;
        $model->msg = $this->message->type == 'text' ? $this->message->content : '';
        $model->img = $this->message->type == 'image' ? $this->message->image : '';
        $model->user_id = $this->userId;
        $model->created_at = Carbon::now();
        return $model;
    }
}

This event class only for incoming data format conversion, here we incoming messages from external objects and the user ID, then getData it is a method of combination Message of model instances and returns.

Since the Message model contains only a creation time, the update does not include the time, so we explicitly specified created_at field, while also the Message model class $timestamps property is set false to avoid the system automatically set the time for its field.

class Message extends Model
{
    public $timestamps = false;
}

Then, create a message listener MessageListener for the above MessageReceived events are processed:

php artisan make:listener MessageListener

Modify the latest generation of app/Listeners/MessageListener.php file code is as follows:

<?php

namespace App\Listeners;

use App\Events\MessageReceived;
use Hhxsv5\LaravelS\Swoole\Task\Listener;
use Illuminate\Support\Facades\Log;

class MessageListener extends Listener
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  MessageReceived  $event
     * @return void
     */
    public function handle($event)
    {
        $message = $event->getData();
        Log::info(__CLASS__ . ': Start Processing', $message->toArray());
        if ($message && $message->user_id && $message->room_id && ($message->msg || $message->img)) {
            $message->save();
            Log::info(__CLASS__ . ': Processed');
        } else {
            Log::error(__CLASS__ . ': The message field is missing and cannot be saved');
        }
    }
}

In the message listener, by handle a method of handling an event, the incoming $event parameters corresponding to the above-described MessageReceived object instance, and then we message data stored in the checksum method while the corresponding print log information.

User authentication checksum message trigger event

With the message after message received event and event listeners, then, we need to trigger the WebSocket server receives a message receiving event, the business logic in WebSocketHandler the onMessage process is complete, modify onMessage codes are as follows:

use App\Events\MessageReceived;
use Hhxsv5\LaravelS\Swoole\Task\Event; 
use App\User;

// Triggered when a message is received
public function onMessage(Server $server, Frame $frame)
{
    // $frame->fd is the client id,$frame->data is the data send by the client
    Log::info("From {$frame->fd} Received data: {$frame->data}");
    $message = json_decode($frame->data);
    // Based on Token user authentication check
    if (empty($message->token) || !($user = User::where('api_token', $message->token)->first())) {
        Log::warning("User" . $message->name . "is offline, cannot send message");
        $server->push($frame->fd, "User offline cannot send message");  // Inform users that offline status cannot send messages
    } else {
        // Trigger message receive event
        event = new MessageReceived($message, $user->id);
        Event::fire($event);
        unset($message->token);  // Remove the current user token field from the message
        foreach ($server->connections as $fd) {
            if (!$server->isEstablished($fd)) {
                // Ignored if connection is not available
                continue;
            }
            $server->push($fd, json_encode($message)); // The server passes push method to send data to all connected clients
        }
    }
}

Here, we first received data is decoded (assuming the client is passed over JSON string), and determines if it contains token the fields, and the tokenvalue is valid, as a basis and determines whether the user is authenticated, for no Authenticated users do not broadcast messages to other clients, but simply inform the user that they need to log in to send messages. Conversely, if the user has logged in, trigger MessageReceived events, and objects incoming messages and user ID, and then save the follow-up process by the message listener, and then through all the WebSocket server build effective client connections, and remove the Token field Messages are broadcast to them, completing one-time sending of chat messages.

NOTE: WebSocket HTTP connection authentication before the connection using a different connection, the authentication logic is independent of, not simply by Auth determining that way, that only applies to a logical HTTP communication.

User authentication logic adjustment

To this end, we must adjust the default user authentication logic Token when a user registration is successful or login is successful, it will update the users list of api_token field values, when the user exits, then empty the field value, corresponding to implement custom code in app/Http/Controllers/AuthController.php the:

<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;

class AuthController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:api')->only('logout');
    }

    public function register(Request $request) 
    {
        // Verify registration fields
        Validator::make($request->all(), [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:6']
        ])->validate();

        // Create user in database and return
        return User::create([
            'name' => $request->input('name'),
            'email' => $request->input('email'),
            'password' => Hash::make($request->input('password')),
            'api_token' => Str::random(60)
        ]);
    }

    public function login(Request $request) 
    {
        // Verify login fields
        $request->validate([
            'email' => 'required|string',
            'password' => 'required|string',
        ]);

        $email = $request->input('email');
        $password = $request->input('password');
        $user = User::where('email', $email)->first();
        // Token information is returned if the user successfully checks
        if ($user && Hash::check($password, $user->password)) {
            $user->api_token = Str::random(60);
            $user->save();
            return response()->json(['user' => $user, 'success' => true]);
        }

        return  response()->json(['success' => false]);
    }

    public function logout(Request $request) 
    {
        $user = Auth::guard('auth:api')->user();
        $userModel = User::find($user->id);
        $userModel->api_token = null;
        $userModel->save();
        return response()->json(['success' => true]);
    }
}

WebSocket server and asynchronous event listening configuration

Finally, we modify config/laravels.php the configuration file, complete WebSocket server configuration, and asynchronous event listeners.

By configuring the first websocket promoter and define WebSocket communication processor:

'websocket'                => [
    'enable' => true,
    'handler' => \App\Services\WebSocketHandler::class,
],

I.e., asynchronous event listener corresponding to mapping in the events configuration item configuration, an event can be processed and a plurality of listeners listen:

'events'                   => [
    \App\Events\MessageReceived::class => [
        \App\Listeners\MessageListener::class,
    ]
],

Also, listen for and handle asynchronous events is the Task Worker process by Swoole process, so it needs to open task_worker_num the configuration, here we use the default configuration to:

'swoole'                   => [
    ...
    'task_worker_num'    => function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 8,
    ...
]

For Laravel applications running on the Swoole HTTP server, because the Laravel container will reside in memory, when it comes to user authentication, it is necessary to clear the authentication status of this request after each request, so as not to be used by other users. profile laravels.php of cleaners configuration items can be uncommented before this line configuration is as follows:

'cleaners'                 => [
    ...
    \Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,    // If you use the authentication or passport in your project, please uncomment this line
    ...
],

Finally, we .env add the following two lines of configuration, respectively, for IP address assignment Swoole HTTP / WebSocket server is running and whether or not running in the background:

LARAVELS_LISTEN_IP=workspace
LARAVELS_DAEMONIZE=true

At this point, the background WebSocket server coding and configuration work of the Laravel project has been completed. Next, we will configure Nginx so that the WebSocket service can provide external access.

Nginx virtual host configuration

Taking the Laradock workbench as an example, a new virtual host webchats.conf( laradock/nginx/sites under the directory) is added to Nginx to configure support for the Swoole HTTP server and WebSocket server (Swoole’s WebSocket server is based on the HTTP server, so both need to be configured at the same time):

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

upstream webchats {
    # Connect IP:Port
    server workspace:5200 weight=5 max_fails=3 fail_timeout=30s;
    keepalive 16;
}

server {
    listen 80;

    server_name webchats.test;
    root /var/www/webchat/public;

    error_log /var/log/nginx/webchats_error.log;
    access_log /var/log/nginx/webchats_access.log;

    autoindex off;
    index index.html index.htm;

    # Nginx handles the static resources(recommend enabling gzip), LaravelS handles the dynamic resource.
    location / {
        try_files $uri @webchats;
    }

    # Response 404 directly when request the PHP file, to avoid exposing public/*.php
    #location ~* \.php$ {
    #    return 404;
    #}

    # Http and WebSocket are concomitant, Nginx identifies them by "location"
    # !!! The location of WebSocket is "/ws"
    # Javascript: var ws = new WebSocket("ws://webchats.test/ws");
    # Handle WebSocket communication
    location ^~ /ws/ {
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout: Nginx will close the connection if the proxied server does not send data to Nginx in 60 seconds; At the same time, this close behavior is also affected by heartbeat setting of Swoole.
        # proxy_read_timeout 60s;
        proxy_http_version 1.1;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://webchats;
    }

    location @webchats {
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout 60s;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_pass http://webchats;
    }
}

In local hosts mapping configuration file webchats.test domain:

127.0.0.1 webchats.test

Next, restart the Nginx container:

docker-compose up -d nginx

Restart Swoole HTTP server for verification

In laradock the directory by docker exec -it laradock_workspace_1 bash entering workspace the container, and then into the webchat directory, restart Swoole HTTP/WebSocket Server:

php bin/laravels restart

laravel

As a result, we can webchats.test access the application:

laravel

Successful access indicates that the Laravel application configuration based on the Swoole HTTP server is successfully configured, but the WebSocket communication and user authentication functions implemented in this tutorial have not been verified and tested. We will start writing the front-end page components in the next tutorial, and Use page automation tests to verify that the backend services are functioning properly.

#laravel #swoole #vuejs

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P3)
27.25 GEEK