Ethan Hughes

Ethan Hughes

1582067700

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

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P13): Send Text/Emotional Messages

Functional Overview

Sending messages supports a variety of formats, including ordinary text, emoticons, pictures, etc. Today we will introduce the most basic text and emoticons (Emoji is essentially a text message). Sending text messages requires entering text in the bottom text input box And click the Send button to send the message:

This is image title

To send an emoticon (only Emoji is supported here) message, you need to click the emoticon icon to pop up a selection box, and then click to select an emoticon. The emoticon will be automatically rendered to the message text box and then sent with the text message:

This is image title

Front-end components

Next, let’s first look at the front-end component implementation.

Message sending logic

The front end interface components in the chat room resources/js/pages/Chat.vue, the bottom of the corresponding transmitted message is a JavaScript code, click the send button, will call this submessmethod:

submess() {
    // Determine if the send message is empty
    if (this.chatValue !== '') {
      if (this.chatValue.length > 200) {
        Alert({
          content: 'Please enter less than 100 words'
        });
        return;
      }
      const msg = inHTMLData(this.chatValue); // Prevent xss

      const obj = {
        username: this.userid,
        src: this.src,
        img: '',
        msg,
        roomid: this.roomid,
        time: new Date(),
        api_token: this.auth_token
      };
      // Pass message
      socket.emit('message', obj);
      this.chatValue = '';
    } else {
      Alert({
        content: 'the content can not be blank'
      });
    }
}

There will be a basic check in it. For example, the message content cannot be empty or exceed 100 characters. In addition, the input information will be processed to avoid XSS attacks. After all the above processes are completed, the message object will be initialized, and then Call the following code to send a message object through WebSocket communication:

socket.emit('message', obj);

After sending, clear the contents of the text box.

Message rendering logic

Rendering logic embedded in the page message from the sub-assembly Message is achieved by two-way data binding:

<Message
    v-for="obj in getInfos"
    :key="obj._id"
    :is-self="obj.userid === userid"
    :name="obj.username"
    :head="obj.src"
    :msg="obj.msg"
    :img="obj.img"
    :mytime="obj.time"
    :container="container"
    ></Message>

Here we noticed obj.username === userid replaced with obj.userid === userid, because the original VueChat implementations userid and username are equivalent, and we are here userid and email equivalence, is-self attributes used to distinguish when rendering the message issued its own or someone else’s hair (his own hair in the right Side, others posted on the left).

Emoji components

The corresponding implementation of the Emoji selection box is as follows:

<div class="fun-li emoji">
    <i class="icon iconfont icon-emoji"></i>
    <div class="emoji-content" v-show="getEmoji">
      <div class="emoji-tabs">
        <div class="emoji-container" ref="emoji">
          <div class="emoji-block" :style="{width: Math.ceil(emoji.people.length / 5) * 48 + 'px'}">
            <span v-for="(item, index) in emoji.people" :key="index">{{item}}</span>
          </div>
          <div class="emoji-block" :style="{width: Math.ceil(emoji.nature.length / 5) * 48 + 'px'}">
            <span v-for="(item, index) in emoji.nature" :key="index">{{item}}</span>
          </div>
          <div class="emoji-block" :style="{width: Math.ceil(emoji.items.length / 5) * 48 + 'px'}">
            <span v-for="(item, index) in emoji.items" :key="index">{{item}}</span>
          </div>
          <div class="emoji-block" :style="{width: Math.ceil(emoji.place.length / 5) * 48 + 'px'}">
            <span v-for="(item, index) in emoji.place" :key="index">{{item}}</span>
          </div>
          <div class="emoji-block" :style="{width: Math.ceil(emoji.single.length / 5) * 48 + 'px'}">
            <span v-for="(item, index) in emoji.single" :key="index">{{item}}</span>
          </div>
        </div>
        <div class="tab">
          <!-- <a href="#hot"><span>Hot</span></a>
          <a href="#people"><span>People</span></a> -->
        </div>
      </div>
    </div>
  </div>

The specific rendering logic is not the focus of this project. Interested students can go through the source code by themselves.

Run npm run dev recompiled front-end resources for the changes to take effect.

Back-end implementation

Writing API resource classes

Since the message rendering components Message need to pass the message data will be rendered, and the front-end and back-end message object attribute messages table can not be one to one, so we can write an API resource to do automatic conversion data structures between the two.

Prior to this, we first Message define its relationship with the user model class:

<?php
namespace App;

use Illuminate\Database\Eloquent\Model;

class Message extends Model
{
    public $timestamps = false;

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Then create by Artisan command Message API resource corresponding model class:

php artisan make:resource MessageResource

The path corresponding to the generated command app/Http/Resources/MessageResource.php to write conversion process toArray is as follows:

<?php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class MessageResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'userid' => $this->user->email,
            'username' => $this->user->name,
            'src' => $this->user->avatar,
            'msg' => $this->msg,
            'img' => $this->img,
            'roomid' => $this->room_id,
            'time' => $this->created_at
        ];
    }
}

The target structure we transform must be consistent with the front-end message object attribute field names, so that the message data returned by the back-end can be rendered normally on the front-end.

Modify history chat history interface

Next, we can write before the interface chat history applications above MessageResource do return JSON data structures automatically converted, open app/Http/Controllers/MessageController.php, amend history as follows:

use App\Http\Resources\MessageResource;

/**
 * Get historical chat history
 * @param Request $request
 * @return \Illuminate\Http\JsonResponse
 */
public function history(Request $request)
{
    ...
    // Paging query messages
    $messages = Message::where('room_id', $roomId)->skip($skip)->take($limit)->orderBy('created_at', 'asc')->get();
    $messagesData = [];
    if ($messages) {
        // Automatic transformation of JSON data structures based on API resource classes
        $messagesData = MessageResource::collection($messages);
    }
    // Return response
    return response()->json([
        'data' => [
            'errno' => 0,
            'data' => $messagesData,
            'total' => $messageTotal,
            'current' => $current
        ]
    ]);
}

Note: For the implementation principle of the API resource class, you can refer to the corresponding documents . We only use it here and do not introduce it in depth.

At this point, we are messages filling some test data in the table:

This is image title

Restart the Swoole HTTP server, and you can see the rendered historical chat records in the front-end chat room room 1:

This is image title

You can view all historical messages by scrolling up and down.

Implement message sending and broadcasting functions

Finally, we implement message sending and broadcasting functions based on Websocket.

Open the back-end Websocket routing file routes/websocket.php, write the implementation code that receives the message and broadcasts it to all online users in the chat room:

use App\Message;
use Carbon\Carbon;
    
WebsocketProxy::on('message', function (WebSocket $websocket, $data) {
    if (!empty($data['api_token']) && ($user = User::where('api_token', $data['api_token'])->first())) {
        // Get message content
        $msg = $data['msg'];
        $roomId = intval($data['roomid']);
        $time = $data['time'];
        // Message content or room number cannot be empty
        if(empty($msg) || empty($roomId)) {
            return;
        }
        // Record log
        Log::info($user->name . 'in the room' . $roomId . 'Post message: ' . $msg);
        // Save message to database
        $message = new Message();
        $message->user_id = $user->id;
        $message->room_id = $roomId;
        $message->msg = $msg;
        $message->img = ''; // The picture field is temporarily blank
        $message->created_at = Carbon::now();
        $message->save();
        // Broadcast messages to all users in the room
        $room = Count::$ROOMLIST[$roomId];
        $messageData = [
            'userid' => $user->email,
            'username' => $user->name,
            'src' => $user->avatar,
            'msg' => $msg,
            'img' => '',
            'roomid' => $roomId,
            'time' => $time
        ];
        $websocket->to($room)->emit('message', $messageData);
        // Update the number of unread messages in this room for all users
        $userIds = Redis::hgetall('socket_id');
        foreach ($userIds as $userId => $socketId) {
            // Update the number of unread messages per user and send them to the corresponding online users
            $result = Count::where('user_id', $userId)->where('room_id', $roomId)->first();
            if ($result) {
                $result->count += 1;
                $result->save();
                $rooms[$room] = $result->count;
            } else {
                // If a record of the number of unread messages for a user does not exist, initialize it
                $count = new Count();
                $count->user_id = $user->id;
                $count->room_id = $roomId;
                $count->count = 1;
                $count->save();
                $rooms[$room] = 1;
            }
            $websocket->to($socketId)->emit('count', $rooms);
        }
    } else {
        $websocket->emit('login', 'Login to enter chat room');
    }
});

