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
#终止
php artisan msg-push stop
#平滑重启
php artisan msg-push reload
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跨平台实时通讯框架简介
感谢推动着时代进步的巨人们,是你们让我等看到了更多的可能!