Swoole 實現直播登錄模塊

1. 環境部署準備;

# 創建項目目錄
cd /data/project/test/swoole/
mkdir tp5
cd tp5
# 附件裏的 tp5 代碼複製到 tp5 文件夾下
# 注意:賽事直播的一些靜態文件已經放入到 tp5/public/static 下

# 創建 server 目錄
mkdir server
cd server
vim http_server.php

# 寫入以下代碼
<?php

$http = new swoole_http_server('0.0.0.0', 8811);

$http->set([
	'worker_num' => 8,
	'enable_static_handler' => true,
	'document_root'	=> "/data/project/test/swoole/tp5/public/static",
]);

$http->on('request', function($request, $response){
	
});
$http->start();

# 開啓 http 服務
php http_server.php
# 瀏覽器訪問:http://192.168.2.214:8811/live/login.html

在這裏插入圖片描述

2. Swoole 支持 TP5;

  • 修改 http_server.php
<?php

$http = new swoole_http_server('0.0.0.0', 8811);

$http->set([
	'worker_num' => 8,
	'enable_static_handler' => true,
	'document_root'	=> "/data/project/test/swoole/tp5/public/static",
]);

// 在 worker 進程啓動時發生,創建的對象可以在進程生命週期內使用
// 在 onWorkerStart() 中加載框架的核心文件後:
// 1. 不用每次請求都加載框架核心文件,提高性能
// 2. 可以在後續的回調事件中繼續使用框架的核心文件或者類庫
$http->on('WorkerStart', function(swoole_server $server,  $worker_id){
	// 1. 先加載 public/index.php 裏的內容
	// 定義應用目錄
	define('APP_PATH', __DIR__ . '/../application/');

	// 加載框架的引導文件(注意修改路徑)
	// ThinkPHP 引導文件
	// 爲什麼不直接加載 /thinkphp/base.php ?
	// worker 進程只需要加載文件,不需要加載應用程序
	// 應用程序是在 request 裏執行
	require __DIR__ . '/../thinkphp/base.php';

	// 以下代碼會直接執行框架 application/index/controller/index.php 中的 index() 裏的內容 8 次
	// 因爲上面 set 方法配置的 work_num 爲 8
	// require __DIR__ . '/../thinkphp/start.php';
});

$http->on('request', function($request, $response){
	// Swoole 不會釋放超全局變量($_GET 等),會有緩存
	// 所以每次都初始化變量
	// 另外注意:define 的常量不會註銷;還要特別注意 die、exit()
	$_SERVER  =  [];
    if(isset($request->server)) {
        foreach($request->server as $k => $v) {
            $_SERVER[strtoupper($k)] = $v;
        }
	}
	if(isset($request->header)) {
        foreach($request->header as $k => $v) {
            $_SERVER[strtoupper($k)] = $v;
        }
    }

	$_GET = [];
    if(isset($request->get)) {
        foreach($request->get as $k => $v) {
            $_GET[$k] = $v;
        }
    }

	$_POST = [];
    if(isset($request->post)) {
        foreach($request->post as $k => $v) {
            $_POST[$k] = $v;
        }
	}
	
	// 開啓緩衝區
	ob_start();
	// 執行應用並響應
	try{
		// 此處代碼摘自 /../thinkphp/start.php,注意加上命名空間 think
		think\Container::get('app', [APP_PATH])
		->run()
		->send();
	}catch(\Exception $e){
		//todo
	}
	
	$res = ob_get_contents();
	ob_end_clean();
	$response->end($res);

});
$http->start();
關於路由解決方案
當第一次請求後下一次再請求不同的模塊或者方法不生效,都是第一次請求模塊/控制器/方法
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
// 在 server/http_server.php 中,執行應用的代碼如下
think\Container::get('app', [APP_PATH])->run()->send();
// 所以需要先找到 run() 方法,
// 在 thinkphp/library/think/App.php,搜索 run() 方法
// 鎖定如下代碼:
// 進行URL路由檢測
$dispatch = $this->routeCheck();
// 搜索 routeCheck() 方法,鎖定如下代碼
$path = $this->request->path();
// 搜索 path() 方法,在 thinkphp/library/think/request.php,搜索 path()
// 鎖定如下代碼
if (is_null($this->path)) {
}
// 把 if 判斷 和後面的又大括號註釋掉,不再複用類成員變量 $this->path,直接走代碼邏輯
// 然後鎖定 $pathinfo = $this->pathinfo();
// 搜索 pathinfo(),鎖定如下代碼
if (is_null($this->pathinfo)){
}
// 同樣,把 if 判斷 和後面的又大括號註釋掉,不再複用類成員變量 $this->pathinfo,直接走代碼邏輯
// 最後,是 tp5 支持 pathinfo 路由,添加如下代碼在function pathinfo() { } 開頭
if (isset($_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] != '/') {
    return ltrim($_SERVER['PATH_INFO'], '/');
}
# 修改成功後,
# http://192.168.2.214:8811/?s=index/index/index
# 和
# http://192.168.2.214:8811/index/index/index
# 兩種都能正確訪問
關於平滑重啓
修改 http_server.php 後不殺進程,平滑重啓 http 服務
# 終端 1 輸入
php http_server.php
# 此時修改了代碼,需要重啓 http 服務

