Swoole

环境部署

  1. 版本要求 php >= 7.1 swoole >= 4.4 尽量使用高版本 系统代码不用调整 实现方式
  2. 除sockets与swoole必须的扩展外, 尽量减少扩展降低可能的冲突, 并留意扩展是否可协程化
  3. 若使用HTTP协议, 建议通过代理方式, 用Ngnix转发php到Swoole
  4. 通过修改框架配置文件, 来保持程序始终运行在Swoole环境中
  5. 启动命令"[sudo -u 用户名 [空格分隔的"key=val"环境变量]] php swoole.php [open:指定服务(优于conf.system.open)] [conf:启动配置绝对路径]", []项可选
  6. 安全停止和重启命令 "kill -[15|10] pid"

配置文件

框架配置
//强制使用网络方式
_of.com.net.isCli = false;
//异步请求网络, 注意端口正确
_of.com.net.async = 'http://127.0.0.1:8888/';
//swoole异步任务方式
_of.com.timer.fork.adapter = 'swoole';
启动配置如"conf:/var/www/xxx.php"
<?php
//以下为默认配置
return array(
    //系统配置
    'system' => array(
        //开启服务, 默认支持http的WebSocket协议服务, 绝对路径则为自定义服务(如: /var/www/html/test.php 参考下方demo)
        'open' => 'WebSocket',
        //监听端口, 默认8888
        'port' => 8888,
        //工作进程超过该时间会安全重启, 单位秒, 默认0不限制
        'cycle' => 0,
        //工作进程超过该内存会安全重启, 支持K M G单位(如: 1G), 默认0不限制
        'memory' => 0,
        //健康检查网址, 可用于初始化, 约10s一次(如: http://127.0.0.1:8888/include/of/index.php), 默认""
        'health' => '',
        //路由规则, 格式不含GET的路径后再与GET合并, 参考preg_replace前两参数, 如: {"@^(/[^/]+)/.*$@" : "$1.php?a=1", ...}
        'routes' => array(),
        //自定义初始化列表, 会在每个协程加载框架完成时执行, [框架回调结构, ...]
        'reinit' => array(),
        //自定义常量
        'define' => array(
            //系统根路径, 默认为swoole.php所在的目录
            'ROOT_DIR' => __DIR__,
            //框架根路径
            'OF_DIR' => __DIR__ . '/include/of',
        )
    ),
    //服务配置
    'server' => array(
        //启动协程化(无法修改)
        'enable_coroutine' => true,
        //安全重启最大等待时间(s), 超时强杀
        'max_wait_time' => 86400,
        //协程函数范围, 默认全覆盖, 选项参考
        'hook_flags' => SWOOLE_HOOK_ALL | SWOOLE_HOOK_CURL,
        //服务启动进程数, 默认CPU核数, 框架限制每个进程同时处理1000个请求
        'worker_num' => swoole_cpu_num(),
        //错误日志记录等级
        'log_level' => SWOOLE_LOG_WARNING,
        //协程抢占式调度, 默认false关闭, 若为true对密集运算的协程更友好(需更多CPU资源)
        'enable_preemptive_scheduler' => false,
        //最大数据包
        'package_max_length' => ini_get('post_max_size')的字节数,
        //更多参考TCP/UDPHTTP配置
        ...
    ),
    //替代PHP配置, 如max_execution_time在cli模式下永远是0, 因此设置到这里
    'phpIni' => array(
        //请求响应超时时间, 默认30s, 等同set_time_limit(30), 此值为包含网络等操作的实际执行时间
        'max_execution_time' => 30,
        //协程静态变量最大内存空间
        'memory_limit' => ini_get('memory_limit'),
        //日期时间函数使用的默认时区
        'date.timezone' => date_default_timezone_get()
    )
);

解释说明

