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();