# 打開終端 2:
netstat -anp | grep 8811
# 獲取主進程號: 10894
kill -USR1 10894
# 此時終端 1 顯示
[2019-09-15 21:46:07 $10895.0]  INFO    Server is reloading all workers now

3. 登錄流程介紹;

  • 登錄採用手機號 + 驗證碼的方式。
  • 用戶的使用場景:先輸入手機號去獲取驗證碼。獲取到驗證碼之後,用戶填寫驗證碼點擊登錄,就可以登錄平臺
  • 在這個過程中,用戶輸入手機號點擊驗證碼的過程中,前端 js 會拋送一個 ajax 地址,這個地址會基於 swoole http 服務。
  • 在這個 http 地址中,會用到阿里大魚短信服務。獲取到手機號碼,隨機生成六位隨機數,存入到 Redis 裏,和手機號進行一個綁定。放入到 Redis 之後,然後將手機號 + 驗證碼通過 SDK 推回給阿里大雨。在推送的過程中,會用到 Swoole 的 task 異步任務來進行相應的我處理。
  • 推送好之後,阿里大魚校驗成功後,就會把驗證碼發送給手機。然後手機收到驗證碼之後,用戶就可以拿到驗證碼進行登錄。
  • 當用戶點擊登錄按鈕的時候,前端 js 拋送一個 ajax 地址,然後後端的服務就會進行手機號 + 短信驗證碼進行驗證。因爲手機號 + 短信驗證碼的數據已經存入到了 Redis ,就基於 Redis 裏的數據進行校驗,如果存在沒有失效就登錄,然後綁定相應的 cookie,存入到瀏覽器當中去,後面的會話就根據 cookie 進行相應的判斷。

在這裏插入圖片描述

4. 登錄實現。

  • 錯誤代碼配置文件:新增 config/code.php
<?php

// 錯誤代碼
return [
    'success' => 1,
    'error' => 10
];
  • Redis 配置文件:新增 config/redis.php
<?php

// redis 配置
return [
    'host' => '127.0.0.1',
    'port' => 6379,
    'auth' => 'asdf',
    'timeOut' => 5,     // 連接超時時間
    'out_time' => 500,  // 過期時間
];
  • 封裝的類文件,統一放在 application/common
    在這裏插入圖片描述

  • 創建第三方短信對接類:application/common/lib/ali/Sms.php

<?php
// 第三方短信對接相關代碼寫在這裏
class Sms{

}
  • 創建 Redis 類,使用單例模式:application/common/lib/redis/Predis.php
<?php

namespace app\common\lib\redis;


class Predis{

    public $redis = "";

    // 定義單例模式變量
    private static $_instance = null;

    public static function getInstance(){
        if(empty(self::$_instance)){
            self::$_instance = new self();
        }
        return self::$_instance;
    }


