一、PHP7源码安装和Swoole源码编译安装
1.1 PHP7源码安装
1.1.1 获取源码与安装
获取PHP7源码:www.php.net
tar -xzvf ... # 解压命令./configure --prefix=/home/study/php # 安装至某个路径,提前安装gcc等make # 编译make install # 安装
源码执行文件放在:bin目录下
php -m # 查看 PHP 安装的扩展
1.1.2 简化PHP执行命令
alias 命令=命令的绝对路径
vim /.bash_profilealias php=/home/work/soft/php/bin/php # 添加source /.bash_profile # 注意
source FileName
作用:在当前bash环境下读取并执行FileName中的命令。 用于重新执行刚修改的初始化文档,如 .bash_profile 和 .profile 等等
注:该命令通常用命令“.”来替代
如:source /etc/profile 与 . /etc/profile是等效的
php -i | grep php.ini # 查找PHP的配置文件
1.2 Swoole源码编译安装
获取swoole源码:https://gitee.com/swoole/swoole.git
phpize是用来扩展php模块的,通过phpize可以建立php的外挂模块,解决没有configure问题
/usr/local/php/bin/phpize # 在需要执行的目录执行这行代码即可./configure --with-php-config=/usr/local/php/bin/php-config
make
make install
最后可以在PHP的扩展目录中看见swoole.so 扩展文件
1.3 双剑合璧,PHP7支持swoole
在php.ini文件中添加:extension=swoole.so
查看是否添加成功:php -m
在swoole/examples/server下执行php echo.php
查看是否执行端口:9501
netstat -anp|grep 9501
二、玩转网络通信引擎(非常重要)
2.1 TCP服务&TCP客户端
2.1.1 TCP服务
Swoole官网文档:创建TCP服务器 | 创建UDP服务器
//创建Server对象,监听 127.0.0.1:9501端口$serv = new swoole_server("127.0.0.1", 9501);//swoole_server->set函数用于设置swoole_server运行时的各项参数$serv->set([ 'worker_num' => 6 , // worker进程数,cpu 1-4倍
'max_request' => 10000,
]);/**
* 监听连接进入事件
* $fd 客户端连接的唯一标示
* $reactor_id 线程id
*/$serv->on('connect', function ($serv, $fd, $reactor_id) { echo "Client: {$reactor_id} - {$fd}-Connect.\n";
});/**
* 监听数据接收事件
* $reactor_id = $from_id
*/$serv->on('receive', function ($serv, $fd, $reactor_id, $data) {
$serv->send($fd, "Server: {$reactor_id} - {$fd}".$data);
});//监听连接关闭事件$serv->on('close', function ($serv, $fd) { echo "Client: Close.\n";
});//启动服务器$serv->start();
测试tcp服务器方法:
netstat -anp | grep 9501
通过telnet方式登录远程主机:telnet 127.0.0.1 9501
tcp客户端脚本
查看当前worker进程数:ps -aft | grep tcp_server.php
Tips:为了保证程序执行的完整性,当修改tcp服务器脚本后最好设置平滑重启worker进程
平滑重启worker进程
2.1.2 TCP客户端
阿里云服务器巨坑----端口未对外打开!!!websocket连接不上服务器,提示Provisional headers are shown
<?php// 连接 swoole tcp 服务$client = new swoole_client(SWOOLE_SOCK_TCP);if(!$client->connect("127.0.0.1", 9501)) {
echo "连接失败"; exit;
}// php cli常量
fwrite(STDOUT, "请输入消息:");$msg = trim(fgets(STDIN));// 发送消息给 tcp server服务器$client->send($msg);// 接受来自server 的数据$result = $client->recv();
echo $result;
2.2 HTTP服务(常用)
$http = new swoole_http_server("0.0.0.0", 8811);//添加测试一:获取参数并打印出来//$http->on('request', function ($request, $response) {// $response->cookie("singwa",'xsssss', time() + 1800);// $response->end('sss'.json_encode($request->get));//});/**
* https://wiki.swoole.com/wiki/page/783.html
* 配置静态文件根目录,与enable_static_handler配合使用。
* 设置document_root并设置enable_static_handler为true后,
* 底层收到Http请求会先判断document_root路径下是否存在此文件,
* 如果存在会直接发送文件内容给客户端,不再触发onRequest回调。
*/$http->set(
[ 'enable_static_handler' => true, 'document_root' => "/home/work/hdtocs/swoole_mooc/data",
]
);
$http->on('request', function($request, $response) { //print_r($request->get);
$content = [ 'date:' => date("Ymd H:i:s"), 'get:' => $request->get, 'post:' => $request->post, 'header:' => $request->header,
];
swoole_async_writefile(__DIR__."/access.log", json_encode($content).PHP_EOL, function($filename){ // todo
}, FILE_APPEND);
$response->cookie("singwa", "xsssss", time() + 1800);
$response->end("sss". json_encode($request->get));
});
$http->start();
2.3 WebSocket服务(重点)
2.3.1 基本概述
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信--允许服务器主动发送信息给客户端
为什么需要WebSocket
缺陷:HTTP的通信只能由客户端发起
WebSocket特点
建立在TCP协议之上
性能开销小通信高效
客户端可以与任意服务器通信
协议标识符ws wss
持久化网络通信协议
2.3.2 案例实现
2.3.2.1 服务端实现
1. 面向过程:procedure_ws_server.php
$server = new swoole_websocket_server("0.0.0.0", 9912);//配置静态文件根目录,可选$server->set(
[ 'enable_static_handler' => true, 'document_root' => "/home/wwwroot/www.lingyuan88.com/public/swoole/data",
]
);//监听websocket连接打开事件$server->on('open', 'onOpen');function onOpen($server, $request) {
print_r($request->fd);
}// 监听ws消息事件$server->on('message', function (swoole_websocket_server $server, $frame) { echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
$server->push($frame->fd, "singwa-push-secesss");
});
$server->on('close', function ($ser, $fd) { echo "client {$fd} closed\n";
});
$server->start();
2. WebSocket服务优化,基础类库面向对象:object_ws_server.php
class Ws { CONST HOST = "0.0.0.0"; CONST PORT = 9912; public $ws = null; public function __construct() { $this->ws = new swoole_websocket_server(self::HOST, self::PORT); //配置静态文件根目录,可选
$this->ws->set(
[ 'enable_static_handler' => true, 'document_root' => "/home/wwwroot/www.lingyuan88.com/public/swoole/data",
]
); $this->ws->on("open", [$this, 'onOpen']); $this->ws->on("message", [$this, 'onMessage']); $this->ws->on("close", [$this, 'onClose']); $this->ws->start();
} /**
* 监听ws连接事件
* @param $ws
* @param $request
*/
public function onOpen($ws, $request) {
print_r($request->fd);
} /**
* 监听ws消息事件
* @param $ws
* @param $frame
*/
public function onMessage($ws, $frame) { echo "ser-push-message:{$frame->data}\n";
$ws->push($frame->fd, "server-push:".date("Y-m-d H:i:s"));
} /**
* close
* @param $ws
* @param $fd
*/
public function onClose($ws, $fd) { echo "clientid:{$fd}\n";
}
}
$obj = new Ws();
2.3.2.2 客户端实现
ws_client.html
<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8">
<title></title></head><body><h1>singwa-swoole-ws测试</h1>
<script>
var wsUrl = "ws://120.77.206.215:9912"; var websocket = new WebSocket(wsUrl); //实例对象的onopen属性
websocket.onopen = function(evt) {
websocket.send("hello-sinwa"); console.log("conected-swoole-success");
} // 实例化 onmessage
websocket.onmessage = function(evt) { console.log("ws-server-return-data:" + evt.data);
} //onclose
websocket.onclose = function(evt) { console.log("close");
} //onerror
websocket.onerror = function(evt, e) { console.log("error:" + evt.data);
} </script></body></html>
2.3.2.3 测试
1. 通过WebSocket静态文件目录测试
2. 通过HTTP服务测试
2.4 异步Task任务使用(重点)
使用场景
执行耗时的操作(发送邮件 广播等)
注意:
投递异步任务之后程序会继续往下执行,不会等待任务执行完后再继续向下执行
class Ws { CONST HOST = "0.0.0.0"; CONST PORT = 9912; public $ws = null; public function __construct() { $this->ws = new swoole_websocket_server(self::HOST, self::PORT); $this->ws->set(
[ 'worker_num' => 2, 'task_worker_num' => 2,
]
); //注册Server的事件回调函数
$this->ws->on("open", [$this, 'onOpen']); $this->ws->on("message", [$this, 'onMessage']); $this->ws->on("task", [$this, 'onTask']); $this->ws->on("finish", [$this, 'onFinish']); $this->ws->on("close", [$this, 'onClose']); $this->ws->start();
} /**
* 监听ws连接事件
* @param $ws
* @param $request
*/
public function onOpen($ws, $request) {
var_dump($request->fd);
} /**
* 监听ws消息事件
* @param $ws
* @param $frame
*/
public function onMessage($ws, $frame) { echo "ser-push-message:{$frame->data}\n"; // todo 10s
$data = [ 'task' => 1, 'fd' => $frame->fd,
]; //投递异步任务
//注意:程序会继续往下执行,不会等待任务执行完后再继续向下执行
$ws->task($data); //客户端会马上收到以下信息
$ws->push($frame->fd, "server-push:".date("Y-m-d H:i:s"));
} /**
* @param $serv
* @param $taskId
* @param $workerId
* @param $data
* @return string
*/
public function onTask($serv, $taskId, $workerId, $data) {
print_r($data); // 耗时场景 10s
sleep(10); return "on task finish"; // 告诉worker,并返回给onFinish的$data
} /**
* @param $serv
* @param $taskId
* @param $data
*/
public function onFinish($serv, $taskId, $data) { echo "taskId:{$taskId}\n"; echo "finish-data-sucess:{$data}\n";
} /**
* close
* @param $ws
* @param $fd
*/
public function onClose($ws, $fd) { echo "clientid:{$fd}\n";
}
}
$obj = new Ws();
三、异步非堵塞IO场景
3.1 异步、阻塞和IO模型(务必理解)
3.1.1 同步和异步
关注的是消息通知机制;
同步:调用发出之后不会立即返回,但一旦返回,则返回最终结果;
异步:调用发出之后,被调用方立即返回消息,但返回的并非最终结果。被调用者通过状态、通知机制等来通知调用者,或通过回调函数来处理结果;
3.1.2 阻塞(block)和非阻塞(nonblock)
关注的是调用者等待被调用者返回调用结果时的状态。
阻塞:调用结果返回之前,调用者会被挂起,调用者只有在得到返回结果之后才能继续。
非阻塞:调用者在结果返回之前,不会被挂起;
3.1.3 IO模型
blocking IO:阻塞式IO
nonblocking IO:非阻塞IOmultiplexing IO:多路复用IO
signal driven IO:事件驱动式IO
asynchronous IO:异步IO
真正执行IO过程的阶段是内核内存数据拷贝到进程内存中
3.2 Swoole异步毫秒定时器
异步高精度定时器,粒度为毫秒级
//每隔2000ms触发一次swoole_timer_tick(2000, function ($timer_id) {
echo "tick-2000ms\n";
});//3000ms后执行此函数swoole_timer_after(3000, function () {
echo "after 3000ms.\n";
});
3.3 异步文件系统IO
Swoole官网文档:异步文件系统IO
3.3.1 异步读
/**
* 读取文件
* __DIR__
* 文件不存在会返回false
* 成功打开文件立即返回true
* 数据读取完毕后会回调指定的callback函数。
*///函数风格$result = swoole_async_readfile(__DIR__."/1.txt", function($filename, $fileContent) { echo "filename:".$filename.PHP_EOL; // \n \r\n
echo "content:".$fileContent.PHP_EOL;
});//命名空间风格$result = Swoole\Async::readfile(__DIR__."/1.txt", function($filename, $fileContent) { echo "filename:".$filename.PHP_EOL; // \n \r\n
echo "content:".$fileContent.PHP_EOL;
});
var_dump($result);echo "start".PHP_EOL;
3.3.2 异步写(如日志)
$http->on('request', function($request, $response) {
$content = [ 'date:' => date("Ymd H:i:s"), 'get:' => $request->get, 'post:' => $request->post, 'header:' => $request->header,
];
swoole_async_writefile(__DIR__."/access.log", json_encode($content).PHP_EOL, function($filename){ // todo
}, FILE_APPEND);
$response->end("response:". json_encode($request->get));
});
3.4 异步MySQL详解
class AsyncMySql { /**
* @var string
*/
public $dbSource = ""; /**
* mysql的配置
* @var array
*/
public $dbConfig = []; public function __construct() { //new swoole_mysql;
$this->dbSource = new Swoole\Mysql; $this->dbConfig = [ 'host' => '127.0.0.1', 'port' => 3306, 'user' => 'root', 'password' => 'test', 'database' => 'test', 'charset' => 'utf8',
];
} public function update() {} public function add() {} /**
* mysql 执行逻辑
* @param $id
* @param $username
* @return bool
*/
public function execute($id, $username) { $this->dbSource->connect($this->dbConfig, function($db, $result) use($id, $username) { echo "mysql-connect".PHP_EOL; if($result === false) {
var_dump($db->connect_error); // todo
}
$sql = "select * from cmf_user where id=1"; //$sql = "update test set `username` = '".$username."' where id=".$id;
// insert into
// query (add select update delete)
$db->query($sql, function($db, $result){ // select => result返回的是 查询的结果内容
if($result === false) { // todo
var_dump($db->error);
}elseif($result === true) {// add update delete
// todo
var_dump($db->affected_rows);
}else {
print_r($result);
}
$db->close();
});
}); return true;
}
}
$obj = new AsyncMySql();
$flag = $obj->execute(1, 'singwa-111112');
var_dump($flag).PHP_EOL;echo "start".PHP_EOL;
3.5 异步Redis
3.5.1 环境准备
swoole使用redis的前置条件
redis服务
hiredis库
编译swoole需要加入 -enable-async-redis
编译安装hiredis
使用Redis客户端,需要安装hiredis库,下载hiredis源码后,执行make -j
sudo make install
sudo ldconfig
hiredis下载地址
启用异步Redis客户端
编译swoole时,在configure指令中加入--enable-async-redis[root@izwz93ee3z8wdxsujiec2oz swoole]# ./configure --with-php-config=/usr/local/php/bin/php-config --enable-async-redis
make clean
make -j
sudo make install
查看PHP的swoole扩展:php -m
查看hiredis是否编译安装成功:php --ri swoole
3.5.2 代码测试
$redisClient = new swoole_redis;// Swoole\Redis$redisClient->connect('127.0.0.1', 6379, function(swoole_redis $redisClient, $result) { echo "connect".PHP_EOL;
var_dump($result);
// 同步 redis (new Redis())->set('key',2);
/*$redisClient->set('singwa_1', time(), function(swoole_redis $redisClient, $result) {
var_dump($result);
});*/
/*$redisClient->get('singwa_1', function(swoole_redis $redisClient, $result) {
var_dump($result); $redisClient->close();
});*/ $redisClient->keys('*gw*', function(swoole_redis $redisClient, $result) {
var_dump($result); $redisClient->close();
});
});echo "start".PHP_EOL;
一、进程,内存和协程
1.1 进程
1.1.1 进程
进程就是正在运行的程序的一个实例$process = new swoole_process(function(swoole_process $pro) { // todo// php redis.php
$pro->exec("/usr/local/php/bin/php", [__DIR__.'/../server/http_server.php']);
}, false);
$pid = $process->start();echo $pid . PHP_EOL;//回收结束运行的子进程swoole_process::wait();
以树状图显示进程间的关系:pstree -p 进程id
启动成功后会创建worker_num+2个进程。Master进程+Manager进程+serv->worker_num个Worker进程
1.1.2 进程使用场景
管道:进程和进程间的一个桥梁echo "process-start-time:".date("Ymd H:i:s");
$workers = [];
$urls = [ 'http://baidu.com', 'http://sina.com.cn', 'http://qq.com', 'http://baidu.com?search=singwa', 'http://baidu.com?search=singwa2', 'http://baidu.com?search=imooc',
];//创建多个子进程分别模拟请求URL的内容for($i = 0; $i < 6; $i++) {
$process = new swoole_process(function(swoole_process $worker) use($i, $urls) { // curl
$content = curlData($urls[$i]); //将内容写入管道
// echo $content.PHP_EOL;
$worker->write($content.PHP_EOL);
}, true);
$pid = $process->start();
$workers[$pid] = $process;
}//获取管道内容foreach($workers as $process) { echo $process->read();
}/**
* 模拟请求URL的内容 1s
* @param $url
* @return string
*/function curlData($url) { // curl file_get_contents
sleep(1); return $url . "success".PHP_EOL;
}echo "process-end-time:".date("Ymd H:i:s");
1.2 Swoole内存-table详解
内存操作模块之:Table
swoole_table一个基于共享内存和锁实现的超高性能,并发数据结构
使用场景:用于解决多进程/多线程数据共享和同步加锁问题
进程结束后内存表会自动释放
// 创建内存表$table = new swoole_table(1024);// 内存表增加一列$table->column('id', $table::TYPE_INT, 4);
$table->column('name', $table::TYPE_STRING, 64);
$table->column('age', $table::TYPE_INT, 3);
$table->create();
$table->set('singwa_imooc', ['id' => 1, 'name'=> 'singwa', 'age' => 30]);// 另外一种方案$table['singwa_imooc_2'] = [ 'id' => 2, 'name' => 'singwa2', 'age' => 31,
];
$table->decr('singwa_imooc_2', 'age', 2);
print_r($table['singwa_imooc_2']);echo "delete start:".PHP_EOL;
$table->del('singwa_imooc_2');
print_r($table['singwa_imooc_2']);
1.3 协程
线程、进程、协程的区别
进程,线程,协程与并行,并发
并发与并行的区别?
$http = new swoole_http_server('0.0.0.0', 9001);
$http->on('request', function($request, $response) { // 获取redis 里面 的key的内容, 然后输出浏览器
$redis = new Swoole\Coroutine\Redis();
$redis->connect('127.0.0.1', 6379);
$value = $redis->get($request->get['a']); // mysql.....
//执行时间取它们中最大的:time = max(redis,mysql)
$response->header("Content-Type", "text/plain");
$response->end($value);
});
$http->start();
二、Swoole完美支持ThinkPHP5(重难点)
2.1 面向过程方案
2.1.1 面向过程代码实现
$http = new swoole_http_server("0.0.0.0", 9911);
$http->set(
[ 'enable_static_handler' => true, 'document_root' => "/home/wwwroot/swoole/thinkphp/public/static", 'worker_num' => 5,
]
);//此事件在Worker进程/Task进程启动时发生,这里创建的对象可以在进程生命周期内使用$http->on('WorkerStart', function(swoole_server $server, $worker_id) { // 定义应用目录
define('APP_PATH', __DIR__ . '/../../../../application/'); // 加载框架里面的文件
require __DIR__ . '/../../../../thinkphp/base.php';
});
$http->on('request', function($request, $response) use($http){ //如果在每次请求时加载框架文件,则不用修改thinkphp5源码// // 定义应用目录// define('APP_PATH', __DIR__ . '/../../../../application/');// // 加载框架里面的文件// require_once __DIR__ . '/../../../../thinkphp/base.php';
/**
* 解决上一次输入的变量还存在的问题
* 方案一:if(!empty($_GET)) {unset($_GET);}
* 方案二:$http-close();把之前的进程kill,swoole会重新启一个进程,重启会释放内存,把上一次的资源包括变量等全部清空
* 方案三:$_SERVER = []
*/
$_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 {
think\Container::get('app', [APP_PATH])
->run()
->send();
}catch (\Exception $e) { // todo
} //输出TP当前请求的控制方法
//echo "-action-".request()->action().PHP_EOL;
//获取缓冲区内容
$res = ob_get_contents();
ob_end_clean();
$response->end($res); //把之前的进程kill,swoole会重新启一个进程,重启会释放内存,把上一次的资源包括变量等全部清空
//$http->close();});
$http->start();
测试:
2.1.2 onWorkerStart事件
//此事件在Worker进程/Task进程启动时发生,这里创建的对象可以在进程生命周期内使用$http->on('WorkerStart', function(swoole_server $server, $worker_id) { // 定义应用目录
define('APP_PATH', __DIR__ . '/../../../../application/'); // 加载框架里面的文件
require __DIR__ . '/../../../../thinkphp/base.php';
});
Tips:如果修改了加载框架文件,需要重启:php php_server.php
onWorkerStart:
此事件在Worker进程/Task进程启动时发生,这里创建的对象可以在进程生命周期内使用
在onWorkerStart中加载框架的核心文件后:
不用每次请求都加载框架核心文件,提高性能
可以在后续的回调事件中继续使用框架的核心文件或者类库
2.1.3 关于再次请求进程缓存解决方案
当前worker进程没有结束,所以会保存上一次的资源等。解决上一次输入的变量还存在的问题:
方案一:if(!empty($_SERVER)) { unset($_SERVER); }
方案二:$http-close();把之前的进程kill,swoole会重新启一个进程,重启会释放内存,把上一次的资源包括变量等全部清空(php-cli控制台会提示错误)
方案三:$_SERVER = [](推荐方案)
2.1.3 关于ThinkPHP5路由解决方案
当第一次请求后下一次再请求不同的模块或者方法不生效,都是‘第一次’请求模块/控制器/方法。如下图:
修改ThinkPHP5框架Request.php源码位置:/thinkphp/library/think/Request.php
修改如下:
function path() { }
//注销判断,不再复用类成员变量$this->path
function pathinfo() { }
//注销判断,不再复用类成员变量$this->pathinfo
使其支持pathinfo路由,添加如下代码在function pathinfo() { }中
if (isset($_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] != '/') { return ltrim($_SERVER['PATH_INFO'], '/');
}
修改后完整Request.php文件:
/**
* 获取当前请求URL的pathinfo信息(含URL后缀)
* @access public
* @return string
*/
public function pathinfo()
{ if (isset($_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] != '/') { return ltrim($_SERVER['PATH_INFO'], '/');
}// if (is_null($this->pathinfo)) {
if (isset($_GET[$this->config->get('var_pathinfo')])) { // 判断URL里面是否有兼容模式参数
$_SERVER['PATH_INFO'] = $_GET[$this->config->get('var_pathinfo')]; unset($_GET[$this->config->get('var_pathinfo')]);
} elseif ($this->isCli()) { // CLI模式下 index.php module/controller/action/params/...
$_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
} // 分析PATHINFO信息
if (!isset($_SERVER['PATH_INFO'])) { foreach ($this->config->get('pathinfo_fetch') as $type) { if (!empty($_SERVER[$type])) {
$_SERVER['PATH_INFO'] = (0 === strpos($_SERVER[$type], $_SERVER['SCRIPT_NAME'])) ?
substr($_SERVER[$type], strlen($_SERVER['SCRIPT_NAME'])) : $_SERVER[$type]; break;
}
}
} $this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/');// }
return $this->pathinfo;
} /**
* 获取当前请求URL的pathinfo信息(不含URL后缀)
* @access public
* @return string
*/
public function path()
{ //注销判断,不再复用类成员变量$this->path// if (is_null($this->path)) {
$suffix = $this->config->get('url_html_suffix');
$pathinfo = $this->pathinfo(); if (false === $suffix) { // 禁止伪静态访问
$this->path = $pathinfo;
} elseif ($suffix) { // 去除正常的URL后缀
$this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
} else { // 允许任何后缀访问
$this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo);
}// }
return $this->path;
}
2.2 面向对象方案
class Http { CONST HOST = "0.0.0.0"; CONST PORT = 9911; public $http = null; public function __construct() { $this->http = new swoole_http_server(self::HOST, self::PORT); $this->http->set(
[ 'enable_static_handler' => true, 'document_root' => "/home/wwwroot/swoole/thinkphp/public/static", 'worker_num' => 4,
]
); $this->http->on("workerstart", [$this, 'onWorkerStart']); $this->http->on("request", [$this, 'onRequest']); $this->http->on("close", [$this, 'onClose']); $this->http->start();
} /**
* 此事件在Worker进程/Task进程启动时发生,这里创建的对象可以在进程生命周期内使用
* 在onWorkerStart中加载框架的核心文件后
* 1.不用每次请求都加载框架核心文件,提高性能
* 2.可以在后续的回调中继续使用框架的核心文件或者类库
*
* @param $server
* @param $worker_id
*/
public function onWorkerStart($server, $worker_id) { // 定义应用目录
define('APP_PATH', __DIR__ . '/../../../../application/'); // 加载框架里面的文件
require __DIR__ . '/../../../../thinkphp/base.php';
} /**
* request回调
* 解决上一次输入的变量还存在的问题例:$_SERVER = []
* @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);
} /**
* close
* @param $ws
* @param $fd
*/
public function onClose($ws, $fd) { echo "clientid:{$fd}\n";
}
}new Http();
三、分发Task异步任务机制实现
示例演示:发送验证码
1、优化,将对接第三方的接口放入异步任务中$_POST['http_server']->task($taskData);
/**
* 发送验证码
*/
public function index() {
$phoneNum = intval($_GET['phone_num']);// tp input
if(empty($phoneNum)) { return Util::show(config('code.error'), 'error');
} // 生成一个随机数
$code = rand(1000, 9999);
$taskData = [ 'method' => 'sendSms', 'data' => [ 'phone' => $phoneNum, 'code' => $code,
]
]; //优化,将对接第三方的接口放入异步任务中
$_POST['http_server']->task($taskData); return Util::show(config('code.success'), 'ok');
}
}
2、将http对象放入预定义$_POST中,传给调用者
$_POST['http_server'] = $this->http; /**
* request回调
*/
public function onRequest($request, $response) {
......
//将http对象放入预定义$_POST中,传给调用者
$_POST['http_server'] = $this->http;
ob_start(); // 执行应用并响应
try {
think\Container::get('app', [APP_PATH])
->run()
->send();
}catch (\Exception $e) { // todo
}
......
}
3、Task任务分发 /**
* Task任务分发
*/
public function onTask($serv, $taskId, $workerId, $data) { // 分发 task 任务机制,让不同的任务 走不同的逻辑
$obj = new app\common\lib\task\Task;
$method = $data['method'];
$flag = $obj->$method($data['data']); return $flag; // 告诉worker
}
4、代表的是swoole里面后续所有task异步任务都放这里来
class Task { /**
* 异步发送 验证码
*/
public function sendSms($data, $serv) { try {
$response = Sms::sendSms($data['phone'], $data['code']);
}catch (\Exception $e) { return false;
} // 如果发送成功 把验证码记录到redis里面
if($response->Code === "OK") {
Predis::getInstance()->set(Redis::smsKey($data['phone']), $data['code'], config('redis.out_time'));
}else { return false;
} return true;
}
}
一、直播、聊天
1.1 图文直播(Redis)
在线用户处理:
方案(一):https://wiki.swoole.com/wiki/...(推荐)
方案(二)redis方案,无序集合Set
方案(三)swoole-table
/**
* 监听ws连接事件
* @param $ws
* @param $request
*/
public function onOpen($ws, $request) { // fd redis [1]
\app\common\lib\redis\Predis::getInstance()->sAdd(config('redis.live_game_key'), $request->fd);
var_dump($request->fd);
} /**
* 监听ws消息事件
* @param $ws
* @param $frame
*/
public function onMessage($ws, $frame) { echo "ser-push-message:{$frame->data}\n";
$ws->push($frame->fd, "server-push:".date("Y-m-d H:i:s"));
} /**
* close
* @param $ws
* @param $fd
*/
public function onClose($ws, $fd) { // fd del
\app\common\lib\redis\Predis::getInstance()->sRem(config('redis.live_game_key'), $fd); echo "clientid:{$fd}\n";
}
// 获取连接的用户// 赛况的基本信息入库 2、数据组织好 push到直播页面
$taskData = [ 'method' => 'pushLive', 'data' => $data
];
$_POST['http_server']->task($taskData);
/**
* 通过task机制发送赛况实时数据给客户端
* @param $data
* @param $serv swoole server对象
*/
public function pushLive($data, $serv) {
$clients = Predis::getInstance()->sMembers(config("redis.live_game_key")); foreach($clients as $fd) {
$serv->push($fd, json_encode($data));
}
}
/**
* 基础类库优化,优化两个参数的方法(重要)
* 1.集合的添加和删除
* @param $name
* @param $arguments
* @return array
*/
public function __call($name, $arguments) { //echo $name.PHP_EOL;
//print_r($arguments);
if(count($arguments) != 2) { return '';
} $this->redis->$name($arguments[0], $arguments[1]);
}
Tips:关闭服务后,清除Redis中所有客户端的id
1.2 聊天(connections)
$this->ws = new swoole_websocket_server(self::HOST, self::PORT);$this->ws->listen(self::HOST, self::CHART_PORT, SWOOLE_SOCK_TCP);
//推荐使用connections这种方式,redis方式也可以foreach($_POST['http_server']->ports[1]->connections as $fd) {
$_POST['http_server']->push($fd, json_encode($data));
}
二、系统监控和性能优化模块
2.1 系统监控
/**
* 监控服务 ws http 9911
*/class Server { const PORT = 9911; public function port() {
$shell = "netstat -anp 2>/dev/null | grep ". self::PORT . " | grep LISTEN | wc -l";
$result = shell_exec($shell); if($result != 1) { // 发送报警服务 邮件 短信
/// todo
echo date("Ymd H:i:s")."error".PHP_EOL;
} else { echo date("Ymd H:i:s")."succss".PHP_EOL;
}
}
}// 每2秒执行一次swoole_timer_tick(2000, function($timer_id) {
(new Server())->port(); echo "time-start".PHP_EOL;
});
以‘守护进程’方式在后台执行:
nohup /usr/local/php/bin/php /home/wwwroot/swoole/thinkphp/extend/swoole/monitor/server.php > /home/wwwlog/monitor.log &
检测:
ps aux | grep monitor/server.php
tail -f /home/wwwlog/monitor.log
2.2 平滑重启
/**
* 设置进程名,为后续平滑重启进程
* @param $server
*/public function onStart($server) {
swoole_set_process_name("live_master");
}
reload.sh
echo "loading..."pid=`pidof live_master`echo $pidkill -USR1 $pidecho "loading success"# linux 信号控制:USR1 平滑重载所有worker进程并重新载入配置和二进制模块
2.3 nginx 转发到 swoole_server
if (!-e $request_filename ) { proxy_pass http://127.0.0.1:9911;
}
三、 负载均衡
负载均衡:nginx必须是单台,其实nginx特别耗费cpu,需要测试nginx的转发量
代理 - 代为办理(如代理理财、代理收货等等)
3.1 代理分类
1、正向代理:
2、反向代理:
3.2 后端服务器在负载均衡调度中的状态
模拟down和backup可通过关闭端口:iptables -I INPUT -p tcp --dport 8003 -j DROP
清理规则:iptables -F
3.3 轮询策略
ip_hash:解决了不同请求打到不同服务器问题,从而保证了session和cookie的一致性。
缺点:客户端可能会再用一层代理**
url_hash:
3.4 负载均衡示例
完!