Implementation logic is very simple, to ensure that the user is authenticated, room number and content of the message is not empty premise, after obtaining the text message sent by the client (including Emoji expression), save it to the messages table, and then broadcast to All users can be in the room, where we did not use MessageResource to do automatically translate data structure, because the server can not get WebSocket Illuminate\Http\Request instance, which can lead to JSON serialization error.

Note: The picture sending is also based on this message channel. We will implement the corresponding processing code in the next article.

Finally, we also updated the number of unread messages for users, stored them in a database, and sent them to all online users.

Test live chat

At this point, we have completed all the coding work, restart the Swoole server:

bin/laravels restart

Log in to different users in Chrome and Firefox browsers and enter the same chat room to start a live chat online:

This is image title

Because it is based on Websocket communication, the page does not need to be refreshed to instantly get messages sent by the other party.

In the next tutorial, we will introduce the implementation of picture message sending. The source code for this project has been submitted to Github: https://github.com/nonfu/webchat.

#laravel #swoole #vue #online-chat

What is GEEK

Buddha Community

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P13)
Ethan Hughes

Ethan Hughes

1582067700

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

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P13): Send Text/Emotional Messages

Functional Overview

Sending messages supports a variety of formats, including ordinary text, emoticons, pictures, etc. Today we will introduce the most basic text and emoticons (Emoji is essentially a text message). Sending text messages requires entering text in the bottom text input box And click the Send button to send the message:

This is image title

To send an emoticon (only Emoji is supported here) message, you need to click the emoticon icon to pop up a selection box, and then click to select an emoticon. The emoticon will be automatically rendered to the message text box and then sent with the text message:

This is image title

Front-end components

Next, let’s first look at the front-end component implementation.

Message sending logic

The front end interface components in the chat room resources/js/pages/Chat.vue, the bottom of the corresponding transmitted message is a JavaScript code, click the send button, will call this submessmethod:

submess() {
    // Determine if the send message is empty
    if (this.chatValue !== '') {
      if (this.chatValue.length > 200) {
        Alert({
          content: 'Please enter less than 100 words'
        });
        return;
      }
      const msg = inHTMLData(this.chatValue); // Prevent xss

      const obj = {
        username: this.userid,
        src: this.src,
        img: '',
        msg,
        roomid: this.roomid,
        time: new Date(),
        api_token: this.auth_token
      };
      // Pass message
      socket.emit('message', obj);
      this.chatValue = '';
    } else {
      Alert({
        content: 'the content can not be blank'
      });
    }
}

There will be a basic check in it. For example, the message content cannot be empty or exceed 100 characters. In addition, the input information will be processed to avoid XSS attacks. After all the above processes are completed, the message object will be initialized, and then Call the following code to send a message object through WebSocket communication:

socket.emit('message', obj);

After sending, clear the contents of the text box.

Message rendering logic

Rendering logic embedded in the page message from the sub-assembly Message is achieved by two-way data binding:

<Message
    v-for="obj in getInfos"
    :key="obj._id"
    :is-self="obj.userid === userid"
    :name="obj.username"
    :head="obj.src"
    :msg="obj.msg"
    :img="obj.img"
    :mytime="obj.time"
    :container="container"
    ></Message>

Here we noticed obj.username === userid replaced with obj.userid === userid, because the original VueChat implementations userid and username are equivalent, and we are here userid and email equivalence, is-self attributes used to distinguish when rendering the message issued its own or someone else’s hair (his own hair in the right Side, others posted on the left).

Emoji components

The corresponding implementation of the Emoji selection box is as follows:

<div class="fun-li emoji">
    <i class="icon iconfont icon-emoji"></i>
    <div class="emoji-content" v-show="getEmoji">
      <div class="emoji-tabs">
        <div class="emoji-container" ref="emoji">
          <div class="emoji-block" :style="{width: Math.ceil(emoji.people.length / 5) * 48 + 'px'}">
            <span v-for="(item, index) in emoji.people" :key="index">{{item}}</span>
          </div>
          <div class="emoji-block" :style="{width: Math.ceil(emoji.nature.length / 5) * 48 + 'px'}">
            <span v-for="(item, index) in emoji.nature" :key="index">{{item}}</span>
          </div>
          <div class="emoji-block" :style="{width: Math.ceil(emoji.items.length / 5) * 48 + 'px'}">
            <span v-for="(item, index) in emoji.items" :key="index">{{item}}</span>
          </div>
          <div class="emoji-block" :style="{width: Math.ceil(emoji.place.length / 5) * 48 + 'px'}">
            <span v-for="(item, index) in emoji.place" :key="index">{{item}}</span>
          </div>
          <div class="emoji-block" :style="{width: Math.ceil(emoji.single.length / 5) * 48 + 'px'}">
            <span v-for="(item, index) in emoji.single" :key="index">{{item}}</span>
          </div>
        </div>
        <div class="tab">
          <!-- <a href="#hot"><span>Hot</span></a>
          <a href="#people"><span>People</span></a> -->
        </div>
      </div>
    </div>
  </div>

The specific rendering logic is not the focus of this project. Interested students can go through the source code by themselves.

Run npm run dev recompiled front-end resources for the changes to take effect.

Back-end implementation

Writing API resource classes

Since the message rendering components Message need to pass the message data will be rendered, and the front-end and back-end message object attribute messages table can not be one to one, so we can write an API resource to do automatic conversion data structures between the two.

Prior to this, we first Message define its relationship with the user model class:

<?php
namespace App;

use Illuminate\Database\Eloquent\Model;

class Message extends Model
{
    public $timestamps = false;

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Then create by Artisan command Message API resource corresponding model class:

php artisan make:resource MessageResource

The path corresponding to the generated command app/Http/Resources/MessageResource.php to write conversion process toArray is as follows:

<?php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class MessageResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'userid' => $this->user->email,
            'username' => $this->user->name,
            'src' => $this->user->avatar,
            'msg' => $this->msg,
            'img' => $this->img,
            'roomid' => $this->room_id,
            'time' => $this->created_at
        ];
    }
}

The target structure we transform must be consistent with the front-end message object attribute field names, so that the message data returned by the back-end can be rendered normally on the front-end.

Modify history chat history interface

Next, we can write before the interface chat history applications above MessageResource do return JSON data structures automatically converted, open app/Http/Controllers/MessageController.php, amend history as follows:

use App\Http\Resources\MessageResource;

/**
 * Get historical chat history
 * @param Request $request
 * @return \Illuminate\Http\JsonResponse
 */
public function history(Request $request)
{
    ...
    // Paging query messages
    $messages = Message::where('room_id', $roomId)->skip($skip)->take($limit)->orderBy('created_at', 'asc')->get();
    $messagesData = [];
    if ($messages) {
        // Automatic transformation of JSON data structures based on API resource classes
        $messagesData = MessageResource::collection($messages);
    }
    // Return response
    return response()->json([
        'data' => [
            'errno' => 0,
            'data' => $messagesData,
            'total' => $messageTotal,
            'current' => $current
        ]
    ]);
}

Note: For the implementation principle of the API resource class, you can refer to the corresponding documents . We only use it here and do not introduce it in depth.

At this point, we are messages filling some test data in the table:

This is image title

Restart the Swoole HTTP server, and you can see the rendered historical chat records in the front-end chat room room 1:

This is image title

You can view all historical messages by scrolling up and down.

Implement message sending and broadcasting functions

Finally, we implement message sending and broadcasting functions based on Websocket.

Open the back-end Websocket routing file routes/websocket.php, write the implementation code that receives the message and broadcasts it to all online users in the chat room:

use App\Message;
use Carbon\Carbon;
    