    private function __construct(){
        $this->redis = new \redis();
        $result = $this->redis->connect(config('redis.host'), config('redis.port'), config('redis.timeOut'));
        $result2 = $this->redis->auth(config('redis.auth'));
        if($result == false || $result2 == false){
            throw new \Exception('redis connect error');
        }

    }

    /**
     * @param $key
     * @param $value
     * @param int $time
     * @return bool|string
     */
    public function set($key, $value, $time = 0){
        if(!$key){
            return '';
        }

        if(is_array($value)){
            $value = json_encode($value);
        }

        if(!$time){
            return $this->redis->set($key, $value);
        }

        return $this->redis->setex($key, $time, $value);
    }

    /**
     * @param $key
     * @return bool|string
     */
    public function get($key){
        if(!$key){
            return '';
        }

        return $this->redis->get($key);
    }

}
  • 創建 Task 類,Swoole 後續所有 task 異步任務全部寫在這裏:application/common/lib/task/Task.php
<?php

/**
 * Swoole 後續所有 task 異步任務 都放到這裏來
 */
namespace app\common\lib\task;

use app\common\lib\ali\Sms;
use app\common\lib\Redis;
use app\common\lib\redis\Predis;

class Task{

    /**
     * 異步發送驗證碼短信邏輯
     * @param $data
     */
    public function sendSms($data){
        print_R($data);

        // 發送成功,驗證碼記錄到 redis
        try{
            Predis::getInstance()->set(Redis::smsKey($data['phone']), $data['code'], config('redis.out_time'));
        }catch (\Exception $e){
            echo $e->getMessage();

        }

    }

}
  • 插入 Redis 數據庫的 key 前綴設定:application/common/lib/Redis.php
<?php

namespace app\common\lib;

class Redis{

    /**
     * 發送驗證碼的前綴
     * @var string
     */
    public static $pre = "sms_";


    /**
     * 用戶前綴
     * @var string
     */
    public static $userpre = "user_";

    /**
     * 存儲驗證碼的 redis key
     * @param $phone
     * @return string
     */
    public static function smsKey($phone){
        return self::$pre . $phone;

    }

    /**
     * 用戶前綴 redis key
     * @param $phone
     * @return string
     */
    public static function userKey($phone){
        return self::$userpre . $phone;

    }
}
  • 通用方法:application/common/lib/Util.php
<?php

namespace app\common\lib;

class Util{

    /**
     * API 輸出格式
     * @param $status
     * @param string $message
     * @param array $data
     */
    public static function show($status, $message = '', $data = []){
        $result = [
            'status' => $status,
            'message' => $message,
            'data' => $data
        ];

        echo json_encode($result);
    }
}
  • 模擬驗證碼接口:application/index/controller/Send.php
<?php
namespace app\index\controller;

use app\common\lib\Util;
use app\common\lib\Redis;

class Send
{
    // 發送驗證碼
    public function index(){
       //$phoneNum = request()->get('phone_num', 0, 'intval');
        $phoneNum = intval($_GET['phone_num']);
       if(empty($phoneNum)){
           return Util::show(config('code.error'), 'error');
       }

       // 生成一個隨機數
        $code = rand(1000, 9999);

       // 對接第三方短信平臺,代碼放到 task 裏
        $taskData = [
            'method' => 'sendSms',
            'data' => [
                'phone' => $phoneNum,
                'code' => $code
            ]
        ];
        $_POST['http_server']->task($taskData);

       // 數據記錄到 redis
        // $redis = new \swoole\Coroutine\redis();
        // $redis->connect(config('redis.host'), config('redis.port'));
        // $redis->auth(config('redis.auth'));
        // $redis->set(Redis::smsKey($phoneNum), $code, config('redis.out_time'));

        return Util::show(config('code.success'), $code);

    }

}
  • 模擬登錄接口:application/index/controller/Login.php
<?php
namespace app\index\controller;

use app\common\lib\Util;
use app\common\lib\Redis;
use app\common\lib\redis\Predis;
use Exception;

