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();
发布了119 篇原创文章 · 获赞 12 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/hualaoshuan/article/details/100853790