WebsocketProxy::on('message', function (WebSocket $websocket, $data) {
    if (!empty($data['api_token']) && ($user = User::where('api_token', $data['api_token'])->first())) {
        // Get message content
        $msg = $data['msg'];
        $roomId = intval($data['roomid']);
        $time = $data['time'];
        // Message content or room number cannot be empty
        if(empty($msg) || empty($roomId)) {
            return;
        }
        // Record log
        Log::info($user->name . 'in the room' . $roomId . 'Post message: ' . $msg);
        // Save message to database
        $message = new Message();
        $message->user_id = $user->id;
        $message->room_id = $roomId;
        $message->msg = $msg;
        $message->img = ''; // The picture field is temporarily blank
        $message->created_at = Carbon::now();
        $message->save();
        // Broadcast messages to all users in the room
        $room = Count::$ROOMLIST[$roomId];
        $messageData = [
            'userid' => $user->email,
            'username' => $user->name,
            'src' => $user->avatar,
            'msg' => $msg,
            'img' => '',
            'roomid' => $roomId,
            'time' => $time
        ];
        $websocket->to($room)->emit('message', $messageData);
        // Update the number of unread messages in this room for all users
        $userIds = Redis::hgetall('socket_id');
        foreach ($userIds as $userId => $socketId) {
            // Update the number of unread messages per user and send them to the corresponding online users
            $result = Count::where('user_id', $userId)->where('room_id', $roomId)->first();
            if ($result) {
                $result->count += 1;
                $result->save();
                $rooms[$room] = $result->count;
            } else {
                // If a record of the number of unread messages for a user does not exist, initialize it
                $count = new Count();
                $count->user_id = $user->id;
                $count->room_id = $roomId;
                $count->count = 1;
                $count->save();
                $rooms[$room] = 1;
            }
            $websocket->to($socketId)->emit('count', $rooms);
        }
    } else {
        $websocket->emit('login', 'Login to enter chat room');
    }
});

Implementation logic is very simple, to ensure that the user is authenticated, room number and content of the message is not empty premise, after obtaining the text message sent by the client (including Emoji expression), save it to the messages table, and then broadcast to All users can be in the room, where we did not use MessageResource to do automatically translate data structure, because the server can not get WebSocket Illuminate\Http\Request instance, which can lead to JSON serialization error.

Note: The picture sending is also based on this message channel. We will implement the corresponding processing code in the next article.

Finally, we also updated the number of unread messages for users, stored them in a database, and sent them to all online users.

Test live chat

At this point, we have completed all the coding work, restart the Swoole server:

bin/laravels restart

Log in to different users in Chrome and Firefox browsers and enter the same chat room to start a live chat online:

This is image title

Because it is based on Websocket communication, the page does not need to be refreshed to instantly get messages sent by the other party.

In the next tutorial, we will introduce the implementation of picture message sending. The source code for this project has been submitted to Github: https://github.com/nonfu/webchat.

#laravel #swoole #vue #online-chat

Ethan Hughes

Ethan Hughes

1582164420

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

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P14): Send a Picture Message

In the last tutorial we demonstrated the release of text/emotional messages in chat rooms. Today we will look at how to post picture messages.

Front-end interaction code

We started from the front end assembly, assembly in a chat room Chat.Vue, the client core logic to send pictures located fileup approach, we need to be adjusted to fit the rear end of the original code based interface Laravel + Swoole of:

fileup() {
    const that = this;
    const file1 = document.getElementById('inputFile').files[0];
    if (file1) {
      const formdata = new window.FormData();
      formdata.append('file', file1);
      formdata.append('api_token', this.auth_token);
      formdata.append('roomid', that.roomid);
      this.$store.dispatch('uploadImg', formdata);
      const fr = new window.FileReader();
      fr.onload = function () {
        const obj = {
          username: that.userid,
          src: that.src,
          img: fr.result,
          msg: '',
          roomid: that.roomid,
          time: new Date(),
          api_token: that.auth_token
        };
        socket.emit('message', obj);
      };
      fr.readAsDataURL(file1);
      this.$nextTick(() => {
        this.container.scrollTop = 10000;
      });
    } else {
      console.log('Must have file');
    }
},

When we click on the camera icon in the chat room, the image upload window will pop up:

This is image title

After selecting the picture, it will call the above fileup method to upload pictures.

It involves two logic: first calls the back-end interface to upload pictures based on the HTTP protocol and save the message to the messages table, it will send a message to Websocket server after a successful upload, and then by Websocket server broadcasts a message to all online users.

The upload image corresponds to this line of code:

this.$store.dispatch('uploadImg', formdata);

The final call to the back-end interface code is located resources/js/api/server.js in:

// upload image
postUploadFile: data => Axios.post('/file/uploadimg', data, {
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
}),

We will write this interface in the backend shortly.

Sending a picture message based on Websocket corresponds to this line of code:

socket.emit('message', obj);

This is no different than sending a text message before the code is simply obj there to add a imgfield only.

Image upload interface

Next, we write an image upload interface on the Laravel backend.

In the routes/api.php new route file/uploadimg:

Route::middleware('auth:api')->group(function () {
    ...
    Route::post('/file/uploadimg', 'FileController@uploadImage');
}

Then create the controller with Artisan commands FileController:

php artisan make:controller FileController

In the newly generated file controller app/Http/Controllers/FileController.php in preparation uploadImage codes are as follows:

<?php
namespace App\Http\Controllers;

use App\Message;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class FileController extends Controller
{
    public function uploadImage(Request $request)
    {
        if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->has('roomid')) {
            return response()->json([
                'data' => [
                    'errno' => 500,
                    'msg'   => 'Invalid parameter (room number/picture file is empty or invalid)'
                ]
            ]);
        }
        $image = $request->file('file');
        $time = time();
        $filename = md5($time . mt_rand(0, 10000)) . '.' . $image->extension();
        $path = $image->storeAs('images/' . date('Y/m/d', $time), $filename, ['disk' => 'public']);
        if ($path) {
            // If the picture is uploaded successfully, the corresponding picture message is saved to the messages table
            $message = new Message();
            $message->user_id = auth('api')->id();
            $message->room_id = $request->post('roomid');
            $message->msg = '';  // Text message left blank
            $message->img = Storage::disk('public')->url($path);
            $message->created_at = Carbon::now();
            $message->save();
            return response()->json([
                'data' => [
                    'errno' => 200,
                    'msg'   => 'Saved successfully'
                ]
            ]);
        } else {
            return response()->json([
                'data' => [
                    'errno' => 500,
                    'msg'   => 'File upload failed, please try again'
                ]
            ]);
        }
    }
}

This mainly involves image upload and message saving logic. Because we will save the picture to the storage/public next directory, in order to let the picture can be requested through the Web URL, you need to storage create a soft catalog:

php artisan storage:link

Websocket server broadcast

Finally, we in routes/websocket.php the messagechannel complementary picture message processing logic:

WebsocketProxy::on('message', function (WebSocket $websocket, $data) {
    ...
    // Get message content
    $msg = $data['msg'];
    $img = $data['img'];
    $roomId = intval($data['roomid']);
    $time = $data['time'];
    // Message content (including pictures) or room number cannot be empty
    if((empty($msg)  && empty($img))|| empty($roomId)) {
        return;
    }
    // Record log
    Log::info($user->name . 'in the room' . $roomId . 'Post message: ' . $msg);
    // Save messages to the database (except for picture messages, because they were saved during the upload)
    if (empty($img)) {
        $message = new Message();
        $message->user_id = $user->id;
        $message->room_id = $roomId;
        $message->msg = $msg;  // Text message
        $message->img = '';  // Picture message left blank
        $message->created_at = Carbon::now();
        $message->save();
    }
    // Broadcast messages to all users in the room
    $room = Count::$ROOMLIST[$roomId];
    $messageData = [
        'userid' => $user->email,
        'username' => $user->name,
        'src' => $user->avatar,
        'msg' => $msg,
        'img' => $img,
        'roomid' => $roomId,
        'time' => $time
    ];
    $websocket->to($room)->emit('message', $messageData);
    ...

Very simple, just add the picture message field uploaded by the client to the field of the previous broadcast message, without any other logic.

At this point, we can complete the front-end and back-end code for image message sending. Next, we test the sending of image messages on the chat room interface.

Test image message release

Before you start, recompile the front-end resources:

npm run dev

Make front-end code changes take effect. And restart Swoole HTTP and WebSocket server:

bin/laravels restart

Let the backend code changes take effect.

Then, open the chat room in Chrome and Firefox browsers, log in and enter the same room, you can send picture messages to each other in real time:

This is image title

At this point, we have completed the main function of the chat room. Next, we will optimize the project code, especially the performance and elegance of the back-end WebSocket communication.

#laravel #swoole #vue #chat-room

Luna  Mosciski

Luna Mosciski

1600583123

8 Popular Websites That Use The Vue.JS Framework

In this article, we are going to list out the most popular websites using Vue JS as their frontend framework.

Vue JS is one of those elite progressive JavaScript frameworks that has huge demand in the web development industry. Many popular websites are developed using Vue in their frontend development because of its imperative features.

This framework was created by Evan You and still it is maintained by his private team members. Vue is of course an open-source framework which is based on MVVM concept (Model-view view-Model) and used extensively in building sublime user-interfaces and also considered a prime choice for developing single-page heavy applications.

Released in February 2014, Vue JS has gained 64,828 stars on Github, making it very popular in recent times.

Evan used Angular JS on many operations while working for Google and integrated many features in Vue to cover the flaws of Angular.

“I figured, what if I could just extract the part that I really liked about Angular and build something really lightweight." - Evan You

#vuejs #vue #vue-with-laravel #vue-top-story #vue-3 #build-vue-frontend #vue-in-laravel #vue.js

Ethan Hughes

Ethan Hughes

1581665400

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

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P8): Websocket Server End Reconstruction and User Authentication

In the last tutorial, we showed you the implementation of logging in to a chat room based on the Vue + Muse UI front end, but this part of the implementation is mainly the interaction between the front end and the Swoole HTTP server, which does not involve WebSocket connection. Today we will demonstrate Websocket-based Connection authentication implementation. Before that, we still need to spend some effort to refactor the current Websocket backend implementation in order to facilitate subsequent development.

Websocket server refactoring

Prior to the establishment of Websocket socket.io client and server connection time, we have set aside a App\Services\WebSocket\WebSocket class of this hint, and referred to follow-up with Websocket communication-related business logic migrated to implement this class, \App\Services\WebSocket\WebSocketHandler will bear only entry function, so today will focus on the reconstruction WebSocket classes.

Create room interface and implementation class

Before you start writing WebSocket before the class code, we first solve the peripheral business logic, chat room feature an inseparable module module room, where we first refer swooletw/laravel-swoole achieve this expansion pack to the relevant class rooms are migrated, to save app/Services/WebSocket/Rooms Under contents:

This is image title

Among them, the RoomContract.php code is as follows:

<?php

namespace App\Services\Websocket\Rooms;

interface RoomContract
{
    /**
     * Rooms key
     *
     * @const string
     */
    public const ROOMS_KEY = 'rooms';

    /**
     * Descriptors key
     *
     * @const string
     */
    public const DESCRIPTORS_KEY = 'fds';

    /**
     * Do some init stuffs before workers started.
     *
     * @return RoomContract
     */
    public function prepare(): RoomContract;

    /**
     * Add multiple socket fds to a room.
     *
     * @param int fd
     * @param array|string rooms
     */
    public function add(int $fd, $rooms);

    /**
     * Delete multiple socket fds from a room.
     *
     * @param int fd
     * @param array|string rooms
     */
    public function delete(int $fd, $rooms);

    /**
     * Get all sockets by a room key.
     *
     * @param string room
     *
     * @return array
     */
    public function getClients(string $room);

    /**
     * Get all rooms by a fd.
     *
     * @param int fd
     *
     * @return array
     */
    public function getRooms(int $fd);
}

This defines the interface of the room. The two default implementations are based on Redis as the storage medium RedisRoom.php:

<?php

namespace App\Services\Websocket\Rooms;

use Illuminate\Support\Arr;
use Predis\Client as RedisClient;
use Predis\Pipeline\Pipeline;

/**
 * Class RedisRoom
 */
class RedisRoom implements RoomContract
{
    /**
     * @var \Predis\Client
     */
    protected $redis;

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

    /**
     * @var string
     */
    protected $prefix = 'swoole:';

    /**
     * RedisRoom constructor.
     *
     * @param array $config
     */
    public function __construct(array $config)
    {
        $this->config = $config;
    }

    /**
     * @param \Predis\Client|null $redis
     *
     * @return RoomContract
     */
    public function prepare(RedisClient $redis = null): RoomContract
    {
        $this->setRedis($redis);
        $this->setPrefix();
        $this->cleanRooms();

        return $this;
    }

    /**
     * Set redis client.
     *
     * @param \Predis\Client|null $redis
     */
    public function setRedis(?RedisClient $redis = null)
    {
        if (! $redis) {
            $server = Arr::get($this->config, 'server', []);
            $options = Arr::get($this->config, 'options', []);

            // forbid setting prefix from options
            if (Arr::has($options, 'prefix')) {
                $options = Arr::except($options, 'prefix');
            }

            $redis = new RedisClient($server, $options);
        }

        $this->redis = $redis;
    }

    /**
     * Set key prefix from config.
     */
    protected function setPrefix()
    {
        if ($prefix = Arr::get($this->config, 'prefix')) {
            $this->prefix = $prefix;
        }
    }

    /**
     * Get redis client.
     */
    public function getRedis()
    {
        return $this->redis;
    }

    /**
     * Add multiple socket fds to a room.
     *
     * @param int fd
     * @param array|string rooms
     */
    public function add(int $fd, $rooms)
    {
        $rooms = is_array($rooms) ? $rooms : [$rooms];

        $this->addValue($fd, $rooms, RoomContract::DESCRIPTORS_KEY);

        foreach ($rooms as $room) {
            $this->addValue($room, [$fd], RoomContract::ROOMS_KEY);
        }
    }

    /**
     * Delete multiple socket fds from a room.
     *
     * @param int fd
     * @param array|string rooms
     */
    public function delete(int $fd, $rooms)
    {
        $rooms = is_array($rooms) ? $rooms : [$rooms];
        $rooms = count($rooms) ? $rooms : $this->getRooms($fd);

        $this->removeValue($fd, $rooms, RoomContract::DESCRIPTORS_KEY);

        foreach ($rooms as $room) {
            $this->removeValue($room, [$fd], RoomContract::ROOMS_KEY);
        }
    }

    /**
     * Add value to redis.
     *
     * @param $key
     * @param array $values
     * @param string $table
     *
     * @return $this
     */
    public function addValue($key, array $values, string $table)
    {
        $this->checkTable($table);
        $redisKey = $this->getKey($key, $table);

        $this->redis->pipeline(function (Pipeline $pipe) use ($redisKey, $values) {
            foreach ($values as $value) {
                $pipe->sadd($redisKey, $value);
            }
        });

        return $this;
    }

    /**
     * Remove value from reddis.
     *
     * @param $key
     * @param array $values
     * @param string $table
     *
     * @return $this
     */
    public function removeValue($key, array $values, string $table)
    {
        $this->checkTable($table);
        $redisKey = $this->getKey($key, $table);

        $this->redis->pipeline(function (Pipeline $pipe) use ($redisKey, $values) {
            foreach ($values as $value) {
                $pipe->srem($redisKey, $value);
            }
        });

        return $this;
    }

    /**
     * Get all sockets by a room key.
     *
     * @param string room
     *
     * @return array
     */
    public function getClients(string $room)
    {
        return $this->getValue($room, RoomContract::ROOMS_KEY) ?? [];
    }

    /**
     * Get all rooms by a fd.
     *
     * @param int fd
     *
     * @return array
     */
    public function getRooms(int $fd)
    {
        return $this->getValue($fd, RoomContract::DESCRIPTORS_KEY) ?? [];
    }

    /**
     * Check table for rooms and descriptors.
     *
     * @param string $table
     */
    protected function checkTable(string $table)
    {
        if (! in_array($table, [RoomContract::ROOMS_KEY, RoomContract::DESCRIPTORS_KEY])) {
            throw new \InvalidArgumentException("Invalid table name: `{$table}`.");
        }
    }

    /**
     * Get value.
     *
     * @param string $key
     * @param string $table
     *
     * @return array
     */
    public function getValue(string $key, string $table)
    {
        $this->checkTable($table);

        $result = $this->redis->smembers($this->getKey($key, $table));

        // Try to fix occasional non-array returned result
        return is_array($result) ? $result : [];
    }

    /**
     * Get key.
     *
     * @param string $key
     * @param string $table
     *
     * @return string
     */
    public function getKey(string $key, string $table)
    {
        return "{$this->prefix}{$table}:{$key}";
    }

    /**
     * Clean all rooms.
     */
    protected function cleanRooms(): void
    {
        if (count($keys = $this->redis->keys("{$this->prefix}*"))) {
            $this->redis->del($keys);
        }
    }
}

One is based on Swoole Table as the storage medium TableRoom.php:

<?php
/**
 * Created by PhpStorm.
 * User: sunqiang
 * Date: 2019/11/7
 * Time: 3:14 PM
 */

namespace App\Services\Websocket\Rooms;

use Swoole\Table;

class TableRoom implements RoomContract
{
    /**
     * @var array
     */
    protected $config;

    /**
     * @var Table
     */
    protected $rooms;

    /**
     * @var Table
     */
    protected $fds;

    /**
     * TableRoom constructor.
     *
     * @param array $config
     */
    public function __construct(array $config)
    {
        $this->config = $config;
    }

    /**
     * Do some init stuffs before workers started.
     *
     * @return RoomContract
     */
    public function prepare(): RoomContract
    {
        $this->initRoomsTable();
        $this->initFdsTable();

        return $this;
    }

    /**
     * Add a socket fd to multiple rooms.
     *
     * @param int fd
     * @param array|string rooms
     */
    public function add(int $fd, $roomNames)
    {
        $rooms = $this->getRooms($fd);
        $roomNames = is_array($roomNames) ? $roomNames : [$roomNames];

        foreach ($roomNames as $room) {
            $fds = $this->getClients($room);

            if (in_array($fd, $fds)) {
                continue;
            }

            $fds[] = $fd;
            $rooms[] = $room;

            $this->setClients($room, $fds);
        }

        $this->setRooms($fd, $rooms);
    }

    /**
     * Delete a socket fd from multiple rooms.
     *
     * @param int fd
     * @param array|string rooms
     */
    public function delete(int $fd, $roomNames = [])
    {
        $allRooms = $this->getRooms($fd);
        $roomNames = is_array($roomNames) ? $roomNames : [$roomNames];
        $rooms = count($roomNames) ? $roomNames : $allRooms;

        $removeRooms = [];
        foreach ($rooms as $room) {
            $fds = $this->getClients($room);

            if (! in_array($fd, $fds)) {
                continue;
            }

            $this->setClients($room, array_values(array_diff($fds, [$fd])));
            $removeRooms[] = $room;
        }

        $this->setRooms($fd, array_values(array_diff($allRooms, $removeRooms)));
    }

    /**
     * Get all sockets by a room key.
     *
     * @param string room
     *
     * @return array
     */
    public function getClients(string $room)
    {
        return $this->getValue($room, RoomContract::ROOMS_KEY) ?? [];
    }

    /**
     * Get all rooms by a fd.
     *
     * @param int fd
     *
     * @return array
     */
    public function getRooms(int $fd)
    {
        return $this->getValue($fd, RoomContract::DESCRIPTORS_KEY) ?? [];
    }

    /**
     * @param string $room
     * @param array $fds
     *
     * @return TableRoom
     */
    protected function setClients(string $room, array $fds): TableRoom
    {
        return $this->setValue($room, $fds, RoomContract::ROOMS_KEY);
    }

    /**
     * @param int $fd
     * @param array $rooms
     *
     * @return TableRoom
     */
    protected function setRooms(int $fd, array $rooms): TableRoom
    {
        return $this->setValue($fd, $rooms, RoomContract::DESCRIPTORS_KEY);
    }

    /**
     * Init rooms table
     */
    protected function initRoomsTable(): void
    {
        $this->rooms = new Table($this->config['room_rows']);
        $this->rooms->column('value', Table::TYPE_STRING, $this->config['room_size']);
        $this->rooms->create();
    }

    /**
     * Init descriptors table
     */
    protected function initFdsTable()
    {
        $this->fds = new Table($this->config['client_rows']);
        $this->fds->column('value', Table::TYPE_STRING, $this->config['client_size']);
        $this->fds->create();
    }

    /**
     * Set value to table
     *
     * @param $key
     * @param array $value
     * @param string $table
     *
     * @return $this
     */
    public function setValue($key, array $value, string $table)
    {
        $this->checkTable($table);

        $this->$table->set($key, ['value' => json_encode($value)]);

        return $this;
    }

    /**
     * Get value from table
     *
     * @param string $key
     * @param string $table
     *
     * @return array|mixed
     */
    public function getValue(string $key, string $table)
    {
        $this->checkTable($table);

        $value = $this->$table->get($key);

        return $value ? json_decode($value['value'], true) : [];
    }

    /**
     * Check table for exists
     *
     * @param string $table
     */
    protected function checkTable(string $table)
    {
        if (! property_exists($this, $table) || ! $this->$table instanceof Table) {
            throw new \InvalidArgumentException("Invalid table name: `{$table}`.");
        }
    }
}

Later we will create a database as a storage medium based on the DatabaseRoom class, leaving behind this particular presentation function room when the re-realization. In addition, we app/Services/WebSocket/Facades create a directory for a facade room service proxy class Room.php, and initialization code is as follows:

<?php

namespace App\Services\Websocket\Facades;

use App\Services\Websocket\Rooms\RoomContract;
use Illuminate\Support\Facades\Facade;

/**
 * @method static $this prepare()
 * @method static $this add($fd, $rooms)
 * @method static $this delete($fd, $rooms)
 * @method static array getClients($room)
 * @method static array getRooms($fd)
 *
 * @see RoomContract
 */
class Room extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'swoole.room';
    }
}

