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.
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).
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.
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 token
value 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.
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]);
}
}
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.
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
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
As a result, we can webchats.test
access the application:
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