博客列表 >Swoole入门到实战(一):PHP7&Swoole源码安装、玩转网络通信引擎、异步非堵塞IO场景

Swoole入门到实战(一):PHP7&Swoole源码安装、玩转网络通信引擎、异步非堵塞IO场景

夏日的烈风的博客
夏日的烈风的博客原创
2018年08月07日 23:03:132078浏览

一、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 负载均衡示例

完!


声明:本文内容转载自脚本之家,由网友自发贡献,版权归原作者所有,如您发现涉嫌抄袭侵权,请联系admin@php.cn 核实处理。
全部评论
文明上网理性发言,请遵守新闻评论服务协议