用 hyperf websocket 实现,类似 qq 单机登录功能

祝贺 hyperf 社区开通!!!

期盼已久的社区终于开通了! ≧◉◡◉≦

功能描述

如果你用过 qq,当你在A手机上登录后,然后在B手机登录。A会被强制下线。这种功能在很多系统中是非常有必要的,比如极客时间最近加入了 这种功能。

具体实现

实现的思路很多,我们选一种最简单的,websocket 实现。以下是流程,本来是画了图的,不知道咋上传!

  • 登录请求----> 查询是否登录 -->未登录,缓存信息----> success
  • 登录请求----> 查询是否登录 -->已登录,更新缓存强制缓存中的 fd 下线 -----> success

请求携带参数格式

可以直接参数拼接,在握手中间件中验证token,onopen事件中验证fd

  • ws://127.0.0.1;9502?token=121212&&channel=pc

hyperf 具体实现 需要的知识

具体核心代码

<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\Contract\OnCloseInterface;
use Hyperf\Contract\OnMessageInterface;
use Hyperf\Contract\OnOpenInterface;
use Swoole\Http\Request;
use Swoole\Server;
use Swoole\Websocket\Frame;
use Hyperf\Utils\ApplicationContext;

class BuildUserController implements OnMessageInterface, OnOpenInterface, OnCloseInterface
{
    public function onMessage(Server $server, Frame $frame): void
    {
        $server->push($frame->fd, 'Recv: ' . $frame->data);
    }
    public function onClose(Server $server, int $fd, int $reactorId): void
    {

    }
    public function onOpen(Server $server, Request $request): void
    {
        $fd = $request->fd;//当前用户fd
        $token = $request->get['token'];//token
        $channel = $request->get['channel'];//登陆渠道
        echo '当前用户fd ===' . $fd;
        //获取缓存数据
        $container = ApplicationContext::getContainer();
        $redis = $container->get(\Redis::class);
        $userData = $redis->get($token);
        if ($userData) {
            $user = json_decode($userData, true);
            if (isset($user['loginInfo']) && !empty($user['loginInfo'])) {
                echo '当前账号已登录过' . PHP_EOL;
                var_dump($user['loginInfo']) . PHP_EOL;
                //让已经存在的连接强制下线
                $loginInfo = $user['loginInfo'];
                //强制下线提示
                $close = json_encode([
                    'code' => 3000,
                    'message' => 'Your account is landing in another place',
                ]);
                switch ($channel) {
                    case 'pc':
                        if (isset($loginInfo['pc']) && !empty($loginInfo['pc'])) {
                            echo 'PC强制下线' . PHP_EOL;
                            var_dump($loginInfo['pc']) . PHP_EOL;
                            if ($server->exist($loginInfo['pc'])) {
                                echo '下线fd ==' . $loginInfo['pc'] . PHP_EOL;
                                $server->push($loginInfo['pc'], $close);
                                $server->close($loginInfo['pc']);
                                //缓存用户信息
                                $user['loginInfo']['pc'] = $fd;
                                LoginController::setCacheUsers($token, $user);
                            }
                        }
                        break;
                    case 'phone':
                        if (isset($loginInfo['phone']) && !empty($loginInfo['phone'])) {
                            if ($server->exist($loginInfo['pc'])) {
                                $server->push($request->fd, $close);
                                $server->close($loginInfo['phone']);
                                $user['loginInfo']['phone'] = $fd;
                                LoginController::setCacheUsers($token, $user);
                            }
                        }
                        break;
                }
            } else {
                echo '没有缓存' . PHP_EOL;
                //没登陆过直接缓存
                $user['loginInfo'][$channel] = $fd;
                var_dump($user) . PHP_EOL;
                LoginController::setCacheUsers($token, $user);
                $server->push($request->fd, 'onopen success');
            }
        }
    }
}

以上代码是 websocket 协程客户端代码。在 onopen事件中做。还需要一个登陆控制器。登陆成功后缓存用户信息。

<?php
declare(strict_types=1);

namespace App\Controller;

use Hyperf\HttpServer\Annotation\AutoController;//注解
use App\Model\UserInfo;
use think\facade\Validate;
use Hyperf\Utils\ApplicationContext;

/**
 * @AutoController()
 */
class LoginController extends Controller
{
    public function loginDoing()
    {
        $validate = Validate::rule([
            'username' => 'require',
            'password' => 'require',
        ]);
        if (!$validate->check($this->request->all())) {
            return $this->error($validate->getError());
        }
        $userData = UserInfo::query()
            ->where('username', '=', $this->request->input('username'))
            ->first();
        if ($userData) {
            if ($userData['password'] === $this->request->input('password')) {
                $token = self::getToken($this->request->input('username'));
                //缓存用户信息
                $userData = [
                    'username' => $userData['username'],
                    'uid' => $userData['uid'],
                    'nickname' => $userData['nickname'],
                ];
                self::setCacheUsers($token, $userData);
                return $this->success(['token' => $token], '登陆成功');
            }
        }
        return $this->error('账号或者密码错误');
    }

    /**
     * 获取唯一的 token
     * @param $username 用户名
     * @return string
     */
    protected static function getToken($username): string
    {
        return md5(uniqid() . $username);
    }

    /**
     * 缓存用户信息
     * @param string $token
     * @param array $userData
     */
    public static function setCacheUsers(string $token, array $userData)
    {
        $container = ApplicationContext::getContainer();
        $redis = $container->get(\Redis::class);
        $redis->set($token, json_encode($userData), 30000);
    }
}

以上代码仅为本人凭空猜测,具体实现我也不知道

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
讨论数量: 7

第一个帖子送不锈钢脸盆(手动滑稽)

5年前 评论
jksusu (楼主) 5年前
李铭昕

比我还快。。

5年前 评论
jksusu (楼主) 5年前

看来是早有准备

5年前 评论
jksusu (楼主) 5年前
guanhui07

有社区了.

5年前 评论

hyperf威武

5年前 评论

大佬大佬 :kissing_heart:

5年前 评论
jksusu (楼主) 5年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!