43

上一篇: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();

    clipboard.png

    以树状图显示进程间的关系:pstree -p 进程id
    启动成功后会创建worker_num+2个进程。Master进程+Manager进程+serv->worker_numWorker进程

1.1.2 进程使用场景

    clipboard.png

    clipboard.png

管道:进程和进程间的一个桥梁
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();

    测试:

    clipboard.png

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中加载框架的核心文件后:

  1. 不用每次请求都加载框架核心文件,提高性能
  2. 可以在后续的回调事件中继续使用框架的核心文件或者类库

2.1.3 关于再次请求进程缓存解决方案

    当前worker进程没有结束,所以会保存上一次的资源等。解决上一次输入的变量还存在的问题:

  1. 方案一:if(!empty($_SERVER)) { unset($_SERVER); }
  2. 方案二:$http-close();把之前的进程killswoole会重新启一个进程,重启会释放内存,把上一次的资源包括变量等全部清空(php-cli控制台会提示错误)
  3. 方案三:$_SERVER = []推荐方案

2.1.3 关于ThinkPHP5路由解决方案

当第一次请求后下一次再请求不同的模块或者方法不生效,都是‘第一次’请求模块/控制器/方法。如下图:

    clipboard.png

    clipboard.png

    clipboard.png

修改ThinkPHP5框架Request.php源码位置:/thinkphp/library/think/Request.php

    修改如下:

  1. function path() { }
    //注销判断,不再复用类成员变量$this->path
  2. function pathinfo() { }
    //注销判断,不再复用类成员变量$this->pathinfo
  3. 使其支持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;
    }
}

下一篇:Swoole入门到实战(三):图文直播和聊天室模块、系统监控和性能优化模块、负载均衡 - 完结篇

参考教程:韩天峰力荐 Swoole入门到实战打造高性能赛事直播平台


Bohr
6.5k 声望3.3k 粉丝