前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >swoole学习笔记

swoole学习笔记

原创
作者头像
CS逍遥剑仙
修改2019-03-03 04:32:11
9510
修改2019-03-03 04:32:11
举报
文章被收录于专栏:禅林阆苑禅林阆苑

swoole学习笔记

TOC

Write By CS逍遥剑仙

我的主页: www.csxiaoyao.com

GitHub: github.com/csxiaoyaojianxian

Email: sunjianfeng@csxiaoyao.com

QQ: 1724338257

swoole是面向生产环境的 PHP 异步网络通信引擎,本笔记是本人在学习完慕课网的课程《Swoole入门到实战打造高性能赛事直播平台》后的笔记,是对学习的代码整理的补充,学习过程中整理的github代码地址:

https://github.com/csxiaoyaojianxian/PhpStudy/tree/master/06-swoole

1. php7源码安装

步骤:解压、configure、make、make install,此处略。

2. 切换Mac默认PHP版本为MAMP

注:在实际学习过程中发现有问题,改为非MAMP的PHP

代码语言:txt
复制
$ sudo vi ~/.bash_profile
export PATH="/Applications/MAMP/bin/php/php5.5.38/bin:$PATH"
$ source ~/.bash_profile
$ which php

如果安装了zsh,会出现重新打开terminal窗口时php又跳回自带的PHP,因为terminal在init的时候默认启动执行脚本变为了~/.zshrc,并不会执行~/.bash_profile、~/.bashrc等脚本了,解决方法是修改zsh配置文件:

代码语言:txt
复制
$ vi ~/.zshrc
# 追加
source ~/.bash_profile

3. swoole安装

源码安装

代码语言:txt
复制
$ git clone https://github.com/swoole/swoole-src.git
$ phpize
# ./configure --help
# ./configure --with-php-config=/home/work/study/soft/php/bin/php-config
$ ./configure
$ make
$ make install

配置php.ini

代码语言:txt
复制
extension=swoole.so

验证

代码语言:txt
复制
$ php -m
# 看到swoole即成功

4. redis安装

swoole使用异步redis的前置条件

  1. 安装redis服务
代码语言:txt
复制
$ tar -zxvf redis-5.0.3.tar.gz
$ make
$ make install
$ cd src
$ ./redis-server
# (可选)修改端口等配置
$ vi redis.conf
# 使用客户端连接
$ ./redis-cli
> set csxiaoyao 1
> get csxiaoyao
  1. 安装hiredis库,参考swoole官方文档下载安装包
代码语言:txt
复制
$ make && make install
  1. 重新编译swoole,需要加入 --enable-async-redis
代码语言:txt
复制
$ ./configure --with-php-config=/home/work/study/soft/php/bin/php-config --enable-async-redis
  1. 验证
代码语言:txt
复制
$ php --ri swoole
# 查看是否有 async_redis => enabled
  1. 封装redis类

关注 public function __call($name, $arguments) 方法

代码语言:txt
复制
<?php
   namespace app\common\lib\redis;
   class Predis {
       public $redis = "";
       // 定义单例模式变量
       private static $_instance = null;
       public static function getInstance() {
           if(empty(self::$_instance)) {
               self::$_instance = new self();
           }
           return self::$_instance;
       }
       private function __construct() {
           $this->redis = new \Redis();
           $result = $this->redis->connect(config('redis.host'), config('redis.port'), config('redis.timeOut'));
           if($result === false) {
               throw new \Exception('redis connect error');
           }
       }
       /**
        * set
        * @return bool|string
        */
       public function set($key, $value, $time = 0 ) {
           if(!$key) {
               return '';
           }
           if(is_array($value)) {
               $value = json_encode($value);
           }
           if(!$time) {
               return $this->redis->set($key, $value);
           }
           return $this->redis->setex($key, $time, $value);
       }
       /**
        * get
        * @return bool|string
        */
       public function get($key) {
           if(!$key) {
               return '';
           }
           return $this->redis->get($key);
       }
       /**
        * 返回key中所有成员
        * @return array
        */
       public function sMembers($key) {
           return $this->redis->sMembers($key);
       }
       // 调用前面不存在的方法,如 sAdd()
       public function __call($name, $arguments) {
           //echo $name.PHP_EOL;
           //print_r($arguments);
           if(count($arguments) != 2) {
               return '';
           }
           $this->redis->$name($arguments[0], $arguments[1]);
       }
   }

5. thinkphp框架整合swoole

ws server 可以包含 http server,本案例在 onRequest 中启动thinkphp框架,实现了 ws server 处理 get、post 请求

5.1 使用swoole构建包含http server的websocket服务

详见demo的/script/server/ws.php