class Login
{
    // 登錄
    public function index(){
        // phone code
        $phoneNum = intval($_GET['phone_num']);
        $code = intval($_GET['code']);
        if(empty($phoneNum) || empty($code)){
            return Util::show(config('code.error'), 'phone or code is error');
        }

        // redis code
        try {
            $redisCode = Predis::getInstance()->get(Redis::smsKey($phoneNum));
        } catch (\Exception $e){
            echo $e->getMessage();
        }

        if($redisCode == $code){
            // 寫入 redis
            $data = [
                'user' => $phoneNum,
                'srcKey' => md5(Redis::userKey($phoneNum)),
                'time' => time(),
                'isLogin' => true
            ];
            Predis::getInstance()->set(Redis::userKey($phoneNum), $data);

            return Util::show(config('code.success'), 'ok');
        }

        return Util::show(config('code.error'), 'login error');
    }

}
  • 前端頁面(重點 js 邏輯):public/static/live/login.html
<!DOCTYPE html>
<html>

<head>
	<meta charset="utf-8" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<title>圖片直播 - 登錄</title>
	<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
	<meta content="yes" name="apple-mobile-web-app-capable" />
	<meta content="black" name="apple-mobile-web-app-status-bar-style" />
	<meta content="telephone=no" name="format-detection" />
	<meta content="email=no" name="format-detection" />
	<link rel="stylesheet" type="text/css" href="./assert/css/reset.css" />
	<link rel="stylesheet" type="text/css" href="./assert/css/main.css" />
	<link rel="stylesheet" href="./assert/iconfont/iconfont.css">
	<link rel="shortcut icon" href="./favicon.ico">
	<script src="./js/jquery-3.3.1.min.js"></script>
	<style>
		body {
			background: #eee;
		}
		.login {
			text-align: center;
			margin-top: 8vh;
			padding: 20px;
		}
		.login h2 {
			font-size: 1.2rem;
			margin-bottom: 1rem;
		}
		.login-item {
			font-size: 0;
			background: #fff;
			padding-left: 1rem;
			border: 1px solid #eee;
		}
		/*避免兩個輸入框間的border重疊*/
		.login-item:last-child {
			border-top: 0;
		}
		input, button {
			width: 100%;
			border: none;
			outline: none;
			height: 50px;
			line-height: 50px;
			font-size: 1.2rem;
			color: #333;
			background: transparent;
		}
		.phone-num {
			width: 70%;
		}
		/*獲取驗證碼的button*/
		.login-item button {
			width: 30%;
			padding: 0 10px;
			background: none;
			color: inherit;
			display: inline-block;
			background: ghostwhite;
			border-left: 1px solid #eee;
		}
		.submit-btn {
			background: #00a1d6;
			width: 100%;
			color: #fff;
			margin-top: 30px;
		}

	</style>
</head>

<body>
	<header class="header xxl-font">
		<i class="icon iconfont icon-fanhui back" id="back"></i>
		登錄
	</header>
	<form class="login" id="form">
		<h2>體育賽事圖文直播平臺</h2>
		<div class="login-item">
			<input type="text" placeholder="手機號" class="phone-num" name="phone_num"/>
			<button type="button" id="authCodeBtn">獲取驗證碼</button>
		</div>
		<div class="login-item">
			<input type="text" placeholder="驗證碼" name="code" />
		</div>
		<button type="submit" class="submit-btn" id="submit-btn">進入平臺</button>
	</form>
	<script>
		$(function () {
			var $back = $('#back');
			var $submitBtn = $('#submit-btn');
			// 獲取驗證嗎
		  $('#authCodeBtn').click(function (event) {
			    var phone_num = $(" input[ name='phone_num' ] ").val();
			    console.log(phone_num);
				url = "http://192.168.2.214:8811?s=index/send&phone_num="+phone_num;
				$(this).html('已發送').attr('disabled', true);
				// $.post()
				$.get(url, function (data) {
					console.log(data);
					// TODO: 將下面3行代碼刪除
					if (data.status == 1) {
						alert('驗證號碼爲:' + data.message);
					}
					// if (result.status != 'ok') {
					// 	alert('網絡錯誤');
					// }
				}, 'json');
			});

			// 提交表單
			$submitBtn.click(function (event) {
				event.preventDefault();
				var formData = $('form').serialize();
				// TODO: 請求後臺接口跳轉界面,前端跳轉或者後臺跳
				$.get("http://192.168.2.214:8811?s=index/login&"+formData, function (data) {
					console.log("http://192.168.2.214:8811?s=index/login&"+formData);
					// location.href='index.html';
					if(data.status == 1){
						// 登錄成功
					}else{
						// 登錄失敗
					}
				}, 'json');
			});

			// 返回上一頁
			$back.click(function (e) {
				window.history.back();
			});
		});
	</script>