Write user authentication implementation code

The room is a static space, and dynamic users are chatting in the room. Therefore, we also need to write user authentication related implementation code in order to uniquely distinguish different users and manage the chat information of different rooms and different users. Here we still refer to swooletw/laravel-swoole achieved in app/Services/WebSocket creating a directory Authenticatable Trait to implement user authentication related business logic:

<?php

namespace App\Services\WebSocket;

use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use InvalidArgumentException;

/**
 * Trait Authenticatable
 */
trait Authenticatable
{
    protected $userId;

    /**
     * Login using current user.
     *
     * @param \Illuminate\Contracts\Auth\Authenticatable $user
     *
     * @return mixed
     */
    public function loginUsing(AuthenticatableContract $user)
    {
        return $this->loginUsingId($user->getAuthIdentifier());
    }

    /**
     * Login using current userId.
     *
     * @param $userId
     *
     * @return mixed
     */
    public function loginUsingId($userId)
    {
        return $this->join(static::USER_PREFIX . $userId);
    }

    /**
     * Logout with current sender's fd.
     *
     * @return mixed
     */
    public function logout()
    {
        if (is_null($userId = $this->getUserId())) {
            return null;
        }

        return $this->leave(static::USER_PREFIX . $userId);
    }

    /**
     * Set multiple recepients' fds by users.
     *
     * @param $users
     *
     * @return Authenticatable
     */
    public function toUser($users)
    {
        $users = is_object($users) ? func_get_args() : $users;

        $userIds = array_map(function (AuthenticatableContract $user) {
            $this->checkUser($user);

            return $user->getAuthIdentifier();
        }, $users);

        return $this->toUserId($userIds);
    }

    /**
     * Set multiple recepients' fds by userIds.
     *
     * @param $userIds
     *
     * @return Authenticatable
     */
    public function toUserId($userIds)
    {
        $userIds = is_string($userIds) || is_integer($userIds) ? func_get_args() : $userIds;

        foreach ($userIds as $userId) {
            $fds = $this->room->getClients(static::USER_PREFIX . $userId);
            $this->to($fds);
        }

        return $this;
    }

    /**
     * Get current auth user id by sender's fd.
     */
    public function getUserId()
    {
        if (! is_null($this->userId)) {
            return $this->userId;
        }

        $rooms = $this->room->getRooms($this->getSender());

        foreach ($rooms as $room) {
            if (count($explode = explode(static::USER_PREFIX, $room)) === 2) {
                $this->userId = $explode[1];
            }
        }

        return $this->userId;
    }

    /**
     * Check if a user is online by given userId.
     *
     * @param $userId
     *
     * @return bool
     */
    public function isUserIdOnline($userId)
    {
        return ! empty($this->room->getClients(static::USER_PREFIX . $userId));
    }

    /**
     * Check if user object implements AuthenticatableContract.
     *
     * @param $user
     */
    protected function checkUser($user)
    {
        if (! $user instanceOf AuthenticatableContract) {
            throw new InvalidArgumentException('user object must implement ' . AuthenticatableContract::class);
        }
    }
}

Implementing Websocket Core Classes

After completing the user authentication and the two peripheral function room, next, the official start of Websocket the class to achieve, or reference swooletw/laravel-swoole achieved, partially achieved just made adjustments to simplify and adapt hhxsv5/laravel-s Expansion Pack :

<?php
namespace App\Services\WebSocket;

use App\Services\Websocket\Rooms\RoomContract;
use Illuminate\Support\Facades\App;
use Swoole\WebSocket\Server;