代码语言:txt
复制
<?php
class Ws {
    CONST HOST = "0.0.0.0"; // 监听的ip
    CONST PORT = 8811; // 第一个port,用于直播
    CONST CHART_PORT = 8812; // 第二个port,用于聊天室
    public $ws = null;
    public function __construct() {
        $this->ws = new swoole_websocket_server(self::HOST, self::PORT);
        $this->ws->listen(self::HOST, self::CHART_PORT, SWOOLE_SOCK_TCP);
        $this->ws->set(
            [
                'enable_static_handler' => true,
                'document_root' => "/home/work/hdtocs/public/static",
                'worker_num' => 4,
                'task_worker_num' => 4,
            ]
        );
        $this->ws->on("start", [$this, 'onStart']);
        $this->ws->on("open", [$this, 'onOpen']); // ws
        $this->ws->on("message", [$this, 'onMessage']); // ws
        $this->ws->on("workerstart", [$this, 'onWorkerStart']);
        $this->ws->on("request", [$this, 'onRequest']); // request
        $this->ws->on("task", [$this, 'onTask']); // task
        $this->ws->on("finish", [$this, 'onFinish']); // task
        $this->ws->on("close", [$this, 'onClose']);
        $this->ws->start();
    }
    ...
}
new Ws();

5.2 onStart & onWorkerStart 指定进程名并加载框架

onStart 中指定进程名称,应用于reload.sh的服务重启脚本,重启脚本见后面小节。

onWorkerStart 加载thinkphp框架,用于处理请求。

代码语言:txt
复制
public function onStart($server) {
    // 指定进程名称,应用于reload.sh的服务重启脚本
    swoole_set_process_name("live_master");
}
public function onWorkerStart($server,  $worker_id) {
    // 定义应用目录
    define('APP_PATH', __DIR__ . '/../application/');
    // 加载框架文件
    require __DIR__ . '/../thinkphp/start.php';
}

5.3 onRequest 处理 http 请求

onRequest 用于处理http请求,包含过滤请求、处理$_SERVER$_GET$_FILES$_POST并写入日志,最后调用thinkphp框架处理请求。日志落盘方法见后面小节。

代码语言:txt
复制
public function onRequest($request, $response) {
    // 请求过滤,/favicon.ico如果不存在则直接返回404
    if($request->server['request_uri'] == '/favicon.ico') {
        $response->status(404);
        $response->end();
        return;
    }
    // 兼容php的 $_SERVER、$_GET、$_FILES、$_POST
    $_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;
        }
    }
    $_FILES = [];
    if(isset($request->files)) {
        foreach($request->files as $k => $v) {
            $_FILES[$k] = $v;
        }
    }
    $_POST = [];
    if(isset($request->post)) {
        foreach($request->post as $k => $v) {
            $_POST[$k] = $v;
        }
    }
    // 请求写入日志
    $this->writeLog();
    // http_server 不需要写入日志
    $_POST['http_server'] = $this->ws;
    ob_start(); // php打开缓冲区, 控制浏览器cache
    // 执行应用并响应
    try {
        think\Container::get('app', [APP_PATH])->run()->send();
    }catch (\Exception $e) {}
    $res = ob_get_contents(); // 获取缓冲区输出
    ob_end_clean(); // 清空cache
    $response->end($res);
}

5.4 onTask & onFinish 处理 task

onTask中可以处理task请求,例如task发送验证码。

代码语言:txt
复制
public function onTask($serv, $taskId, $workerId, $data) {
    $obj = new app\common\lib\ali\Sms();
    try {
        $response = $obj::sendSms($data['phone'], $data['code']);
    }catch (\Exception $e) {
        echo $e->getMessage();
    }
}

task的异步机制能够提高处理效率,随着task类型的增加,将处理事件直接写在ws.php中不利于维护,利用php的动态函数名进行task分发,将具体task内容封装为Task类,用方法进行区分,使代码结构更加清晰。

代码语言:txt
复制
/**
 * task
 * $serv: swoole server对象
 * $data: 调用task时候传入的参数
 */
public function onTask($serv, $taskId, $workerId, $data) {
    // 分发 task 任务机制,类似于不同的 controller
    $obj = new app\common\lib\task\Task;
    $method = $data['method']; // 执行的函数名
    $flag = $obj->$method($data['data'], $serv);
    return $flag; // 通知worker处理结果
}
// task完成
public function onFinish($serv, $taskId, $data) {
    echo "taskId:{$taskId}\n";
    echo "finish-data-sucess:{$data}\n";
}

task的调用,在controller中调用$_POST['http_server']->task,server对象在上一步存入到了POST中,$_POST['http_server'] = $this->ws;

代码语言:txt
复制
$taskData = [
    'method' => 'sendSms',
    'data' => [
        'phone' => $phoneNum,
        'code' => rand(1000, 9999)
    ]
];
$_POST['http_server']->task($taskData);

5.5 onOpen & onMessage & onClose 处理ws客户端连接

onOpen中向redis的指定key的集合存储ws客户端。

onMessage处理ws消息。

onClose在连接断开时清除redis集合中的ws客户端。