</body>

</html>
  • http_server 文件優化:server/http_server.php
<?php

class Http {
    CONST HOST = "0.0.0.0";
    CONST PORT = 8811;

    public $http = null;
    public function __construct() {
        $this->http = new swoole_http_server(self::HOST, self::PORT);

        $this->http->set([
            'worker_num' => 4,
            'task_worker_num' => 4,
            'enable_static_handler' => true,    // 開啓靜態支持
            'document_root'	=> "/data/project/test/swoole/tp5/public/static",
        ]);

        $this->http->on("workerStart", [$this, 'onWorkerStart']);
        $this->http->on("request", [$this, 'onRequest']);

        $this->http->on("task", [$this, 'onTask']);
        $this->http->on("finish", [$this, 'onFinish']);
        $this->http->on("close", [$this, 'onClose']);


        $this->http->start();
    }


    /**
     * @param $server
     * @param $worker_id
     */
    public function onWorkerStart($server,  $worker_id) {
        // 定義應用目錄
        define('APP_PATH', __DIR__ . '/../application/');
        // 加載框架文件
        // 下次所有請求過來就不需要一一加載
        // 作用其它的回調時,能找到框架裏的內容
        // require __DIR__ . '/../thinkphp/base.php';
        require __DIR__ . '/../thinkphp/start.php';
    }

    /**
     * request 回調
     * @param $request
     * @param $response
     */
    public function onRequest($request, $response) {
        $_SERVER = [];
        if(isset($request->server)) {
            foreach($request->server as $k => $v) {
                $_SERVER[strtoupper($k)] = $v;
            }
        }

        if(isset($request->header)) {
            foreach($request->header as $k => $v) {
                $_SERVER[strtoupper($k)] = $v;
            }
        }

        $_GET = [];
        if(isset($request->get)) {
            foreach($request->get as $k => $v) {
                $_GET[$k] = $v;
            }
        }

        $_POST = [];
        if(isset($request->post)) {
            foreach($request->post as $k => $v) {
                $_POST[$k] = $v;
            }
        }

        $_POST['http_server'] = $this->http;

        ob_start();
        try{
            think\Container::get('app', [APP_PATH])->run()->send();
        }catch(\Exception $e){
            // todo
        }

        $res = ob_get_contents();
        ob_end_clean();
        $response->end($res);
    }


    /**
     * @param $serv
     * @param $task_id
     * @param $workerId
     * @param $data
     */
    public function onTask($serv, $task_id, $workerId, $data){
        // 分發 task 任務機制,讓不同的任務走不同邏輯
        $obj = new app\common\lib\task\Task;

        $method = $data['method'];
        // 執行投放過來的 $method 變量 方法
        // 這裏需要判斷一些值是否存在
        $flag = $obj->$method($data['data']);

        return $flag;

        // print_R($data);
        // return "on task finish";
    }


    /**
     * @param $serv
     * @param $taskId
     * @param $data
     */
    public function onFinish($serv, $taskId, $data){
        // 這裏的 $data 是 onTask() 方法 return 的內容
        echo "taskId:{$taskId}\n";
        echo "finish-data-success:{$data}\n";

    }

    public function onClose($ws, $fd){
        echo "clientId: {$fd} \n";
    }

}

new Http();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章