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

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P6): Establish a Connection Between the Socket.io Client and The Swoole Websocket server

Server transformation plan

After completing the development environment, setting up the back-end Websocket server, and initializing the front-end resources, we then officially started debugging the front-end and back-end interfaces to complete the development of the online chat room function.

The first thing we need to do is to establish a Websocket connection and communication between the client and the server. Here, our Websocket client uses socket.io-client, and the server uses the WebSocket server provided by the Swoole-based LaravelS extension package . socket.io has its own set of connection establishment and data encoding mechanisms, so you must adjust the original Websocket server implementation, otherwise you cannot establish a WebSocket connection.

The LaravelS extension pack is not friendly to the Socket.io client, but another popular Laravel Swoole extension pack Laravel-Swoole has good support for it, and it can even be said that it is a PHP server adaptation of the socket.io client For details, you can refer to its official documentation , so it is natural that we can port this part of its implementation to LaravelS.

The code of this project has been submitted to the Github code repository: https://github.com/nonfu/webchat , you can download the code from here for comparison.

Writing a data parser Parser

The data format sent and received by the socket.io client has its own rules. We need to implement the corresponding data parser on the server according to this rule, in order to decode the received client data before processing the data, and then process it. After that, encode the data before sending it to the client so that the client can parse it correctly.

This part of the logic directly Laravel-Swoole expansion pack copy, first app/services create the directory WebSocket subdirectories for storing WebSocket server-related code, and then the previously created WebSocketHandler handler class to move, at the same time do not forget to change config/laravels.php the profile of WebSocketHandler the path:

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

Next, start writing data parser, in app/Services/WebSocket creating an abstract base class directory Parser initialization code as follows:

<?php
/**
 * Data codec abstract base class
 */

namespace App\Services\WebSocket;

use Illuminate\Support\Facades\App;

abstract class Parser
{
    /**
     * Strategy classes need to implement handle method.
     */
    protected $strategies = [];

    /**
     * Execute strategies before decoding payload.
     * If return value is true will skip decoding.
     *
     * @param \Swoole\WebSocket\Server $server
     * @param \Swoole\WebSocket\Frame $frame
     *
     * @return boolean
     */
    public function execute($server, $frame)
    {
        $skip = false;

        foreach ($this->strategies as $strategy) {
            $result = App::call(
                $strategy . '@handle',
                [
                    'server' => $server,
                    'frame' => $frame,
                ]
            );
            if ($result === true) {
                $skip = true;
                break;
            }
        }

        return $skip;
    }

    /**
     * Encode output payload for websocket push.
     *
     * @param string $event
     * @param mixed $data
     *
     * @return mixed
     */
    abstract public function encode(string $event, $data);

    /**
     * Input message on websocket connected.
     * Define and return event name and payload data here.
     *
     * @param \Swoole\Websocket\Frame $frame
     *
     * @return array
     */
    abstract public function decode($frame);
}

Then WebSocket create a subdirectory under the directory SocketIO used to store the relevant code to interact with the client socket.io. The first is the data parser corresponding to the socket.io client SocketIOParser. This class inherits from Parser:

<?php
/**
 * Socket.io corresponding data codec
 */

namespace App\Services\WebSocket\SocketIO;

use App\Services\WebSocket\Parser;
use App\Services\WebSocket\SocketIO\Strategies\HeartbeatStrategy;

class SocketIOParser extends Parser
{
    /**
     * Strategy classes need to implement handle method.
     */
    protected $strategies = [
        HeartbeatStrategy::class,
    ];

    /**
     * Encode output payload for websocket push.
     *
     * @param string $event
     * @param mixed $data
     *
     * @return mixed
     */
    public function encode(string $event, $data)
    {
        $packet = Packet::MESSAGE . Packet::EVENT;
        $shouldEncode = is_array($data) || is_object($data);
        $data = $shouldEncode ? json_encode($data) : $data;
        $format = $shouldEncode ? '["%s",%s]' : '["%s","%s"]';

        return $packet . sprintf($format, $event, $data);
    }

