在很多時候我們需要做這種聊天室的時候需要實時響應,所以今天介紹一下workerman,如果沒有聽過workerman的小夥伴,傳送門:workerman文件 ,官網
本篇文章是整合laravel 5.8
的,使用artisan
命令去管理workerman
。
如果覺得workerman
不好搞,也可以直接使用GatewayWorker
,傳送門:GatewayWorker手冊 , 在 Laravel 中使用 GatewayWorker 進行 socket 通訊
第一步 安裝workerman
在專案根目錄執行
composer require workerman/workerman
第二部 建立命令類,自定義啟停workerman
在專案根目錄執行
php artisan make:command Workerman
執行該命令會在app/Console/Commands/
下面建立Workerman.php
檔案,該檔案程式碼如下:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Workerman\Autoloader;
use Workerman\Worker;
class Workerman extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'Workerman {action} {--daemonize}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'workerman 啟動停止';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
global $argv;//定義全域性變數
$arg = $this->argument('action');
$argv[1] = $arg;
$argv[2] = $this->option('daemonize') ? '-d' : '';//該引數是以daemon(守護程式)方式啟動
global $text_worker;
// 建立一個Worker監聽2345埠,使用websocket協議通訊
$text_worker = new Worker("websocket://0.0.0.0:2345");
$text_worker->uidConnections = array();//線上使用者連線物件
$text_worker->uidInfo = array();//線上使用者的使用者資訊
// 啟動4個程式對外提供服務
$text_worker->count = 4;
//引用類檔案
$handler = \App::make('Handler\WorkermanHandler');
$text_worker->onConnect = array($handler,"handle_connection");
$text_worker->onMessage = array($handler,"handle_message");
$text_worker->onClose = array($handler,"handle_close");
$text_worker->onWorkerStart = array($handler,"handle_start");
// 執行worker
Worker::runAll();
}
}
注意:action 是啟動引數,daemonize選項是是否後臺執行,如果不需要後臺執行請把有關daemonize的程式碼刪掉。
最最重要的地方來了$argv一定不能刪掉,否則啟動workerman的時候會報錯
然後手動建立app/Handler/WorkermanHandler.php
檔案,程式碼如下:
<?php
namespace Handler;
use Illuminate\Console\Command;
use Workerman\Lib\Timer;
use Workerman\Worker;
class WorkermanHandler
{
private $heartbeat_time = 55;//心跳間隔55秒
//當客戶端連上來時分配uid,並儲存連結,並通知所有客戶端
public function handle_connection($connection){
global $text_worker;
//判斷是否設定了UID
if(!isset($connection->uid)){
//給使用者分配一個UID
$connection->uid = random_string(20);
//儲存使用者的uid
$text_worker->uidConnections["{$connection->uid}"] = $connection;
//向使用者返回建立成功的資訊
$connection->send("使用者:[{$connection->uid}] 建立成功");
}
}
public function handle_start(){
global $text_worker;
//每秒都判斷客戶端是否已下線
Timer::add(1, function()use($text_worker){
$time_now = time();
foreach($text_worker->connections as $connection) {
// 有可能該connection還沒收到過訊息,則lastMessageTime設定為當前時間
if (empty($connection->lastMessageTime)) {
$connection->lastMessageTime = $time_now;
continue;
}
// 上次通訊時間間隔大於心跳間隔,則認為客戶端已經下線,關閉連線
if ($time_now - $connection->lastMessageTime > $this->heartbeat_time) {
$connection->close();
}
}
});
//每隔30秒就向客戶端傳送一條心跳驗證
Timer::add(50,function ()use ($text_worker){
foreach ($text_worker->connections as $conn){
$conn->send('{"type":"ping"}');
}
});
}
//當客戶端傳送訊息過來時,轉發給所有人
public function handle_message($connection,$data){
global $text_worker;
//debug
//echo "data_info:".$data.PHP_EOL;
$connection->lastMessageTime = time();
$data_info=json_decode($data,true);
if(!$data_info){
return ;
}
//判斷業務型別
switch($data_info['type'])
{
case 'login':
//判斷使用者資訊是否存在
if(empty($data_info['user_id'])){
$connection->send("{'type':'error','msg':'非法請求'}");
return $connection->close();
}
//判斷使用者是否已經登入了
$user_ids=array_column($text_worker->uidInfo,"user_id");
if(in_array($data_info['user_id'],$user_ids)){
$connection->send("{'type':'error','msg':'你在其它地方已登入'}");
return $connection->close();
}
//儲存使用者資訊
$text_worker->uidInfo["{$connection->uid}"]=array(
"user_id"=>$data_info['user_id'],
"user_name"=>htmlspecialchars($data_info['user_name']),
"user_header"=>$data_info['user_header'],
"create_time"=>date("Y-m-d H:i"),
);
//返回資料
if($data_info['to_uid'] == "all"){
$return_data=array(
"type"=>"login",
"uid"=>$connection->uid,
"user_name"=>htmlspecialchars($data_info['user_name']),
"user_header"=>$data_info['user_header'],
"send_time"=>date("Y-m-d H:i",time()),
"user_lists"=>$text_worker->uidInfo
);
$curral_data=array(
"type"=>"login_uid",
"uid"=>$connection->uid,
);
$connection->send(json_encode($curral_data));
//給所有使用者傳送一條資料
foreach($text_worker->connections as $conn){
$conn->send(json_encode($return_data));
}
}else{
return ;
}
return;
//使用者發訊息
case 'say':
if(!isset($text_worker->uidInfo["{$connection->uid}"]) || empty($text_worker->uidInfo["{$connection->uid}"])){
$connection->send('{"type":"error","msg":"你已經掉線了"}');
}
//獲取到當前使用者的資訊
$user_info=$text_worker->uidInfo["{$connection->uid}"];
//判斷是私聊還是群聊
if($data_info['to_uid'] != "all"){
//私聊
$return_data=array(
"type"=>"say",
"from_uid"=>$connection->uid,
"from_user_name"=>$user_info['user_name'],
"from_user_header"=>$user_info['user_header'],
"to_uid"=>$data_info['to_uid'],
"content"=>nl2br(htmlspecialchars($data_info['content'])),
"send_time"=>date("Y-m-d H:i")
);
if($data_info['to_uid'] == $connection->uid){
$connection->send(json_encode($return_data));
return;
}
//判斷使用者是否存在,並向對方傳送資料
if(isset($text_worker->uidConnections["{$data_info['to_uid']}"])){
$to_connection=$text_worker->uidConnections["{$data_info['to_uid']}"];
$to_connection->send(json_encode($return_data));
}
//向你自己傳送一條資料
$connection->send(json_encode($return_data));
}else{
//群聊
$return_data=array(
"type"=>"say",
"from_uid"=>$connection->uid,
"from_user_name"=>$user_info['user_name'],
"from_user_header"=>$user_info['user_header'],
"to_uid"=>"all",
"content"=>nl2br(htmlspecialchars($data_info['content'])),
"send_time"=>date("Y-m-d H:i")
);
//向所有使用者傳送資料
foreach($text_worker->connections as $conn){
$conn->send(json_encode($return_data));
}
}
return;
case "pong":
return;
}
}
//當客戶端斷開時,廣播給所有客戶端
public function handle_close($connection){
global $text_worker;
$user_name=$text_worker->uidInfo[$connection->uid]['user_name'] ?? "";
unset($text_worker->uidConnections["{$connection->uid}"]);
unset($text_worker->uidInfo["{$connection->uid}"]);
if(!empty($user_name)){
$return_data=array(
"type"=>"logout",
"uid"=>$connection->uid,
"user_name"=>$user_name,
"create_time"=>date("Y-m-d H:i:s"),
);
foreach($text_worker->connections as $conn){
$conn->send(json_encode($return_data));
}
}
}
}
該程式碼是我這邊自己的程式碼,有些東西沒有貼出來,只做參考,比如怎麼實現維持心跳和分配使用者唯一id等。
開啟composer.json
檔案增加一段"app/Handler"
於classmap中,下面是我的部分內容:
...
"autoload": {
"classmap": [
"database/seeds",
"database/factories",
"app/Handler"
],
"psr-4": {
"App\\": "app/"
}
},
...
執行命令: composer dump-autoload
執行命令啟動workerman
php artisan Workerman start --daemonize
再說一遍:app\Console\Commands\Workerman.php 裡的程式碼 $arg = $this->argument('action'); $argv [1] = $arg; 如果這段程式碼不寫那麼就無法啟動服務會報Usage: php yourfile.php {start|stop|restart|reload|status|connections} [-d]
原因: 參考接收位置錯誤. 程式碼位置: \vendor\workerman\workerman\Worker.php 673 684, 問題位置 673行中, $argv 這個全域性變數取的key的位置錯亂的.
如果你是debug模式啟動workerman的話,就把app\Console\Commands\Workerman.php 裡的 關於daemonize的程式碼刪掉,啟動命令就是php artisan Workerman start
參考資料
可以參觀一下 我的部落格
本作品採用《CC 協議》,轉載必須註明作者和本文連結
我的部落格:www.zhangkaixing.com