php实现通知的实时推送

作者: 白云飞 分类: php,workerman,网络知识 发布时间: 2019-12-17 23:31 阅读:

其实这个内容,我在就搞过,只是没记录过。正好最近,项目中遇到了,记录一下,方便有同样需求的小伙伴,也参考下。先说一下我的需求,我们一个呼叫系统,电话接通之后,呼叫体系统会推送一个通知到我指定的url上,而我要通知对应的客服人员,有电话接通了,并弹出这个电话关联的人员的详细信息。首先,我们来分解一下整个的流程

没找到合适的流程图工具,画的不是很好,将就着看。其实这个需求主要的点,在于第4部,如何实时的通知到指定的客服那里。大家都知道 http协议是无状态的,服务器在没有跟客户端建立连接的情况下,是无法主动发消息给客户端的。而第3步,呼叫系统给我客服系统发送的消息是直接通过http协议直接发送到我的服务器,这是如果客服人员的浏览器不与我的服务器建立长连接,那么这个闭环将无法形成。到此,其实就和好解决了。建立长连接,推广吗。那就是 websocket呗。

正好php方面有这方面开源的实现。我们直接拿 workerman 的https://github.com/walkor/web-msg-sender/ 为什么采用workerman 而不是swoole呢,这是因为,我公司的电脑是 windows10的,不好运行 swoole。我也不想用docker去搞,觉得费事。workerman可以运行,也就 50个左右的长连接,直接采用workerman的方案来的快。

web-msg-sender 是基于 phpsocket.io 来实现的。话不多说,上神器

composer require workerman/phpsocket.io

拉下来,phpsocket.io 之后,直接拿 web-msg-sender 的例子改改就好了。服务端我们这么搞:

<?php
use Workerman\Worker;
use PHPSocketIO\SocketIO;

include __DIR__ . '/vendor/autoload.php';

// 全局数组保存uid在线数据
$uidConnectionMap = array();

// PHPSocketIO服务
$sender_io = new SocketIO(2120);
// 客户端发起连接事件时,设置连接socket的各种事件回调
$sender_io->on('connection', function($socket){
    // 当客户端发来登录事件时触发
    $socket->on('login', function ($uid)use($socket){
        global $uidConnectionMap;
        // 已经登录过了
        if(isset($socket->uid)){
            return;
        }
        
        // 更新对应uid的在线数据
        $uid = (string)$uid;
        if(!isset($uidConnectionMap[$uid])) {
            $uidConnectionMap[$uid] = 0;
        }
        
        // 将这个连接加入到uid分组,方便针对uid推送数据
        $socket->join($uid);
        $socket->uid = $uid;
    });

    // 当客户端断开连接是触发(一般是关闭网页或者跳转刷新导致)
    $socket->on('disconnect', function () use($socket) {
        if(!isset($socket->uid)) {
            return;
        }
        global $uidConnectionMap;
        
        unset($uidConnectionMap[$socket->uid]);
    });
});
// 当$sender_io启动后监听一个http端口,通过这个端口可以给任意uid或者所有uid推送数据
$sender_io->on('workerStart', function(){
    // 监听一个http端口
    $inner_http_worker = new Worker('http://0.0.0.0:2121');
    // 当http客户端发来数据时触发
    $inner_http_worker->onMessage = function($http_connection, $data){
        global $uidConnectionMap;
        $_POST = $_POST ? $_POST : $_GET;
        // 推送数据的url格式 type=publish&to=uid&content=xxxx
        switch(@$_POST['type']){
            case 'publish':
                global $sender_io;
                $to = @$_POST['to'];
                $_POST['content'] = htmlspecialchars(@$_POST['content']);
                // 有指定uid则向uid所在socket组发送数据
                if($to){
                    $sender_io->to($to)->emit('new_msg', $_POST['content']);
                    // 否则向所有uid推送数据
                }else{
                    $sender_io->emit('new_msg', @$_POST['content']);
                }
                // http接口返回,如果用户离线socket返回fail
                if($to && !isset($uidConnectionMap[$to])){
                    return $http_connection->send('offline');
                }else{
                    return $http_connection->send('ok');
                }
        }
        return $http_connection->send('fail');
    };
    // 执行监听
    $inner_http_worker->listen();
});

