Laravel整合PHPSocket.Io實現web消息推送

PHPSocket.IO,PHP跨平臺實時通訊框架
PHPSocket.IO是PHP版本的Socket.IO服務端實現,基於workerman開發,用於替換node.js版本Socket.IO服務端。PHPSocket.IO底層採用websocket協議通訊,如果客戶端不支持websocket協議, 則會自動採用http長輪詢的方式通訊。

環境

  • Ubuntu 18
  • Laravel 5.8
  • PHPSocket.IO 1.1

安裝依賴

composer require workerman/phpsocket.io
composer require guzzlehttp/guzzle

啓動程序整合到artisan命令中

創建文件命令php artisan make:command MsgPush

app/Console/Commands/MsgPush.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Workerman\Worker;
use Workerman\Lib\Timer;
use PHPSocketIO\SocketIO;

class MsgPush extends Command
{
    protected $signature = 'msg-push
    {action=start : start | restart | reload(平滑重啓) | stop | status | connetions}
    {--d : deamon or debug}';
    
    protected $description = 'web消息推送服務';
    
    // 全局數組保存uid在線數據
    private static $uidConnectionCounter = [];
    // 廣播的在線用戶數,一個uid代表一個用戶
    private static $onlineCount = 0;
    // 廣播的在線頁面數,同一個uid可能開啓多個頁面
    private static $onlinePageCount = 0;
    //PHPSocketIO服務
    private static $senderIo = null;
    

    public function __construct()
    {
        parent::__construct();
    }
    
    /**
     * 根據腳本參數開啓PHPSocketIO服務
     * PHPSocketIO服務的端口是`2120`
     * 傳遞數據的端口是`2121`
     */
    public function handle()
    {
        global $argv;
        //啓動php腳本所需的命令行參數
        $argv[0] = 'MsgPush';
        $argv[1] = $this->argument('action'); // start | restart | reload(平滑重啓) | stop | status | connetions
        $argv[2] = $this->option('d') ? '-d' : ''; // 守護進程模式或調試模式啓動
        
        // PHPSocketIO服務
        self::$senderIo = new SocketIO(2120);
        
        // 客戶端發起連接事件時,設置連接socket的各種事件回調
        self::$senderIo->on('connection', function ($socket) {
            
            // 當客戶端發來登錄事件時觸發,$uid目前由頁面傳值決定,當然也可以根據業務需要由服務端來決定
            $socket->on('login', function ($uid) use ($socket) {
                // 已經登錄過了
                if (isset($socket->uid)) return;
                
                // 更新對應uid的在線數據
                $uid = (string)$uid;
                // 這個uid有self::$uidConnectionCounter[$uid]個socket連接
                self::$uidConnectionCounter[$uid] = isset(self::$uidConnectionCounter[$uid]) ? self::$uidConnectionCounter[$uid] + 1 : 1;
                
                // 將這個連接加入到uid分組,方便針對uid推送數據
                $socket->join($uid);
                $socket->uid = $uid;
                // 更新這個socket對應頁面的在線數據
                self::emitOnlineCount();
            });
            
            // 當客戶端斷開連接是觸發(一般是關閉網頁或者跳轉刷新導致)
            $socket->on('disconnect', function () use ($socket) {
                if (!isset($socket->uid)) {
                    return;
                }
                
                // 將uid的在線socket數減一
                if (--self::$uidConnectionCounter[$socket->uid] <= 0) {
                    unset(self::$uidConnectionCounter[$socket->uid]);
                }
            });
            
        });
        
        // 當self::$senderIo啓動後監聽一個http端口,通過這個端口可以給任意uid或者所有uid推送數據
        self::$senderIo->on('workerStart', function () {
            // 監聽一個http端口
            $innerHttpWorker = new Worker('http://0.0.0.0:2121');
            // 當http客戶端發來數據時觸發
            $innerHttpWorker->onMessage = function ($httpConnection, $data) {
                
                $type = $_REQUEST['type'] ?? '';
                $content = htmlspecialchars($_REQUEST['content'] ?? '');
                $to = (string)($_REQUEST['to'] ?? '');
                
                // 推送數據的url格式 type=publish&to=uid&content=xxxx
                switch ($type) {
                    case 'publish':
                        // 有指定uid則向uid所在socket組發送數據
                        if ($to) {
                            self::$senderIo->to($to)->emit('new_msg', $content);
                        } else {
                            // 否則向所有uid推送數據
                            self::$senderIo->emit('new_msg', $content);
                        }
                        // http接口返回,如果用戶離線socket返回fail
                        if ($to && !isset(self::$uidConnectionCounter[$to])) {
                            return $httpConnection->send('offline');
                        } else {
                            return $httpConnection->send('ok');
                        }
                }
                return $httpConnection->send('fail');
            };
            // 執行監聽
            $innerHttpWorker->listen();
            
            // 一個定時器,定時向所有uid推送當前uid在線數及在線頁面數
            Timer::add(1, [self::class, 'emitOnlineCount']);
        });

//        Worker::$daemonize = true;
        Worker::runAll();
    }
    