    /**
     * Decode message from websocket client.
     * Define and return payload here.
     *
     * @param \Swoole\Websocket\Frame $frame
     *
     * @return array
     */
    public function decode($frame)
    {
        $payload = Packet::getPayload($frame->data);

        return [
            'event' => $payload['event'] ?? null,
            'data' => $payload['data'] ?? null,
        ];
    }
}

This is used inside the package Packet type communication data analysis processing:

<?php
/**
 * Socket.io communication data parsing underlying class
 */

namespace App\Services\WebSocket\SocketIO;

class Packet
{
    /**
     * Socket.io packet type `open`.
     */
    const OPEN = 0;

    /**
     * Socket.io packet type `close`.
     */
    const CLOSE = 1;

    /**
     * Socket.io packet type `ping`.
     */
    const PING = 2;

    /**
     * Socket.io packet type `pong`.
     */
    const PONG = 3;

    /**
     * Socket.io packet type `message`.
     */
    const MESSAGE = 4;

    /**
     * Socket.io packet type 'upgrade'
     */
    const UPGRADE = 5;

    /**
     * Socket.io packet type `noop`.
     */
    const NOOP = 6;

    /**
     * Engine.io packet type `connect`.
     */
    const CONNECT = 0;

    /**
     * Engine.io packet type `disconnect`.
     */
    const DISCONNECT = 1;

    /**
     * Engine.io packet type `event`.
     */
    const EVENT = 2;

    /**
     * Engine.io packet type `ack`.
     */
    const ACK = 3;

    /**
     * Engine.io packet type `error`.
     */
    const ERROR = 4;

    /**
     * Engine.io packet type 'binary event'
     */
    const BINARY_EVENT = 5;

    /**
     * Engine.io packet type `binary ack`. For acks with binary arguments.
     */
    const BINARY_ACK = 6;

    /**
     * Socket.io packet types.
     */
    public static $socketTypes = [
        0 => 'OPEN',
        1 => 'CLOSE',
        2 => 'PING',
        3 => 'PONG',
        4 => 'MESSAGE',
        5 => 'UPGRADE',
        6 => 'NOOP',
    ];

    /**
     * Engine.io packet types.
     */
    public static $engineTypes = [
        0 => 'CONNECT',
        1 => 'DISCONNECT',
        2 => 'EVENT',
        3 => 'ACK',
        4 => 'ERROR',
        5 => 'BINARY_EVENT',
        6 => 'BINARY_ACK',
    ];

    /**
     * Get socket packet type of a raw payload.
     *
     * @param string $packet
     *
     * @return int|null
     */
    public static function getSocketType(string $packet)
    {
        $type = $packet[0] ?? null;

        if (! array_key_exists($type, static::$socketTypes)) {
            return null;
        }

        return (int) $type;
    }

    /**
     * Get data packet from a raw payload.
     *
     * @param string $packet
     *
     * @return array|null
     */
    public static function getPayload(string $packet)
    {
        $packet = trim($packet);
        $start = strpos($packet, '[');

        if ($start === false || substr($packet, -1) !== ']') {
            return null;
        }

        $data = substr($packet, $start, strlen($packet) - $start);
        $data = json_decode($data, true);

        if (is_null($data)) {
            return null;
        }

        return [
            'event' => $data[0],
            'data' => $data[1] ?? null,
        ];
    }

    /**
     * Return if a socket packet belongs to specific type.
     *
     * @param $packet
     * @param string $typeName
     *
     * @return bool
     */
    public static function isSocketType($packet, string $typeName)
    {
        $type = array_search(strtoupper($typeName), static::$socketTypes);

        if ($type === false) {
            return false;
        }

        return static::getSocketType($packet) === $type;
    }
}

In addition SocketIOParser also introduced strategy for processing the heartbeat connection, so-called heartbeat connection refers to the connection length in order to maintain a connection for communication at predetermined time intervals, which typically are not required for communication processing can be ignored, and there is such a made.

Heartbeat connection policy classes are held in SocketIO/Strategies the directory:

<?php
/**
 * Heartbeat connection processing strategy class
 */