if(!defined('GLOBAL_START')) {
    Worker::runAll();
}

这里我并不进行数量的通知和广播,因此,我直接将demo中的这部分例子直接删除了。我来大致讲讲这部分代码是如何运行的吧

$sender_io = new SocketIO(2120);

首先创建了一个 socketio server并监厅了 2120端口,因此这是我们通过客户端js,编写如下的代码:

<script src='https://cdn.bootcss.com/socket.io/2.0.3/socket.io.js'></script>
<script>
// 如果服务端不在本机,请把127.0.0.1改成服务端ip
var socket = io('http://127.0.0.1:2120');
// 当连接服务端成功时触发connect默认事件
socket.on('connect', function(){
    console.log('connect success');
});
</script>

此时客户端便可以与服务端建立长连接了。那服务端是如何区分客户端,并能一对一通知呢?

$sender_io->on('connection', function($socket){
    // 当客户端发来登录事件时触发
    $socket->on('login', function ($uid)use($socket){
        global $uidConnectionMap;
        // 已经登录过了
        if(isset($socket->uid)){
            return;
        }

        // 更新对应uid的在线数据
        $uid = (string)$uid;
        if(!isset($uidConnectionMap[$uid])) {
            $uidConnectionMap[$uid] = 0;
        }

        // 将这个连接加入到uid分组,方便针对uid推送数据
        $socket->join($uid);
        $socket->uid = $uid;
    });

    // 当客户端断开连接是触发(一般是关闭网页或者跳转刷新导致)
    $socket->on('disconnect', function () use($socket) {
        if(!isset($socket->uid)) {
            return;
        }
        global $uidConnectionMap;

        unset($uidConnectionMap[$socket->uid]);
    });
});

这一段能看出,socket.io 服务器,收到客户端 connection 连接事件,会监厅 “login” 消息并将接收到的 消息体中发送的uid与socket标识做绑定。这样每一次收到消息,从socket的属性中就可以拿到客户端的uid,方便一对一推送。为了建立这个绑定关系,客户端的js,应该这么改

socket.on('connect', function(){
    console.log('connect success');
    socket->emit("login", 123); // 123是客服的id
});

此时,这个uid 123就会跟他的唯一连接句柄socket,建立绑定关系了。那么接下来,收到消息如何推送呢?

$sender_io->on('workerStart', function(){
    // 监听一个http端口
    $inner_http_worker = new Worker('http://0.0.0.0:2121');
    // 当http客户端发来数据时触发
    $inner_http_worker->onMessage = function($http_connection, $data){
        global $uidConnectionMap;
        $_POST = $_POST ? $_POST : $_GET;
        // 推送数据的url格式 type=publish&to=uid&content=xxxx
        switch(@$_POST['type']){
            case 'publish':
                global $sender_io;
                $to = @$_POST['to'];
                $_POST['content'] = htmlspecialchars(@$_POST['content']);
                // 有指定uid则向uid所在socket组发送数据
                if($to){
                    $sender_io->to($to)->emit('new_msg', $_POST['content']);
                    // 否则向所有uid推送数据
                }else{
                    $sender_io->emit('new_msg', @$_POST['content']);
                }
                // http接口返回,如果用户离线socket返回fail
                if($to && !isset($uidConnectionMap[$to])){
                    return $http_connection->send('offline');
                }else{
                    return $http_connection->send('ok');
                }
        }
        return $http_connection->send('fail');
    };
    // 执行监听
    $inner_http_worker->listen();
});

我们在“workerStart” 这个事件回调中,启动了一个内部的 http 服务器,并且以 2121端口对外服务,此时如果我们通过 http协议想这台服务器的 2121端口发送消息,便会触发 onMessage 回调,此时分析 消息体的 type 和 to ,就可以通过 $sender_io 向对应的 websocket建立的长连接客户端发送消息了。我们只要在 js端加入如下的监听,便可以获得消息做后续的逻辑

socket.on('new_msg', function(msg){
    console.log('get message:' + msg + ' from server');
});

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!