用 hyperf websocket 實現,類似 qq 單機登入功能

jksusu發表於2019-09-23

祝賀 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 協議》,轉載必須註明作者和本文連結

相關文章