namespace App\Services\WebSocket\SocketIO\Strategies;

use App\Services\WebSocket\SocketIO\Packet;

class HeartbeatStrategy
{
    /**
     * If return value is true will skip decoding.
     *
     * @param \Swoole\WebSocket\Server $server
     * @param \Swoole\WebSocket\Frame $frame
     *
     * @return boolean
     */
    public function handle($server, $frame)
    {
        $packet = $frame->data;
        $packetLength = strlen($packet);
        $payload = '';

        if (Packet::getPayload($packet)) {
            return false;
        }

        if ($isPing = Packet::isSocketType($packet, 'ping')) {
            $payload .= Packet::PONG;
        }

        if ($isPing && $packetLength > 1) {
            $payload .= substr($packet, 1, $packetLength - 1);
        }

        if ($isPing) {
            $server->push($frame->fd, $payload);
        }

        return true;
    }
}

At this point, our communication data parser is all completed. Next, let’s look at the encapsulated communication data sending class.

Writing data sending classes Pusher

The reconstructed WebSocketHandler class will bear only routing and controller functions, to business logic related services will be peeled off to complete a separate service unit, comprising sending data, because we need to be uniform encapsulation processing, so that the customer can be End resolution. In the app/Services/WebSocket creation directory Pusher class for data transmission:

<?php
/**
 * Communication data transmission class
 */

namespace App\Services\WebSocket;


class Pusher
{
    /**
     * @var \Swoole\Websocket\Server
     */
    protected $server;

    /**
     * @var int
     */
    protected $opcode;

    /**
     * @var int
     */
    protected $sender;

    /**
     * @var array
     */
    protected $descriptors;

    /**
     * @var bool
     */
    protected $broadcast;

    /**
     * @var bool
     */
    protected $assigned;

    /**
     * @var string
     */
    protected $event;

    /**
     * @var mixed|null
     */
    protected $message;

    /**
     * Push constructor.
     *
     * @param int $opcode
     * @param int $sender
     * @param array $descriptors
     * @param bool $broadcast
     * @param bool $assigned
     * @param string $event
     * @param mixed|null $message
     * @param \Swoole\Websocket\Server
     */
    protected function __construct(
        int $opcode,
        int $sender,
        array $descriptors,
        bool $broadcast,
        bool $assigned,
        string $event,
        $message = null,
        $server
    )
    {
        $this->opcode = $opcode;
        $this->sender = $sender;
        $this->descriptors = $descriptors;
        $this->broadcast = $broadcast;
        $this->assigned = $assigned;
        $this->event = $event;
        $this->message = $message;
        $this->server = $server;
    }

    /**
     * Static constructor
     *
     * @param array $data
     * @param \Swoole\Websocket\Server $server
     *
     * @return Pusher
     */
    public static function make(array $data, $server)
    {
        return new static(
            $data['opcode'] ?? 1,
            $data['sender'] ?? 0,
            $data['fds'] ?? [],
            $data['broadcast'] ?? false,
            $data['assigned'] ?? false,
            $data['event'] ?? null,
            $data['message'] ?? null,
            $server
        );
    }

    /**
     * @return int
     */
    public function getOpcode(): int
    {
        return $this->opcode;
    }

    /**
     * @return int
     */
    public function getSender(): int
    {
        return $this->sender;
    }

    /**
     * @return array
     */
    public function getDescriptors(): array
    {
        return $this->descriptors;
    }

    /**
     * @param int $descriptor
     *
     * @return self
     */
    public function addDescriptor($descriptor): self
    {
        return $this->addDescriptors([$descriptor]);
    }

    /**
     * @param array $descriptors
     *
     * @return self
     */
    public function addDescriptors(array $descriptors): self
    {
        $this->descriptors = array_values(
            array_unique(
                array_merge($this->descriptors, $descriptors)
            )
        );

        return $this;
    }

    /**
     * @param int $descriptor
     *
     * @return bool
     */
    public function hasDescriptor(int $descriptor): bool
    {
        return in_array($descriptor, $this->descriptors);
    }

