websocket 导致大量apache进程_Swoole 服务端主动向websocket推送消息

7092c096dce3d9c7c1df20b3f3034a52.png

在之前的博文中,我们已经学完了如果使用swoole搭建websocket长连接,也学会了swoole的多进程数据共享操作。

但在一个完整的websocket长连接日常操作链中,服务端往往会主动给在线的用户单独推送消息,会群发一些消息。

在Swoole-websocket中给我们提供了一个onRequest事件,该事件用于监听外部请求。

也就是我们可以通过http请求向websocket中调取数据,进而发送消息。

onRequest的示例代码如下:

官网文档是:https://wiki.swoole.com/wiki/page/397.html

 $this->_ws->on('request', function ($request, $response) {
    //var_dump($request);
    # 如果你是get的,就改成get,可以用dump看看$request
    $param = $request->post;
    $data  = [];
    $data['code'] = 3;
    $data['user_nice'] = '系统通知';
    $data['content'] = $param['content'];
    # 下面我们来广播消息
    if (empty($param['user_id'])) {
        # 群发
        $this->broadcast($this->_ws, $this->json($data));
        # 返回消息
        $this->endRequest('200', '发送成功', $request, $response);
    } else {
        # 单发
        if (empty($this->_ws->user[$param['user_id']]['fd'])) {
            # 返回消息
            $this->endRequest('500', '客户不存在', $request, $response);
        } else {
            $user = $this->_ws->user[$param['user_id']];
            if ($user['status'] == 0) {
                # 返回消息
                $this->endRequest('500', '客户已下线', $request, $response);
            } else {
                $this->_ws->push($user['fd'], $this->json($data));
                # 返回消息
                $this->endRequest('200', '发送成功', $request, $response);
            }
        }
    }
});
其中最重要的是endRequest这个方法的代码,我们接着往下看:

/**
 * request事件返回值
*/
private function endRequest($code, $msg, $request, $response) {
    $json = [
        'code' => "$code",
        'msg'  => "$msg",
    ];
    # 输出响应
    $return = json_encode($json, JSON_UNESCAPED_UNICODE);
    # 需要end事件,否注会报500错误,并无结果返回
    # 不知道为啥,CLI模式下这个事件一次请求会有2次监听,但发现最后一次其中的server->request_uri会有个/favicon.ico参数
    # 所以凭借这个参数,我们可以做判断,放弃掉第一次监听返回
    # 还有,如果我们直接在onRequest中过滤掉第一次监听,那第二次监听就不会执行,也会报500错误
    # 所以我们只能在返回的时候做下手脚
    //if($request->server['request_uri'] == '/favicon.ico') {
        $response->end($return);
    //}
    # 而且我发现经过这样处理,onRequest事件那边也只会有一次请求了,特别奇怪。
    # 而且这样返回之后,浏览器直接请求还是报500错误。
    # 熟悉Swoole的朋友可以在下方留言,指教下我的疑惑。
}

从上面的注释中我们可以看出,endRequest的输出值很奇怪,它支持CLI模式下运行,但该模式下的会有2次endRequest监听,需要使用server->request_uri/favicon.ico参数进行拦截输出返回值,否注将报错。

而通过CURL或浏览器发包的方式则不能拦截,同时这种请求方式只有1次endRequest监听,所以不能拦截返回值。

同时需要注意,endRequest同步输出返回值,不能直接使用echo,而是需要把返回内容放在$response->end()中。

下面我们来看看完整的server.php端代码:

