上一篇:Swoole入门到实战(一):PHP7&Swoole源码安装、玩转网络通信引擎、异步非堵塞IO场景
一、进程,内存和协程
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;
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。