Swoole的优势不在于单个请求而在于通信协议与并发任务
单个请求的效率提升基本无感知, 甚至可能因为过多协程的调度导致执行速度更慢
每个Swoole进程可同时响应多个请求(框架限制为1000), Ngnix与其相比需要启动更多php进程
Swoole支持的通信协议与并发任务是切实改变开发思维的壮举, 当然这些也需要资源的加持
脚本超时的设置值为实际时间, 包含数据库, 网络等操作时间, 可能需部分优化
php官网说明如下
set_time_limit()函数和配置指令max_execution_time只影响脚本本身执行的时间。
任何发生在诸如使用system()的系统调用,流操作,数据库操作等脚本的执行时间不包括其中。
在Windows上为实际执行时间。

Swoole与Windows行为一致, 默认情况下是无需设置的
1. 计划任务永不超时
2. 消息队列每条是2小时超时, 超过分秒级应该改造队列
3. 网络请求若调整执行时间, 应设置在ctrl层或入口文件

可以通过两种方式优化
1. 调整启动配置 phpIni.max_execution_time 的默认值
2. 通过 set_time_limit(int) 或 ini_set('max_execution_time', int) 动态设置
因常驻内存的原因, 自动寻类加载的文件仅会运行一次 若文件包含全局调用, 需改到"前后包含若干'_'的init方法中"(不可有参数不可有输出) 或 配置reinit参数
<?php
//全局的, 无参数 => 正常运行
class::init();
//全局的, 无参数 => 正常运行
class::__init_();
//非全局(在方法或类中), 有参数 => 正常运行
function () {class::init(1);}
//全局的, 有参数 => 运行报错
class::init(1);
//全局的, 无参数 => 正常运行, 但不正确, 每次请求都不会判断而执行init, 应把条件放在init中
if (true) {class::init();}
//同时也意味着其它初始化作用的方法名无效 => 无法每次运行, 这类情况可配置reinit参数
class::abcd();
仅实现pcntl_signal和pcntl_signal_dispatch两个方法且仅处理SIGTERM信号
<?php
//安装SIGTERM信号处理器
pcntl_signal(15, function ($signo) {}) :
//恢复linux进程对SIGTERM信号处理
pcntl_signal(15, SIG_DFL);
//检查信号
pcntl_signal_dispatch();
&swoole::data($name, $data = array()) 进程内共享数据
<?php
//引用返回, 多协程初始化数据
$data = &swoole::data('test', array('init' => 0));
//多协程初始化动作(无锁方式实现)
++$data['init'] === 1 ? 初始化动作 : $data['init'] = 1;
swoole::fork($call, $mode = 1, $data = array()) 创建一个"加载框架且不超时"的协程回调建议应用层使用任务方式编程保持兼容性
<?php
/**
 * 运行的协程在收到SIGTERM信号后
 *     1. 定时器会立刻退出
 *     2. SIG_DFL默认处理方式的go协程会等到sleep时退出
 *     3. 其它处理方式会在调用pcntl_signal_dispatch时安全退出
 */
swoole::fork(array(     //符合框架的回调结构
    'asCall' => $call,
    'params' => array($data)
));

//模拟一个含两个必要属性的requ对象, 还可以添加get post等参数
$requ = new stdClass;
$requ->server = array(
    'path_info' => '/index.php',
    'remote_addr' => $serv->getClientInfo($fd)['remote_ip']
);
//指定请求信息回调演示
swoole::fork($call, 1, array(
    'requ' => $requ
));

应用开发

HTTP应用开发
1. 兼容原开发方式, 部署同Apache和Ngnix相同, 启动Swoole后便可应用
2. 建议Swoole仅作为应用服务器, 用Ngnix做代理, 转发动态的php请求
WebSocket应用开发
1. WebSocket连接时先加载入口文件, 需返回至少包含"info"的回调数组, {"init" : 初始回调, "info" : 信息回调, "done" : 结束回调}
2. 回调符合框架回调结构, 每个回调触发时接受一个数组参数, 结构如下
    init({"link" : 连接ID, "resp" : 响应对象})
    info({"link" : 连接ID, "resp" : 响应对象, "info" : 信息对象})
    done({"link" : 连接ID})