class WebSocket
{
    use Authenticatable;

    const PUSH_ACTION = 'push';
    const EVENT_CONNECT = 'connect';
    const USER_PREFIX = 'uid_';

    /**
     * Websocket Server
     * @var Server
     */
    protected $server;

    /**
     * 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 = [];

    /**
     * Room adapter.
     *
     * @var RoomContract
     */
    protected $room;

    /**
     * DI Container.
     *
     * @var \Illuminate\Contracts\Container\Container
     */
    protected $container;

    /**
     * Websocket constructor.
     *
     * @param RoomContract $room
     */
    public function __construct(RoomContract $room)
    {
        $this->room = $room;
    }

    /**
     * Set broadcast to true.
     */
    public function broadcast(): self
    {
        $this->isBroadcast = true;

        return $this;
    }

    /**
     * Set multiple recipients fd or room names.
     *
     * @param integer, string, array
     *
     * @return $this
     */
    public function to($values): self
    {
        $values = is_string($values) || is_integer($values) ? func_get_args() : $values;

        foreach ($values as $value) {
            if (! in_array($value, $this->to)) {
                $this->to[] = $value;
            }
        }

        return $this;
    }

    /**
     * Join sender to multiple rooms.
     *
     * @param string, array $rooms
     *
     * @return $this
     */
    public function join($rooms): self
    {
        $rooms = is_string($rooms) || is_integer($rooms) ? func_get_args() : $rooms;

        $this->room->add($this->sender, $rooms);

        return $this;
    }

    /**
     * Make sender leave multiple rooms.
     *
     * @param array $rooms
     *
     * @return $this
     */
    public function leave($rooms = []): self
    {
        $rooms = is_string($rooms) || is_integer($rooms) ? func_get_args() : $rooms;

        $this->room->delete($this->sender, $rooms);

        return $this;
    }

    /**
     * Emit data and reset some status.
     *
     * @param string
     * @param mixed
     *
     * @return boolean
     */
    public function emit(string $event, $data): bool
    {
        $fds = $this->getFds();
        $assigned = ! empty($this->to);

        // if no fds are found, but rooms are assigned
        // that means trying to emit to a non-existing room
        // skip it directly instead of pushing to a task queue
        if (empty($fds) && $assigned) {
            return false;
        }

        $payload = [
            'sender'    => $this->sender,
            'fds'       => $fds,
            'broadcast' => $this->isBroadcast,
            'assigned'  => $assigned,
            'event'     => $event,
            'message'   => $data,
        ];

        $server = app('swoole');
        $pusher = Pusher::make($payload, $server);
        $parser = app('swoole.parser');
        $pusher->push($parser->encode($pusher->getEvent(), $pusher->getMessage()));

        $this->reset();

        return true;
    }

    /**
     * An alias of `join` function.
     *
     * @param string
     *
     * @return $this
     */
    public function in($room)
    {
        $this->join($room);

        return $this;
    }

    /**
     * Register an event name with a closure binding.
     *
     * @param string
     * @param callback
     *
     * @return $this
     */
    public function on(string $event, $callback)
    {
        if (! is_string($callback) && ! is_callable($callback)) {
            throw new \InvalidArgumentException(
                'Invalid websocket callback. Must be a string or callable.'
            );
        }

        $this->callbacks[$event] = $callback;

        return $this;
    }

    /**
     * Check if this event name exists.
     *
     * @param string
     *
     * @return boolean
     */
    public function eventExists(string $event)
    {
        return array_key_exists($event, $this->callbacks);
    }

    /**
     * Execute callback function by its event name.
     *
     * @param string
     * @param mixed
     *
     * @return mixed
     */
    public function call(string $event, $data = null)
    {
        if (! $this->eventExists($event)) {
            return null;
        }

        // inject request param on connect event
        $isConnect = $event === static::EVENT_CONNECT;
        $dataKey = $isConnect ? 'request' : 'data';

        return App::call($this->callbacks[$event], [
            'websocket' => $this,
            $dataKey => $data,
        ]);
    }

    /**
     * Set sender fd.
     *
     * @param integer
     *
     * @return $this
     */
    public function setSender(int $fd)
    {
        $this->sender = $fd;

        return $this;
    }

    /**
     * Get current sender fd.
     */
    public function getSender()
    {
        return $this->sender;
    }

    /**
     * Get broadcast status value.
     */
    public function getIsBroadcast()
    {
        return $this->isBroadcast;
    }

    /**
     * Get push destinations (fd or room name).
     */
    public function getTo()
    {
        return $this->to;
    }

    /**
     * Get all fds we're going to push data to.
     */
    protected function getFds()
    {
        $fds = array_filter($this->to, function ($value) {
            return is_integer($value);
        });
        $rooms = array_diff($this->to, $fds);

        foreach ($rooms as $room) {
            $clients = $this->room->getClients($room);
            // fallback fd with wrong type back to fds array
            if (empty($clients) && is_numeric($room)) {
                $fds[] = $room;
            } else {
                $fds = array_merge($fds, $clients);
            }
        }

        return array_values(array_unique($fds));
    }

    /**
     * Reset some data status.
     *
     * @param bool $force
     *
     * @return $this
     */
    public function reset($force = false)
    {
        $this->isBroadcast = false;
        $this->to = [];

        if ($force) {
            $this->sender = null;
            $this->userId = null;
        }

        return $this;
    }
}

In the Websocket middle, we have introduced a user authentication implementation Authenticatable Trait and rooms instance $room, this property is the RoomContract implementation class instance of an interface, we will immediately binding on its introduction. Compared with the original implementation, the middleware function is removed here, purely to simplify business logic. If necessary, we will add it later.

Let’s roughly introduce the function of each method:

  • to/setSender: Specify the object to send the message, such as a client, room;
  • join/in: Add the current user to a room;
  • leave: Leave a room;
  • emit: Message sending logic package, before writing WebsocketHandler the message code is ported to this, so that the code clearer functions, here only for sending messages, with no business logic processing;
  • on: Websocket register for event routing, registration for split to routes/websocket.php define, to facilitate the maintenance, but also to the code structure more clearly, specific business logic will be defined here, after the completion of the above-described call processing emit method for transmitting a message to the client;
  • eventExists: Determine whether the specified event route exists;
  • call: If there is an event routing, routing the call event corresponding business logic, so on, eventExists, call the three interlocking: on is responsible for registering, eventExits responsible for matching, call is responsible for implementation;
  • getFds: Get all the objects to send a message;
  • reset: Reset the state of some data to avoid reuse.

And like the rooms here we are app/Services/WebSocket/Facades directory facade to create a proxy class:

<?php

namespace App\Services\Websocket\Facades;

use Illuminate\Support\Facades\Facade;

/**
 * @method static $this broadcast()
 * @method static $this to($values)
 * @method static $this join($rooms)
 * @method static $this leave($rooms)
 * @method static boolean emit($event, $data)
 * @method static $this in($room)
 * @method static $this on($event, $callback)
 * @method static boolean eventExists($event)
 * @method static mixed call($event, $data)
 * @method static boolean close($fd)
 * @method static $this setSender($fd)
 * @method static int getSender()
 * @method static boolean getIsBroadcast()
 * @method static array getTo()
 * @method static $this reset()
 * @method static $this middleware($middleware)
 * @method static $this setContainer($container)
 * @method static $this setPipeline($pipeline)
 * @method static \Illuminate\Contracts\Pipeline\Pipeline getPipeline()
 * @method static mixed loginUsing($user)
 * @method static $this loginUsingId($userId)
 * @method static $this logout()
 * @method static $this toUser($users)
 * @method static $this toUserId($userIds)
 * @method static string getUserId()
 * @method static boolean isUserIdOnline($userId)
 *
 * @see \App\Services\WebSocket\WebSocket
 */
class Websocket extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'swoole.websocket';
    }
}

At this point, our app/Services/WebSocket directory structure of the code is as follows:

This is image title

This Websocket involves a number of container objects bound to resolve the core class routing and Websocket event registration, then we have to introduce the corresponding implementation code.

Register Websocket event routing

First, we routes create a new directory websocket.php file to store these events routes:

<?php

use Swoole\Http\Request;
use App\Services\WebSocket\WebSocket;
use App\Services\Websocket\Facades\Websocket as WebsocketProxy;

/*
|--------------------------------------------------------------------------
| Websocket Routes
|--------------------------------------------------------------------------
|
| Here is where you can register websocket events for your application.
|
*/

