swoole在websocket上的应用实战

一、SWOOLE介绍
详见SWOOLE.COM

二、SWOOLE扩展的编译&安装
详见SWOOLE安装

三、SWOOLE SERVER

1、端口监听

//监听端口,0.0.0.0表示允许所有IP来源,9502默认端口
$server = new \swoole_websocket_server('0.0.0.0'), 9502);

2、通信鉴权,通过OPEN时,HEADER里携带的参数socket-id和socket-signature,也可以自己维护一套验证机制,鉴权失败CLOSE掉客户端

$server->on('open', function (\swoole_websocket_server $server, $request) {
$header = $request->header;
$uId = $header['socket-id'] ?? '';
$uSignature = $header['socket-signature'] ?? '';
if (empty($uId) || empty($uSignature)) {
    $server->close($request->fd);
}
/**
* 鉴权方式,可以采用对称加密/非对称加密等形式,验证socket-id和socket-signature是否是匹配上的
* 下面使用简单用户ID[socket-id]+用户密码[socket-signature]进行验证
**/

$signature = Redis::hget('user:'.$uId,'password');
if($signature != $uSignature){
    $server->close($request->fd);
}
});

3、通信数据合法性校验,包括:通信旗帜,通信去向,通信内容三者的合法性

function verifyData($request)
    {
        $result = [
            'code' => 0,
            'msg' => 'success'
        ];
        if (empty($request['flag'])) {
            $result = [
                'code' => 1,
                'msg' => '缺失会话标识来源',
            ];
        } elseif (!in_array($request['flag'], ['init', 'singleChat', 'allUser', 'singleUser', 'ping'])) {
            $result = [
                'code' => 1,
                'msg' => '会话标识非法,flag:' . $request['flag'] ?? '',
            ];
        } elseif ($request['flag'] == 'singleChat' && (
                empty($request['data']) ||
                !array_key_exists('voiceId', $request['data']) ||
                empty($request['data']['dialogContentType']) ||
                empty($request['data']['dialogContentUri'])
            )
        ) {
            $result = [
                'code' => 1,
                'msg' => '会话内容缺失',
            ];
        } elseif ($request['flag'] == 'singleChat' && empty($request['to'])) {
            $result = [
                'code' => 1,
                'msg' => '缺失会话标识去向',
            ];
        } elseif ($request['flag'] == 'singleChat' && !strstr($request['to'], config('swoole.socketPrefix'))) {
            $result = [
                'code' => 1,
                'msg' => '会话标识去向非法,to:' . $request['to'] ?? '',
            ];
        } elseif ($request['flag'] != 'allUser' && $request['flag'] != 'init' && $request['flag'] != 'ping') {
            if (empty($request['to'])) {
                $result = [
                    'code' => 1,
                    'msg' => '会话标识去向不能为空',
                ];
            } else {
                $toParams = explode('_', $request['to']);
                $result['toUserId'] = $toParams[count($toParams) - 1];
                if (!Redis::exists(config('cachekey.userInfo') . $result['toUserId'])) {
                    $result = [
                        'code' => 1,
                        'msg' => '会话标识去向非法',
                    ];
                }
            }
        }
        return $result;
    }

4、Fd的记录与维护,记录需要考虑,多端登录以及终端数量限制,所以FD采用REDIS集合进行存储,维护一共有三种情况,1:用户主动关闭SOCKET,2:用户被动关闭SOCKET,3:SOCKET服务重启
//连接关闭时触发,解绑用户的fd

//记录FD
$verify = ['fromUserId'=>1];
 if ($verify['fromUserId'] == 0) {
                    Redis::set('fdUser:' . $request->fd, $verify['fromUserId']);
                    Redis::sadd('userFd:' . $verify['fromUserId'], $request->fd);
                } else {
                    if (count(Redis::smembers('userFd:' . $verify['fromUserId'])) > 10) {
                        $data = [
                            'code' => 1,
                            'msg' => $verify['fromUserId'], '_socket客户端开启超过10个,禁止开启更多'
                        ];
                    } else {
                        Redis::set('fdUser:' . $request->fd, $verify['fromUserId']);
                        Redis::sadd('userFd:' . $verify['fromUserId'], $request->fd);
                    }
                }
//连接关闭时触发,解绑用户的fd
$server->on('close', function ($server, $fd) {
            if (Redis::get('fdUser:' . $fd)) {
                echo date('Ymd H:i:s'), "Fd: {$fd} Client UserId:" . Redis::get('fdUser:' . $fd) . "_closed\n";
                Redis::srem('userFd:' . Redis::get('fdUser:' . $fd), $fd);
                Redis::del('fdUser:' . $fd);
            }
        });
//定时器,被动关闭
$server->on('workerStart', function ($server, $worker_id) {
            $server->tick(5000, function ($server) {
                foreach ($server->heartbeat() as $fd) {
                    if (Redis::get('fdUser:' . $fd)) {
                        echo '定时器检测:', date('Ymd H:i:s'), "Client UserId:" . Redis::get('fdUser:' . $fd) . "_closed\n";
                        Redis::srem('userFd:' . Redis::get('fdUser:' . $fd), $fd);
                        Redis::del('fdUser:' . $fd);
                        $server->close($fd);
                    }
                }
            });
        });
#shell脚本重启时,清空Redis里的相关key
redis-cli -h 127.0.0.1 -a '这是密码' keys "userFd*" | xargs redis-cli -h  127.0.0.1 -a '这是密码' del
redis-cli -h 127.0.0.1 -a '这是密码' keys "fdUser*" | xargs redis-cli -h  127.0.0.1 -a '这是密码' del

5、通信保持

//设置心跳
$server->set([
    'heartbeat_idle_time' => 20, //与heartbeat_check_interval配合使用。表示连接最大允许空闲的时间
    'heartbeat_check_interval' => 5,  //启用心跳检测,此选项表示每隔多久轮循一次,单位为秒
]);

6、通信释放,采用定时器进行处理过期,可能死掉的通信

//定时器,被动关闭
$server->on('workerStart', function ($server, $worker_id) {
            $server->tick(5000, function ($server) {
                foreach ($server->heartbeat() as $fd) {
                    if (Redis::get('fdUser:' . $fd)) {
                        echo '定时器检测:', date('Ymd H:i:s'), "Client UserId:" . Redis::get('fdUser:' . $fd) . "_closed\n";
                        Redis::srem('userFd:' . Redis::get('fdUser:' . $fd), $fd);
                        Redis::del('fdUser:' . $fd);
                        $server->close($fd);
                    }
                }
            });
        });

7、无人值守监听脚本,是基于LARAVEL COMMAND的脚本,启动命令是php artisan chatWebSocket –env=development

# swoole webscoket 无人值守监控
count=`ps -fe |grep "chatWebSocket" | grep -v "grep" | wc -l`

echo -e $(date "+%Y-%m-%d %H:%M:%S")"\c"
echo -e "__进程数量:${count}"
if [ $count -lt 1 ]; then
    ulimit -c unlimited
    cd /home/wwwroot/web
    nohup php artisan chatWebSocket --env=$1  >> /tmp/chatWebSocket 2>&1 &
    echo "restart";
    count=`ps -fe |grep "chatWebSocket" | grep -v "grep" | wc -l`
    echo -e "重启后,进程数量:${count}"
fi

8、客户端PHPDEMO
composer 引入
“require”:{

“textalk/websocket”: “^1.2”,

}

Leave Comment

电子邮件地址不会被公开。 必填项已用*标注