3. 响应对象包含两个方法
    push(string | array | 信息对象 $data) 推送信息, 数组参数转会为json, 成功返回true, 失败返回false
    clase() 发送关闭帧并关闭连接, 成功返回true, 失败返回false
4. 支持文本ping pong帧处理, 受open_websocket_ping_frame或open_websocket_pong_frame参数影响, 自动响应数据
    {"ping" : "pong", "Ping" : "Pong", "PING" : "PONG"}
<?php
/**
 * info是必须的, 否则会直接关闭连接, init和done是可选的
 * 假设文件路径 /test.php, 先进行登录验证等操作
 */
 return array(
    //连接成功回调
    'init' => function ($params) {
        $params['resp']->push(print_r($params, true));
    },
    //接受信息回调
    'info' => function ($params) {
        //发送信息
        $params['resp']->push(print_r($params, true));
        //关闭连接
        $params['resp']->close();
    }
);
?>

<script>
//创建连接, 注意端口
var websocket = new WebSocket('ws://127.0.0.1:8888/test.php');
//连接成功回调
websocket.onopen = function (evt) {
    console.log("Connected to WebSocket server.");
    //连接成功后发送信息
    websocket.send('hello');
};
//接受信息回调
websocket.onmessage = function (evt) {
    console.log('Retrieved data from server: ' + evt.data);
};
//关闭连接回调
websocket.onclose = function (evt) {
    console.log("Disconnected");
};
//出现错误回调
websocket.onerror = function (evt, e) {
    console.log('Error occured: ' + evt.data);
};
</script>
自定义服务
1. 自定义服务完整的Swoole服务代码写在独立的文件中并返回服务对象, 如: /var/www/html/demo.php
2. 服务代码中调用$GLOBALS中的"system server phpIni"分别对应配置文件中的数据, 键名可扩展
3. 可以调用swoole类的私有方法和属性, 如: self::request($requ, null);
4. 参考swoole.php中"webSocket"方法的代码能更容易实现大部分自定义服务的逻辑
5. 通过命令行启动自定义服务, 如: php swoole.php "open:/var/www/html/demo.php"
<?php
/**
 * 演示http服务
 * 假设文件路径 /var/www/html/demo.php
 */
$serv = new Swoole\Http\Server('0.0.0.0', $GLOBALS['system']['port'], SWOOLE_PROCESS);
//请求回调
$serv->on('request', function ($requ, $resp) {
    //调用私有方法, 处理请求信息
    self::request($requ, $resp);
});
//返回服务
return $serv;

##############################################################################################################
########服务开发的具体实现思路总体分五步, 客户端连接执行前三步, 接到消息执行第四步, 关闭连接执行第五步########
##############################################################################################################
//第一步拼接一个requ请求对象, 如:
$requ = new stdClass;
$requ->server = array(
    //入口文件
    'path_info' => '/index.php',
    //客户端IP
    'remote_addr' => $serv->getClientInfo($fd)['remote_ip']
);

//第二步调用swoole::request加载入口文件并保存入口文件返回的回调函数, 如:
$call = self::request($requ, null);

//第三步封装一个响应对象, 为应用层提供交互功能, 并把上述数据保存到静态变量中, 如:
$resp = new stdClass;

//第四步在接到客户端消息时, 通过指定请求信息和触发参数的方式创建协程回调, 如:
self::fork($call, 1, array(
    //指定请求头
    'requ' => $requ,
    //触发参数, 内容自定义, 如: {"link" : 连接标识, "resp" : 响应对象, "info" : 客户消息}
    'argv' => array('link' => $frame->fd, 'resp' => $resp, 'info' => $frame)
));

//第五步在关闭连接时清理连接标识对应的数据, 如:
unset($requ, $call, $resp);