    /**
     * @return bool
     */
    public function isBroadcast(): bool
    {
        return $this->broadcast;
    }

    /**
     * @return bool
     */
    public function isAssigned(): bool
    {
        return $this->assigned;
    }

    /**
     * @return string
     */
    public function getEvent(): string
    {
        return $this->event;
    }

    /**
     * @return mixed|null
     */
    public function getMessage()
    {
        return $this->message;
    }

    /**
     * @return \Swoole\Websocket\Server
     */
    public function getServer()
    {
        return $this->server;
    }

    /**
     * @return bool
     */
    public function shouldBroadcast(): bool
    {
        return $this->broadcast && empty($this->descriptors) && ! $this->assigned;
    }

    /**
     * Returns all descriptors that are websocket
     *
     * @param \Swoole\Connection\Iterator $descriptors
     *
     * @return array
     */
    protected function getWebsocketConnections(): array
    {
        return array_filter(iterator_to_array($this->server->connections), function ($fd) {
            return $this->server->isEstablished($fd);
        });
    }

    /**
     * @param int $fd
     *
     * @return bool
     */
    public function shouldPushToDescriptor(int $fd): bool
    {
        if (! $this->server->isEstablished($fd)) {
            return false;
        }

        return $this->broadcast ? $this->sender !== (int) $fd : true;
    }

    /**
     * Push message to related descriptors
     *
     * @param mixed $payload
     *
     * @return void
     */
    public function push($payload): void
    {
        // attach sender if not broadcast
        if (! $this->broadcast && $this->sender && ! $this->hasDescriptor($this->sender)) {
            $this->addDescriptor($this->sender);
        }

        // check if to broadcast to other clients
        if ($this->shouldBroadcast()) {
            $this->addDescriptors($this->getWebsocketConnections());
        }

        // push message to designated fds
        foreach ($this->descriptors as $descriptor) {
            if ($this->shouldPushToDescriptor($descriptor)) {
                $this->server->push($descriptor, $payload, $this->opcode);
            }
        }
    }
}

This class is mainly used for business logic processing sent to the client after data processing, including data parsing and unified encapsulation, whether to broadcast, etc.

Writing WebSocket service class

In addition to simple data receiving and sending, our online chat room has many other complex functions, so it is necessary to create a separate service class to implement these functions, such as room join and exit, user authentication and acquisition, data transmission and broadcasting, where the final will call Pusher the class to send data, it can be said that the service class is the core of the WebSocket back-end services, but this tutorial to simplify this section, just copy over an empty placeholder class, more Multifunctionality will be gradually added in subsequent tutorials:

<?php
namespace App\Services\WebSocket;

class WebSocket
{
    const PUSH_ACTION = 'push';
    const EVENT_CONNECT = 'connect';
    const USER_PREFIX = 'uid_';

    /**
     * Determine if to broadcast.
     *
     * @var boolean
     */
    protected $isBroadcast = false;

    /**
     * Scoket sender's fd.
     *
     * @var integer
     */
    protected $sender;

    /**
     * Recepient's fd or room name.
     *
     * @var array
     */
    protected $to = [];

    /**
     * Websocket event callbacks.
     *
     * @var array
     */
    protected $callbacks = [];
}

In this tutorial, in order to simplify the process, we WebSocketHandler directly call the Pusher class to send data to the client to quickly demonstrate establish WebSocket communication connection.

Rewrite WebSocketHandler processors

Finally, we re-implement the code according to the new structure of WebSocketHandler the processor onOpen and onMessage method:

<?php
/**
 * WebSocket service communication processor class
 * Author: College Jun
 */
 
namespace App\Services\WebSocket;