代码语言:txt
复制
// 监听ws连接事件
public function onOpen($ws, $request) {
    // 调用封装的redis类Predis向redis集合中存入连接的客户端信息 fd
    \app\common\lib\redis\Predis::getInstance()->sAdd(config('redis.live_game_key'), $request->fd);
    var_dump($request->fd);
}
// 监听ws消息事件
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"));
}
public function onClose($ws, $fd) {
    // fd del
    \app\common\lib\redis\Predis::getInstance()->sRem(config('redis.live_game_key'), $fd);
    echo "clientid:{$fd}\n";
}

6. 日志落盘

在前面的http请求中,对于每个请求,需要调用$this->writeLog();记录日志,使用swoole的异步文件写入来记录日志。

代码语言:txt
复制
// swoole异步记录日志
public function writeLog() {
    $datas = array_merge(['date' => date("Ymd H:i:s")],$_GET, $_POST, $_SERVER);
    $logs = "";
    foreach($datas as $key => $value) {
        $logs .= $key . ":" . $value . " ";
    }
    swoole_async_writefile(APP_PATH.'../runtime/log/'.date("Ym")."/".date("d")."_access.log", $logs.PHP_EOL, function($filename){
        // todo
    }, FILE_APPEND);
}

7. 服务平滑重启

利用kill -USR1 $pid可以平滑重启服务,不影响处理请求,其中$pid为onStart中swoole_set_process_name("live_master")指定的进程名称,脚本reload.sh如下。

代码语言:txt
复制
echo "loading..."
pid=`pidof live_master`
echo $pid
kill -USR1 $pid
echo "loading success"

8. 服务监控

8.1 监控shell脚本

在项目下建立 monitor/server.php 监控程序对ws http 8811服务进行监控。首先分析监控的shell脚本。

代码语言:txt
复制
$ netstat -anp | grep 8811

得到的结果大致为

代码语言:txt
复制
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
 xxx
 xxx
 xxx

使用 grep LISTEN 在得到的多条结果中过滤掉其他非目标结果,使用wc -l返回得到的结果行数。

代码语言:txt
复制
$ netstat -anp | grep 8811 | grep LISTEN | wc -l

此时的正常返回结果为:

代码语言:txt
复制
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
1

使用2>/dev/null将前两行提示输出到null,只留下结果行数。

代码语言:txt
复制
$ netstat -anp 2>/dev/null | grep 8811 | grep LISTEN | wc -l

最终的正常结果为:

代码语言:txt
复制
1

8.2 使用swoole定时器执行shell脚本

linux的定时任务crontab的精度为分钟,用来进行实时监控太长,需要利用swoole的定时器来调用shell脚本监控,定时器每两秒执行一次。

代码语言:txt
复制
class Server {
    const PORT = 8811;
    public function port() {
        $shell = "netstat -anp 2>/dev/null | grep ". self::PORT . " | grep LISTEN | wc -l";
		// php 执行 shell 脚本
        $result = shell_exec($shell);
        if($result != 1) { // 返回的结果行数不为1
            // todo 告警服务 邮件 短信 ...
            echo date("Ymd H:i:s")."error".PHP_EOL;
        } else {
            echo date("Ymd H:i:s")."succss".PHP_EOL;
        }
    }
}
// nohup
swoole_timer_tick(2000, function($timer_id) {
    (new Server())->port(); // 执行
    echo "time-start".PHP_EOL;
});

8.3 启动监控服务

后台执行监控服务,每两秒输出结果到demo.log文件。

代码语言:txt
复制
$ nohub /xxx/php /xxx/ws.php > /xxx/demo.log &

查看监控服务是否正常启动。

代码语言:txt
复制
$ ps aux | grep monitor/server.php

查看监控日志文件。

代码语言:txt
复制
$ tail -f demo.log

9. 附录1: websocket使用task群发消息的实现

task任务的实现。

代码语言:txt
复制
<?php
namespace app\common\lib\task;
use app\common\lib\redis\Predis;
class Task {
    /**
     * 通过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));
        }
    }
}

controller中调用task 任务

代码语言:txt
复制
$taskData = [
	'method' => 'pushLive',
	'data' => $data
];
$_POST['http_server']->task($taskData);

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • swoole学习笔记
    • 1. php7源码安装
      • 2. 切换Mac默认PHP版本为MAMP
        • 3. swoole安装
          • 4. redis安装
            • 5. thinkphp框架整合swoole
              • 5.1 使用swoole构建包含http server的websocket服务
              • 5.2 onStart & onWorkerStart 指定进程名并加载框架
              • 5.3 onRequest 处理 http 请求
              • 5.4 onTask & onFinish 处理 task
              • 5.5 onOpen & onMessage & onClose 处理ws客户端连接
            • 6. 日志落盘
              • 7. 服务平滑重启
                • 8. 服务监控
                  • 8.1 监控shell脚本
                  • 8.2 使用swoole定时器执行shell脚本
                  • 8.3 启动监控服务
                • 9. 附录1: websocket使用task群发消息的实现
                相关产品与服务
                云数据库 Redis
                腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档