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