Swoole入門到實戰(二):進程,內存和協程、Swoole完美支持ThinkPHP五、分發Task異步任務機制實現

上一篇:Swoole入門到實戰(一):PHP7&Swoole源碼安裝、玩轉網絡通訊引擎、異步非堵塞IO場景

1、進程,內存和協程

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進程php

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一個基於共享內存和鎖實現的超高性能,併發數據結構
    使用場景:用於解決多進程/多線程數據共享和同步加鎖問題
    進程結束後內存表會自動釋放html

// 建立內存表
$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 協程

    線程、進程、協程的區別
    進程,線程,協程與並行,併發
    併發與並行的區別?mysql

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

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

    測試:redis

    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:sql

    此事件在Worker進程/Task進程啓動時發生,這裏建立的對象能夠在進程生命週期內使用
    在onWorkerStart中加載框架的核心文件後:thinkphp

  1. 不用每次請求都加載框架核心文件,提升性能
  2. 能夠在後續的回調事件中繼續使用框架的核心文件或者類庫

2.1.3 關於再次請求進程緩存解決方案

    當前worker進程沒有結束,因此會保存上一次的資源等。解決上一次輸入的變量還存在的問題:segmentfault

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

3、分發Task異步任務機制實現

    示例演示:發送驗證碼性能優化

一、優化,將對接第三方的接口放入異步任務中
$_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');
   }
}
二、將 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
        }
           
        ......
    }
三、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
    }
四、表明的是 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入門到實戰打造高性能賽事直播平臺

相關文章
相關標籤/搜索