1582164420
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.
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:
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.
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
Finally, we in routes/websocket.php
the message
channel 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.
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:
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
1587123105
Exceelent
1582164420
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.
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:
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.
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
Finally, we in routes/websocket.php
the message
channel 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.
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:
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
1582067700
Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P13): Send Text/Emotional Messages
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:
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:
Next, let’s first look at the front-end component implementation.
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 submess
method:
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.
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).
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.
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.
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:
Restart the Swoole HTTP server, and you can see the rendered historical chat records in the front-end chat room room 1:
You can view all historical messages by scrolling up and down.
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.
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:
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
1600583123
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
1582249320
Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P15): Implement User Avatar Upload Function
I missed the avatar upload function before, here to add, the avatar upload entrance is located in my-> modify avatar:
We just need to click upload the avatar on the avatar upload page, crop it, save the avatar, and then upload and save it to the user table.
However, this page is still reporting errors:
Muse UI 3.0 version or removal of the mu-icon-button
causes of elements support the following, we will adjust it for the mu-button
components.
Open the avatar upload page components resources/js/pages/Avatar.vue
, mu-icon-button
introduced located in Header
the sub-components:
<Header></Header>
Open the file resources/js/components/Header/index.vue
that defines the component and find the following template code:
<mu-appbar title="Title">
<mu-icon-button icon="chevron_left" slot="left" @click="goback"/>
<div class="center">
</div>
<mu-icon-button icon="expand_more" slot="right"/>
</mu-appbar>
All mu-icon-button
components of adjusted mu-button
components:
<mu-appbar title="Title">
<mu-button icon slot="left" @click="goback">
<mu-icon value="chevron_left"></mu-icon>
</mu-button>
<div class="center">
</div>
<mu-button icon slot="right">
<mu-icon value="expand_more"></mu-icon>
</mu-button>
</mu-appbar>
Then recompile the front-end resources:
npm run dev
In this way, you will not report an error when you visit the avatar upload page again.
Back to resources/js/pages/Avatar.vue
upload picture files to the back-end logic defined in the postAvatar()
method follows the code (click on the “Save Avatar” button will execute, and upload and crop operations are pure front-end behavior):
let files = new window.File([data], this.name, {type: this.type});
const formdata = new window.FormData();
formdata.append('file', files);
formdata.append('username', getItem('userid'));
const res = await this.$store.dispatch('uploadAvatar', formdata);
Front and rear ends, and many other interactive logic as previously defined, here we will form fields username
adjusted api_token
to backend API interface through which automatic authentication:
formdata.append('api_token', this.auth_token);
Accordingly, the need to add attributes in the calculation auth_token
and reads the corresponding localStorage from the token
field values:
computed: {
...mapState({
userid: state => state.userInfo.userid,
src: state => state.userInfo.src,
auth_token: state => state.userInfo.token
})
}
Then, based on Vuex defined uploadAvatar
action initiates a request for back-end interface to upload picture, the corresponding interface calls defined resources/js/api/server.js
in:
// Upload Avatar
postUploadAvatar: data => Axios.post('/file/avatar', data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}),
This interface has not yet been implemented in the backend. Next, we will complete this interface at the Laravel backend.
In the routes/api.php
new route file/avatar
:
Route::middleware('auth:api')->group(function () {
...
Route::post('/file/avatar', 'FileController@avatar');
}
Then FileController
write controllers avatar
ways:
public function avatar(Request $request)
{
if (!$request->hasFile('file') || !$request->file('file')->isValid()) {
return response()->json([
'errno' => 500,
'msg' => 'Invalid parameters (avatar picture is empty or invalid)'
]);
}
$image = $request->file('file');
$time = time();
$filename = md5($time . mt_rand(0, 10000)) . '.' . $image->extension();
$path = $image->storeAs('images/avatars/' . date('Y/m/d', $time), $filename, ['disk' => 'public']);
if ($path) {
// Save user avatar information to database
$user = auth('api')->user();
$user->avatar = Storage::disk('public')->url($path);
$user->save();
return response()->json([
'errno' => 0,
'data' => [
'url' => $path
],
'msg' => 'Saved successfully'
]);
} else {
return response()->json([
'errno' => 500,
'msg' => 'File upload failed, please try again'
]);
}
}
And a tutorial written message picture upload interfaces are similar, but here is to save the avatar to the images/avatars
directory, and then save the full path information to the user table avatar
field, thus completing the user avatar updates.
Recompile the front-end resources and restart the Swoole HTTP server for the front-end and back-end code changes to take effect.
Finally, let’s test the avatar upload function, forcibly refresh the avatar upload page, upload a new user avatar, and then save the avatar to call the upload avatar interface. After the save is successful, the following prompt box will pop up:
After clicking OK, the page will jump to the user’s personal center page and display the new user avatar:
At this point, the user avatar upload function is complete.
In this way, we have completed all the front-end page interaction functions of the chat room project. Next, we will spend two or three tutorials to optimize the code of the back-end Websocket server to complete the development of the Laravel + Swoole chat room project. Go to the next Laravel journey.
#laravel #swoole #vue
1581478260
Building a Live Online Chat Room Based on Laravel + Swoole + Vue (P6): Establish a Connection Between the Socket.io Client and The Swoole Websocket server
After completing the development environment, setting up the back-end Websocket server, and initializing the front-end resources, we then officially started debugging the front-end and back-end interfaces to complete the development of the online chat room function.
The first thing we need to do is to establish a Websocket connection and communication between the client and the server. Here, our Websocket client uses socket.io-client, and the server uses the WebSocket server provided by the Swoole-based LaravelS extension package . socket.io has its own set of connection establishment and data encoding mechanisms, so you must adjust the original Websocket server implementation, otherwise you cannot establish a WebSocket connection.
The LaravelS extension pack is not friendly to the Socket.io client, but another popular Laravel Swoole extension pack Laravel-Swoole has good support for it, and it can even be said that it is a PHP server adaptation of the socket.io client For details, you can refer to its official documentation , so it is natural that we can port this part of its implementation to LaravelS.
The code of this project has been submitted to the Github code repository: https://github.com/nonfu/webchat , you can download the code from here for comparison.
The data format sent and received by the socket.io client has its own rules. We need to implement the corresponding data parser on the server according to this rule, in order to decode the received client data before processing the data, and then process it. After that, encode the data before sending it to the client so that the client can parse it correctly.
This part of the logic directly Laravel-Swoole expansion pack copy, first app/services
create the directory WebSocket
subdirectories for storing WebSocket server-related code, and then the previously created WebSocketHandler
handler class to move, at the same time do not forget to change config/laravels.php
the profile of WebSocketHandler
the path:
'websocket' => [
'enable' => true,
'handler' => \App\Services\WebSocket\WebSocketHandler::class,
],
Next, start writing data parser, in app/Services/WebSocket
creating an abstract base class directory Parser
initialization code as follows:
<?php
/**
* Data codec abstract base class
*/
namespace App\Services\WebSocket;
use Illuminate\Support\Facades\App;
abstract class Parser
{
/**
* Strategy classes need to implement handle method.
*/
protected $strategies = [];
/**
* Execute strategies before decoding payload.
* If return value is true will skip decoding.
*
* @param \Swoole\WebSocket\Server $server
* @param \Swoole\WebSocket\Frame $frame
*
* @return boolean
*/
public function execute($server, $frame)
{
$skip = false;
foreach ($this->strategies as $strategy) {
$result = App::call(
$strategy . '@handle',
[
'server' => $server,
'frame' => $frame,
]
);
if ($result === true) {
$skip = true;
break;
}
}
return $skip;
}
/**
* Encode output payload for websocket push.
*
* @param string $event
* @param mixed $data
*
* @return mixed
*/
abstract public function encode(string $event, $data);
/**
* Input message on websocket connected.
* Define and return event name and payload data here.
*
* @param \Swoole\Websocket\Frame $frame
*
* @return array
*/
abstract public function decode($frame);
}
Then WebSocket
create a subdirectory under the directory SocketIO
used to store the relevant code to interact with the client socket.io. The first is the data parser corresponding to the socket.io client SocketIOParser
. This class inherits from Parser
:
<?php
/**
* Socket.io corresponding data codec
*/
namespace App\Services\WebSocket\SocketIO;
use App\Services\WebSocket\Parser;
use App\Services\WebSocket\SocketIO\Strategies\HeartbeatStrategy;
class SocketIOParser extends Parser
{
/**
* Strategy classes need to implement handle method.
*/
protected $strategies = [
HeartbeatStrategy::class,
];
/**
* Encode output payload for websocket push.
*
* @param string $event
* @param mixed $data
*
* @return mixed
*/
public function encode(string $event, $data)
{
$packet = Packet::MESSAGE . Packet::EVENT;
$shouldEncode = is_array($data) || is_object($data);
$data = $shouldEncode ? json_encode($data) : $data;
$format = $shouldEncode ? '["%s",%s]' : '["%s","%s"]';
return $packet . sprintf($format, $event, $data);
}
/**
* Decode message from websocket client.
* Define and return payload here.
*
* @param \Swoole\Websocket\Frame $frame
*
* @return array
*/
public function decode($frame)
{
$payload = Packet::getPayload($frame->data);
return [
'event' => $payload['event'] ?? null,
'data' => $payload['data'] ?? null,
];
}
}
This is used inside the package Packet
type communication data analysis processing:
<?php
/**
* Socket.io communication data parsing underlying class
*/
namespace App\Services\WebSocket\SocketIO;
class Packet
{
/**
* Socket.io packet type `open`.
*/
const OPEN = 0;
/**
* Socket.io packet type `close`.
*/
const CLOSE = 1;
/**
* Socket.io packet type `ping`.
*/
const PING = 2;
/**
* Socket.io packet type `pong`.
*/
const PONG = 3;
/**
* Socket.io packet type `message`.
*/
const MESSAGE = 4;
/**
* Socket.io packet type 'upgrade'
*/
const UPGRADE = 5;
/**
* Socket.io packet type `noop`.
*/
const NOOP = 6;
/**
* Engine.io packet type `connect`.
*/
const CONNECT = 0;
/**
* Engine.io packet type `disconnect`.
*/
const DISCONNECT = 1;
/**
* Engine.io packet type `event`.
*/
const EVENT = 2;
/**
* Engine.io packet type `ack`.
*/
const ACK = 3;
/**
* Engine.io packet type `error`.
*/
const ERROR = 4;
/**
* Engine.io packet type 'binary event'
*/
const BINARY_EVENT = 5;
/**
* Engine.io packet type `binary ack`. For acks with binary arguments.
*/
const BINARY_ACK = 6;
/**
* Socket.io packet types.
*/
public static $socketTypes = [
0 => 'OPEN',
1 => 'CLOSE',
2 => 'PING',
3 => 'PONG',
4 => 'MESSAGE',
5 => 'UPGRADE',
6 => 'NOOP',
];
/**
* Engine.io packet types.
*/
public static $engineTypes = [
0 => 'CONNECT',
1 => 'DISCONNECT',
2 => 'EVENT',
3 => 'ACK',
4 => 'ERROR',
5 => 'BINARY_EVENT',
6 => 'BINARY_ACK',
];
/**
* Get socket packet type of a raw payload.
*
* @param string $packet
*
* @return int|null
*/
public static function getSocketType(string $packet)
{
$type = $packet[0] ?? null;
if (! array_key_exists($type, static::$socketTypes)) {
return null;
}
return (int) $type;
}
/**
* Get data packet from a raw payload.
*
* @param string $packet
*
* @return array|null
*/
public static function getPayload(string $packet)
{
$packet = trim($packet);
$start = strpos($packet, '[');
if ($start === false || substr($packet, -1) !== ']') {
return null;
}
$data = substr($packet, $start, strlen($packet) - $start);
$data = json_decode($data, true);
if (is_null($data)) {
return null;
}
return [
'event' => $data[0],
'data' => $data[1] ?? null,
];
}
/**
* Return if a socket packet belongs to specific type.
*
* @param $packet
* @param string $typeName
*
* @return bool
*/
public static function isSocketType($packet, string $typeName)
{
$type = array_search(strtoupper($typeName), static::$socketTypes);
if ($type === false) {
return false;
}
return static::getSocketType($packet) === $type;
}
}
In addition SocketIOParser
also introduced strategy for processing the heartbeat connection, so-called heartbeat connection refers to the connection length in order to maintain a connection for communication at predetermined time intervals, which typically are not required for communication processing can be ignored, and there is such a made.
Heartbeat connection policy classes are held in SocketIO/Strategies
the directory:
<?php
/**
* Heartbeat connection processing strategy class
*/
namespace App\Services\WebSocket\SocketIO\Strategies;
use App\Services\WebSocket\SocketIO\Packet;
class HeartbeatStrategy
{
/**
* If return value is true will skip decoding.
*
* @param \Swoole\WebSocket\Server $server
* @param \Swoole\WebSocket\Frame $frame
*
* @return boolean
*/
public function handle($server, $frame)
{
$packet = $frame->data;
$packetLength = strlen($packet);
$payload = '';
if (Packet::getPayload($packet)) {
return false;
}
if ($isPing = Packet::isSocketType($packet, 'ping')) {
$payload .= Packet::PONG;
}
if ($isPing && $packetLength > 1) {
$payload .= substr($packet, 1, $packetLength - 1);
}
if ($isPing) {
$server->push($frame->fd, $payload);
}
return true;
}
}
At this point, our communication data parser is all completed. Next, let’s look at the encapsulated communication data sending class.
Pusher
The reconstructed WebSocketHandler
class will bear only routing and controller functions, to business logic related services will be peeled off to complete a separate service unit, comprising sending data, because we need to be uniform encapsulation processing, so that the customer can be End resolution. In the app/Services/WebSocket
creation directory Pusher
class for data transmission:
<?php
/**
* Communication data transmission class
*/
namespace App\Services\WebSocket;
class Pusher
{
/**
* @var \Swoole\Websocket\Server
*/
protected $server;
/**
* @var int
*/
protected $opcode;
/**
* @var int
*/
protected $sender;
/**
* @var array
*/
protected $descriptors;
/**
* @var bool
*/
protected $broadcast;
/**
* @var bool
*/
protected $assigned;
/**
* @var string
*/
protected $event;
/**
* @var mixed|null
*/
protected $message;
/**
* Push constructor.
*
* @param int $opcode
* @param int $sender
* @param array $descriptors
* @param bool $broadcast
* @param bool $assigned
* @param string $event
* @param mixed|null $message
* @param \Swoole\Websocket\Server
*/
protected function __construct(
int $opcode,
int $sender,
array $descriptors,
bool $broadcast,
bool $assigned,
string $event,
$message = null,
$server
)
{
$this->opcode = $opcode;
$this->sender = $sender;
$this->descriptors = $descriptors;
$this->broadcast = $broadcast;
$this->assigned = $assigned;
$this->event = $event;
$this->message = $message;
$this->server = $server;
}
/**
* Static constructor
*
* @param array $data
* @param \Swoole\Websocket\Server $server
*
* @return Pusher
*/
public static function make(array $data, $server)
{
return new static(
$data['opcode'] ?? 1,
$data['sender'] ?? 0,
$data['fds'] ?? [],
$data['broadcast'] ?? false,
$data['assigned'] ?? false,
$data['event'] ?? null,
$data['message'] ?? null,
$server
);
}
/**
* @return int
*/
public function getOpcode(): int
{
return $this->opcode;
}
/**
* @return int
*/
public function getSender(): int
{
return $this->sender;
}
/**
* @return array
*/
public function getDescriptors(): array
{
return $this->descriptors;
}
/**
* @param int $descriptor
*
* @return self
*/
public function addDescriptor($descriptor): self
{
return $this->addDescriptors([$descriptor]);
}
/**
* @param array $descriptors
*
* @return self
*/
public function addDescriptors(array $descriptors): self
{
$this->descriptors = array_values(
array_unique(
array_merge($this->descriptors, $descriptors)
)
);
return $this;
}
/**
* @param int $descriptor
*
* @return bool
*/
public function hasDescriptor(int $descriptor): bool
{
return in_array($descriptor, $this->descriptors);
}
/**
* @return bool
*/
public function isBroadcast(): bool
{
return $this->broadcast;
}
/**
* @return bool
*/
public function isAssigned(): bool
{
return $this->assigned;
}
/**
* @return string
*/
public function getEvent(): string
{
return $this->event;
}
/**
* @return mixed|null
*/
public function getMessage()
{
return $this->message;
}
/**
* @return \Swoole\Websocket\Server
*/
public function getServer()
{
return $this->server;
}
/**
* @return bool
*/
public function shouldBroadcast(): bool
{
return $this->broadcast && empty($this->descriptors) && ! $this->assigned;
}
/**
* Returns all descriptors that are websocket
*
* @param \Swoole\Connection\Iterator $descriptors
*
* @return array
*/
protected function getWebsocketConnections(): array
{
return array_filter(iterator_to_array($this->server->connections), function ($fd) {
return $this->server->isEstablished($fd);
});
}
/**
* @param int $fd
*
* @return bool
*/
public function shouldPushToDescriptor(int $fd): bool
{
if (! $this->server->isEstablished($fd)) {
return false;
}
return $this->broadcast ? $this->sender !== (int) $fd : true;
}
/**
* Push message to related descriptors
*
* @param mixed $payload
*
* @return void
*/
public function push($payload): void
{
// attach sender if not broadcast
if (! $this->broadcast && $this->sender && ! $this->hasDescriptor($this->sender)) {
$this->addDescriptor($this->sender);
}
// check if to broadcast to other clients
if ($this->shouldBroadcast()) {
$this->addDescriptors($this->getWebsocketConnections());
}
// push message to designated fds
foreach ($this->descriptors as $descriptor) {
if ($this->shouldPushToDescriptor($descriptor)) {
$this->server->push($descriptor, $payload, $this->opcode);
}
}
}
}
This class is mainly used for business logic processing sent to the client after data processing, including data parsing and unified encapsulation, whether to broadcast, etc.
WebSocket
service classIn addition to simple data receiving and sending, our online chat room has many other complex functions, so it is necessary to create a separate service class to implement these functions, such as room join and exit, user authentication and acquisition, data transmission and broadcasting, where the final will call Pusher
the class to send data, it can be said that the service class is the core of the WebSocket back-end services, but this tutorial to simplify this section, just copy over an empty placeholder class, more Multifunctionality will be gradually added in subsequent tutorials:
<?php
namespace App\Services\WebSocket;
class WebSocket
{
const PUSH_ACTION = 'push';
const EVENT_CONNECT = 'connect';
const USER_PREFIX = 'uid_';
/**
* Determine if to broadcast.
*
* @var boolean
*/
protected $isBroadcast = false;
/**
* Scoket sender's fd.
*
* @var integer
*/
protected $sender;
/**
* Recepient's fd or room name.
*
* @var array
*/
protected $to = [];
/**
* Websocket event callbacks.
*
* @var array
*/
protected $callbacks = [];
}
In this tutorial, in order to simplify the process, we WebSocketHandler
directly call the Pusher
class to send data to the client to quickly demonstrate establish WebSocket communication connection.
WebSocketHandler
processorsFinally, we re-implement the code according to the new structure of WebSocketHandler
the processor onOpen
and onMessage
method:
<?php
/**
* WebSocket service communication processor class
* Author: College Jun
*/
namespace App\Services\WebSocket;
use App\Services\WebSocket\SocketIO\SocketIOParser;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Illuminate\Support\Facades\Log;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
class WebSocketHandler implements WebSocketHandlerInterface
{
/**
* @var WebSocket
*/
protected $websocket;
/**
* @var Parser
*/
protected $parser;
public function __construct()
{
$this->websocket = app(WebSocket::class);
$this->parser = app(SocketIOParser::class);
}
// Triggered when the connection is established
public function onOpen(Server $server, Request $request)
{
if (!request()->input('sid')) {
// Initialize the connection information to adapt socket.io-client, this code cannot be omitted, otherwise the connection cannot be established
$payload = json_encode([
'sid' => base64_encode(uniqid()),
'upgrades' => [],
'pingInterval' => config('laravels.swoole.heartbeat_idle_time') * 1000,
'pingTimeout' => config('laravels.swoole.heartbeat_check_interval') * 1000,
]);
$initPayload = Packet::OPEN . $payload;
$connectPayload = Packet::MESSAGE . Packet::CONNECT;
$server->push($request->fd, $initPayload);
$server->push($request->fd, $connectPayload);
}
Log::info('WebSocket Connection establishment:' . $request->fd);
$payload = [
'sender' => $request->fd,
'fds' => [$request->fd],
'broadcast' => false,
'assigned' => false,
'event' => 'message',
'message' => 'Welcome to chat room',
];
$pusher = Pusher::make($payload, $server);
$pusher->push($this->parser->encode($pusher->getEvent(), $pusher->getMessage()));
}
// Triggered when a message is received
public function onMessage(Server $server, Frame $frame)
{
// $frame->fd Is the client id,$frame->data Is the data sent by the client
Log::info("From {$frame->fd} Received data: {$frame->data}");
if ($this->parser->execute($server, $frame)) {
// Skip heartbeat connection processing
return;
}
$payload = $this->parser->decode($frame);
['event' => $event, 'data' => $data] = $payload;
$payload = [
'sender' => $frame->fd,
'fds' => [$frame->fd],
'broadcast' => false,
'assigned' => false,
'event' => $event,
'message' => $data,
];
$pusher = Pusher::make($payload, $server);
$pusher->push($this->parser->encode($pusher->getEvent(), $pusher->getMessage()));
}
// Triggered when the connection is closed
public function onClose(Server $server, $fd, $reactorId)
{
Log::info('WebSocket Connection closed:' . $fd);
}
}
First initialized in the constructor $websocket
and $parser
attributes, then the connection establishment callback method onOpen
, it is determined request data contains sid
fields not included the need to send connection initiation information to the client in order to successfully establish WebSocket connection, here we also specify the heartbeat interval time and time-out, the appropriate, we also need to profile config/laravels.php
the swoole
new configuration the following two items:
'heartbeat_idle_time' => 600,
'heartbeat_check_interval' => 60,
Then call the Pusher
class push
method to send a welcome message encoded in order to be resolved socket.io end customers.
In the method of the callback message is received onMessage
, the first call the Parser
class execute
method determines whether the heartbeat connection, if the heartbeat connection is skipped if not treated, otherwise decoding the received information, after a simple treatment, and then through Pusher
the class push
method is sent back to the client.
At this point, the Swoole WebSocket server connection establishment and simple communication logic adapted to the socket.io client have been initially implemented. Next, we need to restart the Swoole WebSocket server for the code to take effect:
bin/laravels restart
Finally, we revise down socket.io client connection code, open resources/js/socket.js
modify the connection establishment code is as follows:
import io from 'socket.io-client';
const socket = io('http://webchats.test', {
path: '/ws',
transports: ['websocket']
});
export default socket;
Here we set the server address to be driven by the Swoole HTTP server http://webchats.test
, then set the path /ws
to connect to the Swoole WebSocket server, and finally set the transport layer protocol to websocket
replace the default long polling(polling
) mechanism.
In addition Vue front-end components and view files and JavaScript files main entrance app.js
also made some minor adjustments, the corresponding code to the code repository https://github.com/nonfu/webchat latest submitted version shall prevail, as in app.js
, we printed Log of whether the Socket connection is established:
socket.on('connect', async () => {
console.log('websocket connected: ' + socket.connected);
...
Next, we run npm run dev
recompiled resources, access the browser http://webchats.test
, you can see through F12 WebSocket connection establishment and communication of the data.
In the console Consoletab
you can also see the following logs:
websocket connected: true
Indicates that the connection was established successfully.
#laravel #swoole #vue