<?php
// +----------------------------------------------------------------------
// 小黄牛blog - Swoole 即时通讯交互处理
// +----------------------------------------------------------------------
// Copyright (c) 2018 https://xiuxian.junphp.com All rights reserved.
// +----------------------------------------------------------------------
// Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// Author: 小黄牛 <1731223728@qq.com>
// +----------------------------------------------------------------------
class Server{
    /**
     * 客户端身份存储器
    */
    private $_table = []; 
    /**
     * WS的启动实例
    */
    private $_ws;
    /**
     * host-IP,0.0.0.0表示允许接收所有请求
    */
    private $_host = '0.0.0.0';
    /**
     * 端口号
    */
    private $_port = '9502';
    /**
     * 最大服务端心跳重连次数
    */
    private $_max  = 3;
    /**
     * 强制心跳重连启动状态
    */
    private $_status = true;
    /**
     * 这是启动服务端的入口
    */
    public function run() { 
        $this->start_service(); 
        $this->start_table(); 
        $this->request();
        $this->start_handshake();
        $this->start_message();
        $this->end();
    }
    /**
     * ①启动websocker服务
    */
    private function start_service() {
        # 创建websocket服务器对象,监听0.0.0.0:9502端口
        $this->_ws = new swoole_websocket_server($this->_host, $this->_port);
        $this->_ws->set([
            'worker_num' => 4,// 开4个工作进程
        ]);
    }
    /**
     * ①创建Table服务
    */
    private function start_table() {
        # 创建最大只能存储1024个用户的数据
        $this->_table = new swoole_table(1024);
        # 创建字段
        $this->_table->column('fd', swoole_table::TYPE_INT, 8); // FD
        $this->_table->column('status', swoole_table::TYPE_INT, 8); // 离线状态
        $this->_table->column('heartbeat', swoole_table::TYPE_INT, 8); // 心跳重连数
        $this->_table->column('user_id', swoole_table::TYPE_STRING, 32); // 会员ID
        $this->_table->column('user_nice', swoole_table::TYPE_STRING, 32); // 会员名称
        $this->_table->create();
        # 将表附加到ws实例里,方便后续使用
        $this->_ws->user = $this->_table;
    }
    /**
     * ②监听WebSocket握手申请
    */
    private function start_handshake() {
        # 监听WebSocket连接打开事件
        $this->_ws->on('open', function ($ws, $request){
            # 这里可以做些鉴权验证之类的
        });
    }
    /**
     * ③监听客户端消息发送请求
    */
    private function start_message() {
        # 监听WebSocket消息事件
        $this->_ws->on('message', function ($ws, $frame) {
            $data    = json_decode($frame->data, true);
            $user_id = $data['user_id'];
            # 加入存储器
            $this->_ws->user->set($user_id, [
                'fd'        => $frame->fd, # FD
                'status'    => 1, # 设置上线状态
                'heartbeat' => 0, # 重置心跳重连数
                'user_id' => $data['user_id'], # 用户ID
                'user_nice' => $data['user_nice'], # 用户昵称
            ]);
            # 登录广播处理
            if ($data['code'] == 1) {
                # 发送广播上线消息
                $data['content'] = '【'.$data['user_nice'].'】骑着小黄牛上线啦~!';
                $this->broadcast($ws, $this->json($data), $user_id);
            # 心跳重连检测
            } else if ($data['code'] == 4) {
                $this->broadcast($ws, $frame->data, $user_id);
                $this->timer();
            # 其他请求
            } else {
                # 广播消息
                $this->broadcast($ws, $frame->data, $user_id);
            }
        });
    } 
    /**
     * ④监听客户端退出事件
    */
    private function end() {
        # 这里加入了unset,清除open存储器,防止存储器无限增大
        # 监听WebSocket连接关闭事件
        $this->_ws->on('close', function ($ws, $fd) {
            $user = null;
            foreach ($this->_ws->user as $k=>$v) {
                if ($v['fd'] == $fd) {
                    $user = $v;
                }
            }
            # 如果没用用户就跳过
            if (!$user) {
                return false;
            }
            # 获取用户ID
            $user_id = $user['user_id'];
            # 获取用户nice
            $user_nice = $user['user_nice'];
            # 设置离线状态
            $this->_ws->user->set($user_id, [
                'status'    => 0, # 设置离线状态
            ]);
            $data = [
                'code' => 2,
                'user_id' => $user_id,
                'user_nice' => $user_nice,
                'content' => '【'.$user_nice.'】骑着小扫帚灰溜溜的走了~~!'
            ];
            # 广播消息
            $this->broadcast($ws, $this->json($data));
        });
        $this->_ws->start();
    }
    /**
     * ④监听外部请求推送事件
    */
    private function request() {
        $this->_ws->on('request', function ($request, $response) {
            //var_dump($request);
            # 如果你是get的,就改成get,可以用dump看看$request
            $param = $request->post;
            $data  = [];
            $data['code'] = 3;
            $data['user_nice'] = '系统通知';
            $data['content'] = $param['content'];
            # 下面我们来广播消息
            if (empty($param['user_id'])) {
                # 群发
                $this->broadcast($this->_ws, $this->json($data));
                # 返回消息
                $this->endRequest('200', '发送成功', $request, $response);
            } else {
                # 单发
                if (empty($this->_ws->user[$param['user_id']]['fd'])) {
                    # 返回消息
                    $this->endRequest('500', '客户不存在', $request, $response);
                } else {
                    $user = $this->_ws->user[$param['user_id']];
                    if ($user['status'] == 0) {
                        # 返回消息
                        $this->endRequest('500', '客户已下线', $request, $response);
                    } else {
                        $this->_ws->push($user['fd'], $this->json($data));
                        # 返回消息
                        $this->endRequest('200', '发送成功', $request, $response);
                    }
                }
            }
        });
    }
    /**
     * request事件返回值
    */
    private function endRequest($code, $msg, $request, $response) {
        $json = [
            'code' => "$code",
            'msg'  => "$msg",
        ];
        # 输出响应
        $return = json_encode($json, JSON_UNESCAPED_UNICODE);
        # 需要end事件,否注会报500错误,并无结果返回
        # 不知道为啥,CLI模式下这个事件一次请求会有2次监听,但发现最后一次其中的server->request_uri会有个/favicon.ico参数
        # 所以凭借这个参数,我们可以做判断,放弃掉第一次监听返回
        # 还有,如果我们直接在onRequest中过滤掉第一次监听,那第二次监听就不会执行,也会报500错误
        # 所以我们只能在返回的时候做下手脚
        //if($request->server['request_uri'] == '/favicon.ico') {
            $response->end($return);
        //}
        # 而且我发现经过这样处理,onRequest事件那边也只会有一次请求了,特别奇怪。
        # 而且这样返回之后,浏览器直接请求还是报500错误。
        # 熟悉Swoole的朋友可以在下方留言,指教下我的疑惑。
    }
    /**
     * 广播消息
     * @todo 无
     * @author 小黄牛
     * @version v1.0.0.1 + 2018.11.12
     * @deprecated 暂不弃用
     * @global 无
     * @param object $wx 实例
     * @param string $content 广播内容
     * @param string $id 用户的userid
     *  @param bool $status 是否做心跳限制 
     * @return void
    */
    private function broadcast($ws, $content, $id=null, $status=false) {
        # 向所有人广播
        foreach ($this->_ws->user as $k=>$v) {
            # 不向自己广播,并且要在线的
            # 注意,这里一定要有上线状态的限制,否则假设用户已经退出,但你的进程还开着,实际上已经关闭,这时候push就会报错
            # 只有正常在线的用户才能接收到广播
            # 加入心跳检测限制
            if ($k != $id && $v['status'] == 1 && $status == true) {
                $ws->push($v['fd'], $content);
            } else if ($v['user_id'] != $id && $v['status'] == 1 && $v['heartbeat'] == 0) {
                $ws->push($v['fd'], $content);
            }
        }
    }
    /**
     * 数组转json
     * @todo 无
     * @author 小黄牛
     * @version v1.0.0.1 + 2018.11.08
     * @deprecated 暂不弃用
     * @global 无
     * @param array $array 数组
     * @return json
    */
    private function json($array) {
        return json_encode($array, JSON_UNESCAPED_UNICODE);
    }
    /**
     * 服务端定时强制心跳检测
     * @todo 无
     * @author 小黄牛
     * @version v1.0.0.1 + 2018.11.08
     * @deprecated 暂不弃用
     * @global 无
     * @return void
    */
    private function timer() {
        # 注意强制心跳触发器不能放在open事件里,因为那时候用户还没有提交登录请求,是还没有userID的
        # 还有,强制心跳定时器只能触发一次,否则会出现生成多个定时器的情况
        if ($this->_status) {
            $this->_status = false;
            /**
             * ⑤服务端强制心跳检测
             * 每隔1分钟发送1次,如果连续3次强制心跳检测未通过,服务端将强制断开连接
            */
            $obj = $this;
            swoole_timer_tick(60000, function ($timer_id) use (&$obj) {
                # 广播消息
                $obj->broadcast($obj->_ws, $obj->json(['code' => 5]), null, true);
                # 所有人的心跳次数+1
                foreach ($this->_ws->user as $k=>$v) {
                    if (empty($v['heartbeat'])) {
                        # 重置心跳次数
                        $this->_ws->user->set($v['user_id'], [
                            'heartbeat' => 0,
                        ]);
                    }
                    # 心跳次数累加
                    $this->_ws->user->set($v['user_id'], [
                        'heartbeat' => $v['heartbeat']+1
                    ]);
                    # 心跳次数大于等于_max && 在线的 的连接关闭
                    if ($v['heartbeat'] >= $obj->_max && $v['status'] == 1) {
                        $data = $v;
                        # 发送强制掉线广播
                        $data['code'] = 6;
                        $data['content'] = '【'.$data['user_nice'].'】已被服务端强制下线!';
                        $obj->broadcast($obj->_ws, $obj->json($data), null, true);
                        # 这里不需要unset连接,因为在close事件中,已经将这个连接设置为离线了
                        # 主动关闭连接k
                        $obj->_ws->close($v['fd']);
                    }
                }
            });
        }
    }
}
$socketServer = new Server();
$socketServer->run();

