websocket receive方法内 有循环怎么退出_Swoole学习笔记七:搭建WebSocket长连接 之 使用 USER_ID 作为身份凭证...

a0b56b7b80776bcd3cc483bd538f1fc1.png

0、前言

前面基本的WebSocket操作,我们基本都已经掌握了,接下来我们要学习的是怎么用user_id去关联一个fd凭证呢?
按我们的思路应该是在data存储器中,将fd替换成user_id,但这样在server的close事件中,就需要用遍历的方式去读取关闭的对应连接了。
这样费时的操作,肯定不是我们想要的。
实际上,我们只需要新增一个open连接存储器记录fd即可,然后在code=1的时候用user_id更新到原来的data存储器中即可。

1、注意事项

① 客户端全部使用user_id作为身份凭证
② 服务端新建一个open存储器
③ 服务端在code=1时,在从open存储器中读取fd凭证写入data存储器中。
④ 服务端在close事件中记得清除已经关闭的open存储器凭证,防止存储器无限增大。

2、客户端代码修改:

<?php
// +----------------------------------------------------------------------
// 小黄牛blog - websocket
// +----------------------------------------------------------------------
// Copyright (c) 2018 https://xiuxian.junphp.com All rights reserved.
// +----------------------------------------------------------------------
// Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// Author: 小黄牛 <1731223728@qq.com>
// +----------------------------------------------------------------------
session_start();
# 模拟用户登录
if (!empty($_POST['nice'])) {
    $data = [
        'nice' => $_POST['nice'],
        'id'   => uniqid(),
    ];
    $_SESSION['user'] = $data;
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
# 模拟用于退出登录
} else if (!empty($_POST['out'])) {
    $_SESSION['user'] = '';
}
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Swoole+Websocket案例 - 小黄牛</title>
<style>
html,body{margin:0;padding:0;font-size:13px}
.left{width: 20%;height: 600px;border: 1px solid #ddd;float: left;}
.right{width: 59.7%;height: 400px;border: 1px solid #ddd;border-left: 0px;float: left;overflow: auto;}
.bottom{width: 79.7%;height: 199px;border: 1px solid #ddd;border-left: 0px;border-top: 0px;float: left;}
#content{width: 99.5%;height: 165px;}
.blue{color:blue}
.red{color:red}
.div_left{width:100%;float:left}
.div_right{width:100%;float:left;text-align: right;}
.div_centent{width:100%;float:left;text-align: center;}
#USER{width:100%;height: 40px;line-height: 40px;border-bottom: 1px solid #ddd;float:left}
#error{width:20%;height:400px;float: left;overflow: auto;}
</style>
<link rel="stylesheet" type="text/css" href="./css/user.css" />
<script src="./js/jquery.min.js"></script>
<script language="javascript" src="./js/jquery.easing.min.js"></script>
<script language="javascript" src="./js/custom.js"></script>
</head>
<body>
<!--登录导航-->
<div id="header">
    <div class="common">
        <div class="login fr">
            <ul>
                <li class="openlogin"><a href="" onclick="return false;">登录</a></li>
                <li class="reg" style="display:none"><a href="" onclick="return false;">退出</a></li>
            </ul>
        </div>
        <div class="clear"></div>
    </div>
</div>
<!--模拟登录弹窗-->
<div class="loginmask"></div>
<div id="loginalert">
    <div class="pd20 loginpd">
        <h3>
            <i class="closealert fr"></i>
            <div class="clear"></div>
        </h3>
        <div class="loginwrap">
            <div class="loginh">
                <div class="fl">模拟会员登录</div>
                <div class="clear"></div>
            </div>
            <div class="clear"></div>
            <div class="logininput">
                <input type="text" class="loginusername" placeholder="随便输入一个昵称" />
            </div>
            <div class="clear"></div>
            <div class="loginbtn">
                <div class="loginsubmit fl">
                    <input type="button" id="login_form" value="登录" />
                    <div class="loginsubmiting">
                        <div class="loginsubmiting_inner"></div>
                    </div>
                </div>
            </div>
        </div>
        </div>
    </div>
</div>
<!--以下为聊天室窗口-->
<div id="USER"></div>
<div class="left">
    <ul>
        <li>用户列表:</li>
    </ul>
</div>
<div class="right"></div>
<div id="error"></div>
<div class="bottom">
    <textarea id="content"></textarea>
    <button type="button" id="submit">发送消息</button>
</div>
<h3>使用方法:</h3>
<p>①:CD进您的server.php文件目录</p>
<p>②:如果您是调试阶段,可以直接php server.php,激活程序,这样的话在运行过程中出错,能在cmd界面查看报错内容</p>
<p>③:如果您是部署阶段,可以使用nohup server.php >>/dev/null 2>&1 &命令,后台守护进程运行。</p>
</body>
</html>
<script>
var USER_ID = '';
var USER_NICE = '';
var lockReconnect = false; // 正常情况下我们是关闭心跳重连的
var wsServer = 'ws://47.106.187.208:9502';
var websocket;
var time;
<?php
if (isset($_SESSION['user'])) {
    $user = $_SESSION['user'];
    echo 'USER_ID = "'.$user['id'].'";';
    echo 'USER_NICE = "'.$user['nice'].'";';
    echo 'createWebSocket();';
    echo "$('.openlogin').hide();";
    echo "$('.login .reg').show();";
    echo "$('#USER').html('您的USER_ID为:".$user['id']." 昵称为:".$user['nice']."');";
}
?>
// ①开启WebSocket
function createWebSocket() {
    try {
        websocket = new WebSocket(wsServer);
        init();
    } catch(e) {
        reconnect(wsUrl);
    }
}
// ②初始化WebSocket,并设置心跳检测
function init() {
    // 接收Socket断开时的消息通知
    websocket.onclose = function(evt) {
        $('#error').append('<p class="red">Socket断开了...正在试图重新连接...</p>');
        reconnect(wsServer);
    };
    // 接收Socket连接失败时的异常通知
    websocket.onerror = function(e){
        $('#error').append('<p class="red">Socket断开了...正在试图重新连接...</p>');
        reconnect(wsServer);
    };
    // 连接成功
    websocket.onopen = function (evt) {
        $('#error').append('<p class="blue">握手成功,打开socket连接了。。。</p>');
        var data = {
            'code':1, // 我们假设code为1时,是绑定登录请求
            'user_id':USER_ID,
            'user_nice':USER_NICE
        };
        // 前端发送json前,必须先转义成字符串
        data = JSON.stringify(data);
        console.log(data);
        websocket.send(data);
        // 心跳检测重置
        heartCheck.start();
    };
    var message = '';
    var flag    = true;
    // 接收服务端广播的消息通知
    websocket.onmessage = function(evt){
        heartCheck.start();
        var obj = JSON.parse(evt.data); 
        // 不存在,添加用户列表
        if ($("#"+obj.user_id).length>0) {}else if (obj.user_id != undefined && obj.user_id != null && obj.user_id != ''){
            $('.left ul').append('<li id="'+obj.user_id+'">'+obj.user_nice+' <span class="blue">(在线)</span></li>');
        }
        // 登录广播
        if (obj.code == 1) {
            // 存在修改上线状态
            if ($("#"+obj.user_id).length>0) {
                $("#"+obj.user_id+' span').removeClass('red');
                $("#"+obj.user_id+' span').addClass('blue');
                $("#"+obj.user_id+' span').html('在线');
            }
            $('.right').append('<div class="div_centent">'+obj.content+'</div>');
        // 下线广播 或 服务端强制下线广播
        } else if (obj.code == 2 || obj.code == 6) {
            // 存在修改下线状态
            if ($("#"+obj.user_id).length>0) {
                $("#"+obj.user_id+' span').removeClass('blue');
                $("#"+obj.user_id+' span').addClass('red');
                $("#"+obj.user_id+' span').html('离线');
                $('.right').append('<div class="div_centent">'+obj.content+'</div>');
            // 不存在,添加用户列表
            } else {
                $('.left ul').append('<li id="'+obj.user_id+'">'+obj.user_nice+' <span class="reg">(离线)</span></li>');
            }
        // 聊天消息广播
        } else if (obj.code == 3) {
            $('.right').append('<div class="div_left">'+obj.user_nice+':'+obj.content+'</div>');
            // 聊天界面默认自动底部
            $('.right').scrollTop( $('.right')[0].scrollHeight );
        // 如果是心跳检测的广播就不做任何操作
        } else if (obj.code == 4){
            return false;
        // 检测是否后端发起了强制心跳检测,是则发送一次心跳检测
        } else if (obj.code == 5) {
            $('#error').append('<p class="red">服务端发起了一次强制心跳检测...</p>');
            var data = {
                'code':4, // 我们假设code为4时,既为心跳检测
                'user_id':USER_ID,
                'user_nice':USER_NICE
            };
            // 前端发送json前,必须先转义成字符串
            data = JSON.stringify(data);
            websocket.send(data);
        }
    };
}
// ③ 掉线重连
function reconnect(url) {
    if(lockReconnect) {
        return;
    };
    lockReconnect = true;
    // 没连接上会一直重连,设置心跳延迟避免请求过多
    time && clearTimeout(time);
    time = setTimeout(function () {
        createWebSocket(url);
        lockReconnect = false;
    }, 5000);
}
// ④心跳检测
var heartCheck = {
    timeout: 5000,
    timeoutObj: null,
    serverTimeoutObj: null,
    start: function() {
        var self = this;
        this.timeoutObj && clearTimeout(this.timeoutObj);
        this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
        this.timeoutObj = setTimeout(function(){
            // 这里发送一个心跳,后端收到后,返回一个心跳消息,
            // onmessage拿到返回的心跳就说明连接正常
            var data = {
                'code':4, // 我们假设code为4时,既为心跳检测
                'user_id':USER_ID,
                'user_nice':USER_NICE
            };
            // 前端发送json前,必须先转义成字符串
            data = JSON.stringify(data);
            websocket.send(data);
        }, this.timeout)
    }
}
// 点击发送消息按钮
$('#submit').click(function(){
    if (USER_ID == '') {
        $('.openlogin').click();
        return;
    }
    var content = $('#content').val();
    $('.right').append('<div class="div_right">'+content+':'+USER_NICE+'</div>');
    var data = {
        'code':3, // 我们假设code为3时,既为聊天消息广播请求
        'user_id':USER_ID,
        'user_nice':USER_NICE,
        'content':content
    };
    // 前端发送json前,必须先转义成字符串
    data = JSON.stringify(data);
    websocket.send(data);
    // 输入表单清空
    $('#content').val('');
    // 聊天界面默认自动底部
    $('.right').scrollTop( $('.right')[0].scrollHeight );
});
// 点击发送模拟登录请求
$('#login_form').click(function(){
    var nice    = $('.loginusername').val();
    $.ajax({
        type: 'post',
        data:{'nice':nice},
        url: "",
        success: function(data) {
            var obj = JSON.parse(data);
            $('.openlogin').hide();
            $('.login .reg').show();
            $('#USER').html('您的USER_ID为:'+obj.id+' 昵称为:'+obj.nice);
            $('.closealert').click();
            USER_ID = obj.id;
            USER_NICE = obj.nice;
            createWebSocket();
        }
    });
});
// 点击退出登录请求
$('.login .reg a').click(function(){
    $.ajax({
        type: 'post',
        data:{'out':1},
        url: "",
        success: function(data) {
            $('.openlogin').show();
            $('.login .reg').hide();
            $('#USER').html('');
            $("#"+USER_ID+' span').removeClass('blue');
            $("#"+USER_ID+' span').addClass('red');
            $("#"+USER_ID+' span').html('离线');
            USER_ID = '';
            USER_NICE = '';
            websocket.close();
        }
    });
});
</script>

3、服务端代码修改:

<?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{
    /**
     * open打开连接存储器
    */
    private $_open = []; 
    /**
     * 客户端身份存储器
    */
    private $_data = []; 
    /**
     * 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_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);
    }
    /**
     * ②监听WebSocket握手申请
    */
    private function start_handshake() {
        # 监听WebSocket连接打开事件
        $this->_ws->on('open', function ($ws, $request){
            # 加入open存储器
            $this->_open[$request->fd]['fd']      = $request->fd;
            # 默认离线状态
            $this->_open[$request->fd]['status']  = 0;
            # 默认心跳重连数
            $this->_open[$request->fd]['heartbeat']  = 0;
        });
    }
    /**
     * ③监听客户端消息发送请求
    */
    private function start_message() {
        # 监听WebSocket消息事件
        $this->_ws->on('message', function ($ws, $frame) {
            $data    = json_decode($frame->data, true);
            $user_id = $data['user_id'];
            # 默认心跳重连数
            $this->_open[$frame->fd]['heartbeat'] = 0;
            $this->_data[$user_id]['heartbeat']   = 0;
            # 登录广播处理
            if ($data['code'] == 1) {
                # 先更新open存储器里的信息
                $this->_open[$frame->fd]['fd']        = $frame->fd;
                # 修改fd对应的身份
                $this->_open[$frame->fd]['user_id']   = $data['user_id'];
                # 设置昵称
                $this->_open[$frame->fd]['user_nice'] = $data['user_nice'];
                # 设置上线状态
                $this->_open[$frame->fd]['status']    = 1;
                # 再读取open存储器更新连接信息
                $this->_data[$user_id] = $this->_open[$frame->fd];
                # 发送广播上线消息
                $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) {
            # 不知道为啥这里高并发下会偶尔存在为空的情况
            if (empty($this->_open[$fd]['user_id']) || empty($this->_open[$fd]['user_nice'])) {
                unset($this->_open[$fd]);
                return false;
            }
            # 获取用户ID
            $user_id = $this->_open[$fd]['user_id'];
            # 获取用户nice
            $user_nice = $this->_open[$fd]['user_nice'];
            # 设置离线状态
            $this->_data[$user_id]['status']  = 0;
            $data = [
                'code' => 2,
                'user_id' => $user_id,
                'user_nice' => $user_nice,
                'content' => '【'.$user_nice.'】骑着小扫帚灰溜溜的走了~~!'
            ];
            # 广播消息
            $this->broadcast($ws, $this->json($data));
            unset($this->_open[$fd]);
        });
        $this->_ws->start();
    }
    /**
     * 广播消息
     * @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->_data as $k=>$v) {
            # 不向自己广播,并且要在线的
            # 注意,这里一定要有上线状态的限制,否则假设用户已经退出,但你的进程还开着,实际上已经关闭,这时候push就会报错
            # 只有正常在线的用户才能接收到广播
            # 加入心跳检测限制
            if ($k != $id && $v['status'] == 1 && $status == true) {
                $ws->push($v['fd'], $content);
            } else if ($k != $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 ($obj->_data as $k=>$v) {
                    if (empty($obj->_data[$k]['heartbeat'])) {
                        $obj->_data[$k]['heartbeat'] = 0;
                    }
                    $obj->_data[$k]['heartbeat'] = $v['heartbeat']+1;
                    # 心跳次数大于等于_max && 在线的 的连接关闭
                    if ($v['heartbeat'] >= $obj->_max && $v['status'] == 1) {
                        if (empty($v['fd'])) {
                            continue;
                        }
                        $data = $v;
                        # 发送强制掉线广播
                        $data['code'] = 6;
                        $data['content'] = '【'.$data['user_nice'].'】已被服务端强制下线!';
                        $obj->broadcast($obj->_ws, $obj->json($data), null, true);
                        # 这里不需要unset连接,因为在close事件中,已经将这个连接设置为离线了
                        # 主动关闭连接
                        $obj->_ws->close($v['fd']);
                    }
                }
            });
        }
    }
}
$socketServer = new Server();
$socketServer->run();

4、一些思维的延伸

基于USER_ID作为身份凭证后,我们可以实现更多的功能,例如聊天室的分组聊天,只需要在身份标记中加多一个元素,记录该用户加入的群组即可。然后提交消息广播时就把群组ID带上,只广播给该群组下的在线成员。
也可以实现单个私聊,消息广播的时候把对方的USER_ID带上,然后服务端编写一个单个消息广播方法,同时利用数据库查询两人是否为好友。

完整的案例DEMO,可以直接到我的开源栏目中进行下载:Swoole聊天室Demo之四


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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值