    /**
     * 將在線數變化推送給所有登錄端
     * 須是public方法,可供其它類調用
     */
    public static function emitOnlineCount()
    {
        $newOnlineCount = count(self::$uidConnectionCounter);
        $newOnlinePageCount = array_sum(self::$uidConnectionCounter);
        
        // 只有在客戶端在線數變化了才廣播,減少不必要的客戶端通訊
        if ($newOnlineCount != self::$onlineCount || $newOnlinePageCount != self::$onlinePageCount) {
//            var_dump('emitOnlineCount: ', self::$uidConnectionCounter);
            //將在線數變化推送給所有登錄端
            self::$senderIo->emit(
                'update_online_count',
                [
                    'onlineCount' => $newOnlineCount,
                    'onlinePageCount' => $newOnlinePageCount
                ]
            );
            self::$onlineCount = $newOnlineCount;
            self::$onlinePageCount = $newOnlinePageCount;
        }
    }
}

啓動PHPSocket.Io服務

#守護進程模式啓動
php artisan msg-push start -d
#調式模式啓動
php artisan msg-push start

web頁面

resources/views/socketio.blade.php

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
    <title>laravel整合phpSocketIo</title>
</head>
<body>
<h1>laravel整合phpSocketIo</h1>
<h2>實現laravel服務端推送消息到web端</h2>
<h5>效果查看console</h5>


<script src='https://cdn.bootcss.com/socket.io/2.0.3/socket.io.js'></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
  const uid = Date.now(), //這個識別id可以換成項目相應業務的id,同一個id可以多端登錄,能同時收到消息
      domain = document.domain, //當前打開頁面的域名或ip
      sendToOneApi = `http://${domain}:2121/?type=publish&content=msg_content&to=${uid}`,
      sendToAllApi = `http://${domain}:2121/?type=publish&content=msg_content`,
      socket = io(`http://${domain}:2120`); // 連接socket服務端

  console.log('給指定uid登錄端發送消息接口: ', sendToOneApi); //支持get和post方法
  console.log('給所有登錄端發送消息接口: ', sendToAllApi);

  // 連接後登錄
  socket.on('connect', function () {
    socket.emit('login', uid);
  });

  // 後端推送來消息時
  socket.on('new_msg', function (msg) {
    console.log('收到消息: ' + msg);
  });

  // 後端推送來在線數據時
  socket.on('update_online_count', function (online_stat) {
    console.log('即時在線數據: ', online_stat);
  });

});
</script>
</body>
</html>

web頁面路由

routes/web.php

Route::get('/socketio', function () {
    return view('socketio');
});

laravel內以觸發事件方式推送消息

app/Providers/EventServiceProvider.php

//定義事件
//App/Providers/EventServiceProvider
public function boot()
    {
        parent::boot();
        
        //推送消息到web端,這個閉包只能傳入一個參數
        Event::listen('send-msg', function (object $data) {
//            dump($data);
            $response = (new \GuzzleHttp\Client())->post('http://127.0.0.1:2121', [
                'form_params' => [
                    'content' => $data->content,
                    'to' => $data->to ?? '',
                    'type' => $data->type ?? 'publish',
                ],
            ]);
        
            return (string)$response->getBody();
        });
    }

瀏覽器方式測試推送

地址欄輸入http://${domain}:2121/?type=publish&content=Are_you_ok推送給全體成員,${domain}是你實際的ip或域名

tinker方式測試推送

#進入tinker
php artisan tinker
#推送給全體
event('send-msg',(object)['content'=>'hello'])
#推送給個體,`to`改成你的實際值
event('send-msg',(object)['content'=>'hello','to'=>1556645595484])

通過以上操作即可在php服務端向web端推送消息啦,解鎖新功能是不是有點小興奮呢?
感謝推動着時代進步的巨人們,是你們讓我等看到了更多的可能!

參考

workerman手冊
PHPSocket.IO跨平臺實時通訊框架簡介

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章