然后再看如何给websocket主动推送的client.php端代码:

<?php
// +----------------------------------------------------------------------
// 小黄牛blog - websocket - http发包给TCP
// +----------------------------------------------------------------------
// Copyright (c) 2018 https://xiuxian.junphp.com All rights reserved.
// +----------------------------------------------------------------------
// Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// Author: 小黄牛 <1731223728@qq.com>
// +----------------------------------------------------------------------
function https_request($url, $data = null){
    # 初始化一个cURL会话
    $curl = curl_init();  
    //设置请求选项, 包括具体的url
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);  //禁用后cURL将终止从服务端进行验证
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
    if (!empty($data)){
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);  //设置具体的post数据
    }
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);        
    $response = curl_exec($curl);  //执行一个cURL会话并且获取相关回复
    //$httpCode = curl_getinfo($curl,CURLINFO_HTTP_CODE); 
    //echo $httpCode;
    curl_close($curl);  //释放cURL句柄,关闭一个cURL会话
    return $response;
}
var_dump(https_request('http://IP:端口', [
    'user_id' => '用户ID',// 为空群发
    'content'=> '测试内容'
]));

我们只需要通过访问client.php,就能给指定用户推送消息拉。

同时我们需要注意,在真正开发中,我们还需要对onRequest事件的请求进行加密跟鉴权处理,否注很容易被竞争对手恶意攻击。

而且服务端还可以通过onRequest事件拉取到所有的在线用户消息,更多相关的功能都可自行扩展。


最后推荐大家可以用下我开源的一个基于Swoole4.5+研发的PHP框架。该框架基于注解实现了很多好玩的功能,很适合新人快速上手Swoole扩展。

SW-X框架-专注高性能便捷开发而生的PHP-SwooleX框架​www.sw-x.cn
604f8e745dacdb12498ada925ea17262.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值