use App\Services\WebSocket\SocketIO\SocketIOParser;
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
{
    /**
     * @var WebSocket
     */
    protected $websocket;
    /**
     * @var Parser
     */
    protected $parser;

    public function __construct()
    {
        $this->websocket = app(WebSocket::class);
        $this->parser = app(SocketIOParser::class);

    }

    // Triggered when the connection is established
    public function onOpen(Server $server, Request $request)
    {
        if (!request()->input('sid')) {
            // Initialize the connection information to adapt socket.io-client, this code cannot be omitted, otherwise the connection cannot be established
            $payload = json_encode([
                'sid' => base64_encode(uniqid()),
                'upgrades' => [],
                'pingInterval' => config('laravels.swoole.heartbeat_idle_time') * 1000,
                'pingTimeout' => config('laravels.swoole.heartbeat_check_interval') * 1000,
            ]);
            $initPayload = Packet::OPEN . $payload;
            $connectPayload = Packet::MESSAGE . Packet::CONNECT;
            $server->push($request->fd, $initPayload);
            $server->push($request->fd, $connectPayload);
        }
    
        Log::info('WebSocket Connection establishment:' . $request->fd);
        $payload = [
            'sender'    => $request->fd,
            'fds'       => [$request->fd],
            'broadcast' => false,
            'assigned'  => false,
            'event'     => 'message',
            'message'   => 'Welcome to chat room',
        ];
        $pusher = Pusher::make($payload, $server);
        $pusher->push($this->parser->encode($pusher->getEvent(), $pusher->getMessage()));
    }

    // Triggered when a message is received
    public function onMessage(Server $server, Frame $frame)
    {
        // $frame->fd Is the client id,$frame->data Is the data sent by the client
        Log::info("From {$frame->fd} Received data: {$frame->data}");
        if ($this->parser->execute($server, $frame)) {
            // Skip heartbeat connection processing
            return;
        }
        $payload = $this->parser->decode($frame);
        ['event' => $event, 'data' => $data] = $payload;
        $payload = [
            'sender' => $frame->fd,
            'fds'    => [$frame->fd],
            'broadcast' => false,
            'assigned'  => false,
            'event'     => $event,
            'message'   => $data,
        ];
        $pusher = Pusher::make($payload, $server);
        $pusher->push($this->parser->encode($pusher->getEvent(), $pusher->getMessage()));
    }

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

First initialized in the constructor $websocket and $parser attributes, then the connection establishment callback method onOpen , it is determined request data contains sid fields not included the need to send connection initiation information to the client in order to successfully establish WebSocket connection, here we also specify the heartbeat interval time and time-out, the appropriate, we also need to profile config/laravels.php the swoole new configuration the following two items:

'heartbeat_idle_time' => 600,
'heartbeat_check_interval' => 60,

Then call the Pusher class push method to send a welcome message encoded in order to be resolved socket.io end customers.

In the method of the callback message is received onMessage, the first call the Parser class execute method determines whether the heartbeat connection, if the heartbeat connection is skipped if not treated, otherwise decoding the received information, after a simple treatment, and then through Pusher the class push method is sent back to the client.

At this point, the Swoole WebSocket server connection establishment and simple communication logic adapted to the socket.io client have been initially implemented. Next, we need to restart the Swoole WebSocket server for the code to take effect:

bin/laravels restart

socket.io client code tuning

Finally, we revise down socket.io client connection code, open resources/js/socket.js modify the connection establishment code is as follows:

import io from 'socket.io-client';
const socket = io('http://webchats.test', {
    path: '/ws',
    transports: ['websocket']
});
export default socket;

Here we set the server address to be driven by the Swoole HTTP server http://webchats.test, then set the path /ws to connect to the Swoole WebSocket server, and finally set the transport layer protocol to websocket replace the default long polling(polling) mechanism.

In addition Vue front-end components and view files and JavaScript files main entrance app.js also made some minor adjustments, the corresponding code to the code repository https://github.com/nonfu/webchat latest submitted version shall prevail, as in app.js, we printed Log of whether the Socket connection is established:

socket.on('connect', async () => {
    console.log('websocket connected: ' + socket.connected);
    ...

Next, we run npm run dev recompiled resources, access the browser http://webchats.test, you can see through F12 WebSocket connection establishment and communication of the data.

In the console Consoletab you can also see the following logs:

websocket connected: true

Indicates that the connection was established successfully.

#laravel #swoole #vue

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P6)
38.40 GEEK