WebsocketProxy::on('connect', function (WebSocket $websocket, Request $request) {
    // Send welcome message
    $websocket->setSender($request->fd);
    $websocket->emit('connect', 'Welcome to chat room');

});

WebsocketProxy::on('disconnect', function (WebSocket $websocket) {
    // called while socket on disconnect
});

WebsocketProxy::on('login', function (WebSocket $websocket, $data) {
    if (!empty($data['token']) && ($user = \App\User::where('api_token', $data['token'])->first())) {
        $websocket->loginUsing($user);
        // todo Read unread messages
        $websocket->toUser($user)->emit('login', 'login successful');
    } else {
        $websocket->emit('login', 'Login to enter chat room');
    }
});

The reason is called event routing, because these routes are based on the event name passed by the client to match the call, here we initialize connect and login these two events are routed closures implemented here WebsocketProxy corresponds Websocket facade class, so static on method call eventually fall Websocket to on a method to perform, i.e., an event register corresponding service logic, implemented in a closure parameter, $websocket corresponding to the calltransmission method over $this the object, $data it is after Parser message data parsing implementation class. Thus, in a closure function, we can call Websocket any of the methods of the class, and finally by emit the method of sending a message to the client.

Container binding and route loading

Let’s look at container bindings, including binding facade and proxy classes, binding interface and implementation class, as well as the above-mentioned event routing when loaded, is the most appropriate time in Swoole Worker Start time event triggered because of the laravels expansion pack, the The Laravel container is initialized before this:

public function onWorkerStart(HttpServer $server, $workerId)
{
    parent::onWorkerStart($server, $workerId);

    // To implement gracefully reload
    // Delay to create Laravel
    // Delay to include Laravel's autoload.php
    $this->laravel = $this->initLaravel($this->laravelConf, $this->swoole);

    // Fire WorkerStart event
    $this->fireEvent('WorkerStart', WorkerStartInterface::class, func_get_args());
}

Is also very simple to implement, monitor application Swoole in the WorkerStart event you can (note not Laravel events, so you can not use Laravel event listener mechanism to achieve), another advantage of the event to listen to bind and registered container load routing is beneficial improve application performance, and if we are onRequest, onOpen, OnMesssage when such events trigger the binding, then each request must be registered again, and based on Laravel PHP-FPM application model lacks distinction, and in addition, it can not be onWorkerStart before execution binding, because this time Laravel not initialized, there is no Application container this thing, where to register it?

Well, ado, we app/Events created the directory WorkerStartEvent.php file, then write binding container loading and routing code is as follows:

<?php
/**
 * Websocket related service container binding and route loading
 * Author: College Jun
 */
namespace App\Events;

use App\Services\WebSocket\Parser;
use App\Services\Websocket\Rooms\RoomContract;
use App\Services\WebSocket\WebSocket;
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
use Illuminate\Container\Container;
use Illuminate\Pipeline\Pipeline;
use Swoole\Http\Server;

class WorkerStartEvent implements WorkerStartInterface
{
    public function __construct()
    {
    }

    public function handle(Server $server, $workerId)
    {
        $isWebsocket = config('laravels.websocket.enable') == true;
        if (!$isWebsocket) {
            return;
        }
        // Laravel has been initialized when the WorkerStart event occurs. It is most suitable to do some initialization of component binding to the container here.
        app()->singleton(Parser::class, function () {
            $parserClass = config('laravels.websocket.parser');
            return new $parserClass;
        });
        app()->alias(Parser::class, 'swoole.parser');

        app()->singleton(RoomContract::class, function () {
            $driver = config('laravels.websocket.drivers.default', 'table');
            $driverClass = config('laravels.websocket.drivers.' . $driver);
            $driverConfig = config('laravels.websocket.drivers.settings.' . $driver);
            $roomInstance = new $driverClass($driverConfig);
            if ($roomInstance instanceof RoomContract) {
                $roomInstance->prepare();
            }
            return $roomInstance;
        });
        app()->alias(RoomContract::class, 'swoole.room');

        app()->singleton(WebSocket::class, function (Container $app) {
            return new WebSocket($app->make(RoomContract::class));
        });
        app()->alias(WebSocket::class, 'swoole.websocket');

        // Introducing Websocket routing files
        $routePath = base_path('routes/websocket.php');
        require $routePath;
    }
}

Note that I am here only judge in the case Websocket server startup, before doing the appropriate service Websocket bound container loading and routing, including Parser, RoomContract, WebSocket concrete realization of bindings and aliases settings (can be used to resolve the facade class), and finally Load the Websocket event routing file for the corresponding event routing to take effect.

Finally, do not forget config/laravels.php the event_handlers bound events and the corresponding processor to take effect:

'event_handlers' => [
    'WorkerStart' => \App\Events\WorkerStartEvent::class,
],

Improve Websocket configuration

In the binding implementation of the container above, also added a number of configuration items, we are in config/laravels.php the websocket supplemented these configurations:

'websocket' => [
   'enable' => true,
   'handler' => \App\Services\WebSocket\WebSocketHandler::class,
   'middleware' => [
       //\Illuminate\Auth\Middleware\Authenticate::class,
       //\App\Http\Middleware\VerifyCsrfToken::class,
   ],
   'parser' => \App\Services\WebSocket\SocketIO\SocketIOParser::class,
   'drivers' => [
       'default' => 'table',
       'table' => \App\Services\Websocket\Rooms\TableRoom::class,
       'redis' => \App\Services\Websocket\Rooms\RedisRoom::class,
       'settings' => [
           'table' => [
               'room_rows' => 4096,
               'room_size' => 2048,
               'client_rows' => 8192,
               'client_size' => 2048,
           ],
           'redis' => [
               'server' => [
                   'host' => env('REDIS_HOST', '127.0.0.1'),
                   'password' => env('REDIS_PASSWORD', null),
                   'port' => env('REDIS_PORT', 6379),
                   'database' => 0,
                   'persistent' => true,
               ],
               'options' => [
                   //
               ],
               'prefix' => 'swoole:',
           ],
       ],
   ],
],

At this point, after the initial completion of the business logic reconfiguration of the Websocket backend, we have the last step to connect them in the processor entry.

Refactoring the WebSocketHandler processor

That we have to make up the ring, reconfigurable App\Services\WebSocket\WebSocketHandler processor implementation class as follows:

<?php
namespace App\Services\WebSocket;

use App\Services\WebSocket\SocketIO\Packet;
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('swoole.websocket');
        $this->parser = app('swoole.parser');
    }

    // Triggered when the connection is established
    public function onOpen(Server $server, Request $request)
    {
        // If the connection is not established, establish the connection first
        if (!request()->input('sid')) {
            // Initial connection information socket.io-client
            $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);
        if ($this->websocket->eventExists('connect')) {
            $this->websocket->call('connect', $request);
        }
    }

    // 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)) {
            return;
        }
        $payload = $this->parser->decode($frame);
        ['event' => $event, 'data' => $data] = $payload;
        $this->websocket->reset(true)->setSender($frame->fd);
        if ($this->websocket->eventExists($event)) {
            $this->websocket->call($event, $data);
        } else {
            // Pocket processing, generally not performed here
            return;
        }
    }

    // Triggered when the connection is closed
    public function onClose(Server $server, $fd, $reactorId)
    {
        Log::info('WebSocket Connection closed:' . $fd);
        $this->websocket->setSender($fd);
        if ($this->websocket->eventExists('disconnect')) {
            $this->websocket->call('disconnect', 'Connection closed');
        }
    }
}

Before jumbled code has been split into the Websocket event routing class event calls and messaging implementation code, the entire processor implements become leaner, more elegant, and here, we will Parser parse out the data from the client implementation class event name and message content, and then calls Websocket the instance eventExists method of determining whether there is a corresponding event routing, if present, by a call method corresponding to the closure function call, the business logic processing, and finally sends a message to the corresponding client.

At this point, even if our Swoole Websocket server code is refactored, there may be details optimization later, but the main structure has been completed, and the scalability is very good, especially the processing of the event routing, making Swoole-based Laravel The application is truly integrated with Laravel’s ideas, thanks to the design ideas provided by swooletw/laravel-swoole.

Websocket client and server user authentication

If it is based on Session authentication, you can totally follow swooletw/laravel-swoole own middleware and user authentication to achieve, but we end this project before and after the separation, I am here simply in the login event route by acquiring request data from the token field For certification:

WebsocketProxy::on('login', function (WebSocket $websocket, $data) {
    if (!empty($data['token']) && ($user = \App\User::where('api_token', $data['token'])->first())) {
        $websocket->loginUsing($user);
        // todo Read unread messages
        $websocket->toUser($user)->emit('login', 'login successful');
    } else {
        $websocket->emit('login', 'Login to enter chat room');
    }
});

The corresponding client code is as follows(resources/js/app.js):

const userId = store.state.userInfo.userid;
const token = store.state.userInfo.token;
if (userId) {
    socket.emit('login', {
        name: userId,
        token: token
    });
}

Restart the Swoole Websocket server for the refactored code to take effect:

bin/laravels reload

Then refresh the chat page, by F12->Network->WS you can see the new Websocket the communication data:

This is image title

In the next tutorial, we will implement front-end and back-end room initialization and client code optimization.

The project code has been submitted to the Github repository: https://github.com/nonfu/webchat , you can get the latest code from here.

#laravel #swoole #vue #php

Ethan Hughes

Ethan Hughes

1581901320

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

Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P11): Get History Chat History After Entering The Chat Room

In the last tutorial, we showed you how to get unread messages after the user logs in. Today we enter the chat room to see what happened in the chat room.

Chat room page initialization logic

The corresponding Vue component of the chat room page is resources/js/pages/Chat.vue, we open this file and see the initial rendering logic of the page and the interaction with the back-end interface.

First look created and mounted hook function of these two pages initialization phase calls.

created hook function

In the created process, it will be from the current page URL query string in room obtain information (Tips: URL chat room is http://webchats.test/#/chat?roomId=1):

async created() {
  const roomId = queryString(window.location.href, 'roomId');
  this.roomid = roomId;
  if (!roomId) {
    this.$router.push({path: '/'});
  }
  if (!this.userid) {
    // Prevent unlogged
    this.$router.push({path: '/login'});
  }
  const res = await url.getNotice();
  this.noticeList = res.data.noticeList;
  if (res.data.version !== res.data.version) {
    this.noticeBar = false;
  }
  this.noticeVersion = res.data.version;
},

If the room information is empty, it will jump to the homepage, if the user is not logged in, it will jump to the login interface.

Then call url.getNotice() to obtain information bulletin, corresponding to the back-end interface call is located resources/js/api/server.js:

// Request announcement
getNotice: () => Axios.get('https://s3.qiufengh.com/config/notice-config.js'

This piece does not belong to the core logic, and the specific details are ignored.

mounted hook function

Next, look at mounted logic function:

async mounted() {
  // WeChat rebound bug
  ios();
  this.container = document.querySelector('.chat-inner');
  // socket internal,this pointer to question
  const that = this;
  await this.$store.commit('setRoomDetailInfos');
  await this.$store.commit('setTotal', 0);
  const obj = {
    name: this.userid,
    src: this.src,
    roomid: this.roomid
  };
  socket.emit('room', obj);
  socket.on('room', function (obj) {
    that.$store.commit('setUsers', obj);
  });
  socket.on('roomout', function (obj) {
    that.$store.commit('setUsers', obj);
  });
  loading.show();
  setTimeout(async () => {
    const data = {
      total: +this.getTotal,
      current: +this.current,
      roomid: this.roomid
    };
    this.isloading = true;
    await this.$store.dispatch('getAllMessHistory', data);
    this.isloading = false;
    loading.hide();
    this.$nextTick(() => {
      this.container.scrollTop = 10000;
    });
  }, 500);

  this.container.addEventListener('scroll', debounce(async (e) => {
    if (e.target.scrollTop >= 0 && e.target.scrollTop < 50) {
      this.$store.commit('setCurrent', +this.getCurrent + 1);
      const data = {
        total: +this.getTotal,
        current: +this.getCurrent,
        roomid: this.roomid
      };
      this.isloading = true;
      await this.$store.dispatch('getAllMessHistory', data);
      this.isloading = false;
    }
  }, 50));

  this.$refs.emoji.addEventListener('click', function(e) {
    var target = e.target || e.srcElement;
    if (!!target && target.tagName.toLowerCase() === 'span') {
      that.chatValue = that.chatValue + target.innerHTML;
    }
    e.stopPropagation();
  });
},

Here is first initialized room total number of messages and information, and then Websocket channel routing server roominitiated the request, the server has to inform the user enters the room, and by listening to the client roomand roomoutreturns the server to receive the event message into the room and left the room:

socket.emit('room', obj);
socket.on('room', function (obj) {
    that.$store.commit('setUsers', obj);
});
socket.on('roomout', function (obj) {
    that.$store.commit('setUsers', obj);
});

In the callback function, we set the online user information of the room through Vuex.

In order to achieve this Laravel based authentication backend API interface, we are united in obj the new api_token field:

Of course, we need to calculate the property computed settings auth_token to make the above code to take effect:

computed: {
  ... // Other configurations
  ...mapState({
    userid: state => state.userInfo.userid,
    src: state => state.userInfo.src,
    auth_token: state => state.userInfo.token,
  })
},

Continue to look at mounted the logic followed by setTimeout the definition of a timer, mainly to get the chat room chat history from the server room, too, we have added in the Request field api_token fields:

const data = {
    total: +this.getTotal,
    current: +this.current,
    roomid: this.roomid,
    api_token: this.auth_token
};
this.isloading = true;
await this.$store.dispatch('getAllMessHistory', data);

getAllMessHistory The final request interface is defined in the rear end of resources/js/api/server.js the:

// Get all historical chats in the current room
RoomHistoryAll: data => Axios.get('/history/message', {
    params: data
}),

There is the request parameters of total the total number indicates that the message, current the current page (or the current screen news, because a large amount of information possible, here it is necessary to do the pagination process), roomid represents a room ID, api_token interface for authentication.

We will need to implement this historical chat history interface in the backend later, this is an HTTP request.

The last is the definition of event listening for two components: one is the processing of the chat window scroll operation, and the other is the processing after the Emoji icon is clicked. Note that when scrolling chat window also involves calling the chat history interfaces, which in fact we are very familiar pull-down refresh the phone App, we need here objparameter also joined api_token the field:

const data = {
    total: +this.getTotal,
    current: +this.getCurrent,
    roomid: this.roomid,
    api_token: this.auth_token
};

It can be seen that during the initialization phase of the chat room page, the two most important things are to establish a connection with the WebSocket server, tell it that the user has entered a certain room, and then obtain the historical chat history of this room and render it to the chat interface.

Next, we implement the corresponding Websocket interaction logic and HTTP routing interface on the back end.

History chat history backend interface

Let’s first implement the interface for obtaining historical chat records from the backend.

Create the controller first MessageController:

php artisan make:controller MessageController

The controller then write app/Http/Controllers/MessageController.php codes are as follows:

<?php
namespace App\Http\Controllers;

use App\Message;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class MessageController extends Controller
{
    /**
     * Get historical chat history
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function history(Request $request)
    {
        $roomId = intval($request->get('roomid'));
        $current = intval($request->get('current'));
        $total = intval($request->get('total'));
        if ($roomId <= 0 || $current <= 0) {
            Log::error('Invalid room and page information');
            return response()->json([
                'data' => [
                    'errno' => 1
                ]
            ]);
        }
        // Get the total number of messages
        $messageTotal = Message::where('room_id', $roomId)->count();
        $limit = 20;  // 20 messages per page
        $skip = ($current - 1) * 20;  // From which message
        // Paging query messages
        $messageData = Message::where('room_id', $roomId)->skip($skip)->take($limit)->orderBy('created_at', 'desc')->get();
        // Return response
        return response()->json([
            'data' => [
                'errno' => 0,
                'data' => $messageData,
                'total' => $messageTotal,
                'current' => $current
            ]
        ]);
    }
}

This controller currently provides only history method that returns JSON formatted chat history.

Finally, we in the API route file routes/api.php definitions this route, the route after authentication is required to access, and because of this request will include api_token field, so certification will by auth:api done automatically Middleware:

Route::middleware('auth:api')->group(function () {
    ...
    Route::get('/history/message', 'MessageController@history');
});

Next, you can test the request on the front end. Since there is no message yet, the data returned is empty:

This is image title

In the next tutorial, we will write the code for users to enter and exit the room on the back end of the Websocket server, and update the online user information after entering and exiting, and push it to all online users in the chat room.

#laravel #swoole #vue