图片 2

swoole 在 swoft 中的应用

什么是 Swoft ?

Swoft 是一款基于 Swoole 扩展实现的 PHP 微服务协程框架。Swoft 能像 Go
一样,内置协程网络服务器及常用的协程客户端且常驻内存,不依赖传统的
PHP-FPM。有类似 Go 语言的协程操作方式,有类似 Spring Cloud
框架灵活的注解、强大的全局依赖注入容器、完善的服务治理、灵活强大的
AOP、标准的 PSR 规范实现等等。

Swoft 通过长达三年的积累和方向的探索,把 Swoft 打造成 PHP 界的 Spring
Cloud, 它是 PHP 高性能框架和微服务治理的最佳选择。

date: 2017-12-14 21:34:51title: swoole 在 swoft 中的应用

 Swoole 4.4 正式版已发布,该版本包含大量更新,详细信息如下:

date: 2017-12-22 11:29:15
title: php-msf 源码解读

Swoft v2.0.7

2.0.7 在 2.0.6 上继续扬帆,已在大量的生产业务中使用,得到很多用户的肯定和支持。正式版本我们做了许多改进和优化,拥有了更好的性能。

  • 新增 Http Session 功能组件,提供http会话管理, 支持多种存储驱动
  • 增强 TCP server 请求支持添加全局或对应的方法中间件
  • 增强 Websocket server 消息请求支持添加全局或对应的方法中间件

swoft 官网:

swoft 源码解读:

号外号外, 欢迎大家 star, 我们开发组定了一个 star 1000+
就线下聚一次的小目标

向下不兼容改动

  • PHP官方保持一致, 不再支持PHP7.0 (@matyhtf)
  • 移除Serialize模块,
    在单独的 ext-serialize 扩展中维护.
    废弃原因: 由于PHP内核频繁变更, 导致无法实现稳定可用的模块,
    php serialize相比没有太大差异化定位
  • 移除PostgreSQL模块,在单独的 ext-postgresql 扩展中维护.
    废弃原因: PostgreSQL使用了异步回调方式实现协程调度,
    不符合目前内核协程化的统一规划。另外PostgreSQL目前用户量非常低,
    并且缺少必要的单元测试, 无法保证质量
  • Runtime::enableCoroutine不再会自动兼容协程内外环境, 一旦开启,
    则一切阻塞操作必须在协程内调用 (@matyhtf)
  • 由于引入了全新的协程MySQL客户端驱动, 底层设计更加规范,
    但有一些小的向下不兼容的变化

    • fetch/nextResult优化为按需读取, 会产生IO调度
    • 启动defer特性时, statement发出的的请求,
      需要使用statement->recv接收
    • 启动defer/fetch_mode特性时, 如有未接收完的数据,
      将无法发起新的请求
    • 与异步不同, connected属性不再会实时基于事件更新,
      而是在IO操作失败后更新

php-msf:
https://github.com/pinguo/php-msf

百度脑图 – php-msf 源码解读:
http://naotu.baidu.com/file/cc7b5a49dfed46001d22222b1afa99ba?token=c9628331e99143c2

Http Session

通过 Composer 安装 swoft/session 组件

  • 在项目 composer.json 所在目录执行 composer require swoft/session
  • 将 SwoftHttpSessionSessionMiddleware 中间件加入到全局中间件

在配置文件 app/bean.php 里:

    'httpDispatcher'    => [
        // Add global http middleware
        'middlewares'      => [
            SwoftHttpSessionSessionMiddleware::class,
        ],
    ],

默认是基于本地文件驱动,保存在 runtime/sessions 目录

更在驱动只需要配置对应 handler 类,例如配置 Redis 驱动:

'sessionHandler' => [
    'class'    => RedisHandler::class,
    // Config redis pool
    'redis' => bean('redis.pool')
],

上一篇 blog – swoft 源码解读 反响还不错, 不少同学推荐再加一篇, 讲解一下
swoft 中使用到的 swoole 功能, 帮助大家开启 swoole 的 实战之旅.

废弃警告

  • 将废弃Buffer模块,废弃原因:可替代性强,使用率低,可用PHP字符串、fopen("memory")代替。
  • 将废弃Lock模块,废弃原因:在协程模式下加锁可能存在问题,可使用chan实现协程版本的锁
  • 由于引入了stream_socket_pair协程化, 建议开启hook时,
    如有单独配置需求,
    请使用SWOOLE_HOOK_STREAM_FUNCTION常量而不是SWOOLE_HOOK_STREAM_SELECT

源码解读也做了一段时间了, 总结一下自己的心得:

Websocket消息中间件

  • 全局中间件

配置于 app/bean.php:

    /** @see SwoftWebSocketServerWsMessageDispatcher */
    'wsMsgDispatcher' => [
        'middlewares' => [
            AppWebSocketMiddlewareGlobalWsMiddleware::class
        ],
    ],
  • 作用于控制器的

/**
 * Class HomeController
 *
 * @WsController(middlewares={DemoMiddleware::class})
 */
class TestController
{}

服务器开发涉及到的相关技术领域的知识非常多, 不日积月累打好基础,
是很难真正做好的. 所以我建议:

新特性

  • 新增Library, 使用纯PHP编写内核功能而非C/C++, 提供了以下功能

    • 新增高质量PHP模块CoroutineWaitGroup (@twose)
    • 使用PHP代码实现CURL的hook, 一键使CURL协程化, 目前为实验特性,
      需特别调用Runtime::enableCoroutine(SWOOLE_HOOK_CURL)来开启 (@matyhtf)
      (@Yurunsoft)
    • 使用PHP代码实现exec/shell_exec的协程化
      (#2657) (@Yurunsoft)
    • 开启RuntimeHook时,
      将替换函数array_walkarray_walk_recursive为swoole实现的版本,
      解决原生函数不可重入的问题, 但会造成无法遍历object (@matyhtf)
      (@twose)
  • 新增协程抢占式调度器, 可防止协程占用CPU时间过长导致其它协程饿死,
    通过php.ini配置swoole.enable_preemptive_scheduler = On 开启,
    相关例子详见preemptive_scheduler (@shiguangqi)

  • 新增Timer::list()返回TimerIterator,
    可遍历所有定时器, TimerclearAll清除所有定时器, Timerinfo(int $id)获取定时器信息, Timer::stats()获取全局定时器状态
    (#2498) (@twose)
  • 新增 CoSocket的两个方法getOption 和 setOption (9d13c29) (@matyhtf)
  • 新增 ProcessPool$master_pid 属性和 shutdown方法
    (a1d6eaa) (@matyhtf)
  • 新增ProcessPool的构造方法的第四个参数,
    为true时底层将自动在onWorkerStart回调开启协程
    (8ceb32cd) (@matyhtf)
  • 新增stream_socket_pair协程化支持 (#2546) (@matyhtf)
  • 新增HttpServerstatic_handler_locations设置,
    可以设定静态文件路径 (@matyhtf)
  • 新增CoHttpClient->setBasciAuth方法,
    用于自动发送Authorization头 (#2542) (@hongbshi)
  • 新增 CoHttp2Client->ping方法 (40041f6) (@shiguangqi)
  • 新增hook_flags配置项,用于取代Runtime::enableCoroutine()函数调用
  • 抓住 生命周期, 让代码在你脑海中 跑起来
  • 分析架构, 关键字 分层 边界 隔离

TCP 请求中间件

  • 全局中间件

配置于 app/bean.php:

    /** @see SwoftTcpServerTcpDispatcher */
    'tcpDispatcher' => [
        'middlewares' => [
            AppTcpMiddlewareGlobalTcpMiddleware::class
        ],
    ],
  • 作用于控制器的

/**
 * Class DemoController
 *
 * @TcpController(middlewares={DemoMiddleware::class})
 */
class DemoController
{
    // ....
}

swoole wiki 最好看 3 遍, 包括评论. 第一遍快速过一遍, 形成大致印象;
第二遍边看边敲代码; 第三遍可以选择衍生的开源框架进行实战. swoft
就是不错的选择.

增强

  • 全新的协程MySQL客户端驱动, 底层全面协程化 (#2538) (@twose)

    • 底层使用C++和协程的编程模式(同步阻塞写法, 异步性能)
    • 支持SSL连接 (connect时配置 ['ssl' => true]即可,
      暂不支持证书等配置)
    • 支持超大数据发送 (无上限, 底层自动拼包,
      上限为MySQL服务器配置上限)
    • 支持超大数据接收
    • 支持fetch按行读取 (现在的fetch为按需读取,
      未fetch的数据不会耗费用户内存)
      (#2106)
    • 支持nextResult按需读取 (同上)
    • 客户端close后, 客户端持有的statements自动转为不可用状态,
      避免边界问题
    • 优化掉了一些不必要的内存拷贝(协议解析时)
    • date相关类型小数精度支持
    • 错误代码和信息与PDO/mysqli保持一致
  • CoRedis兼容模式,
    通过$redis->set(['compatibility_mode' => true])开启,
    可使得hmGet/hGetAll/zRange/zRevRange/zRangeByScore/zRevRangeByScore等方法返回结果和phpredis保持一致
    (#2529) (@caohao-php)

  • 默认允许有100K个协程同时存在 (c69d320b) (@twose)
  • 支持bailout机制 (协程内发生致命错误时能正确退出进程)
    (#2579) (@twose)
  • Server发生错误时会根据情况展示友好的400/404/503界面而不是没有任何输出 (@matyhtf)
    (f3f2be9d)
  • Server默认开启异步安全重启特性和超大数据发送的自动协程调度功能
    (#2555) (9d4a4c47) (@matyhtf)
  • ServeronFinish回调支持自动协程环境 (@twose)
  • Http客户端默认开启websocket_mask,
    不再会出现莫名其妙连不上websocket的问题 (c02f4f85) (@twose)
  • 不再允许在协程外使用Channel的调度操作 (519b6043) (@twose)
  • WebSocket握手失败时切断连接 (#2510) (@twose)
  • Linux下父进程异常退出时底层会自动发送信号杀死子进程
    (4b833a3d) (@matyhtf)
  • Socket->recv的数据长度不足时回收末尾无用的内存
    (642a3552) (@twose)
  • 浮点数计算误差优化 (#2572) (@tangl163)
  • 所有内置类都 禁止克隆/禁止序列化/禁止删除底层定义的属性
    (f9c974b8) (@twose)
  • Server->binduid超过UINT32_MAX时会产生警告并返回
  • 兼容PHP7.4 (#2506) (@twose)

一个好的框架, 弄清楚 生命周期架构, 基本就已经到了 熟悉
的状态了, 之后是填充细节和编码熟练了

更多

  • GitHub: 

  • Gitee: 

  • 官网:https://www.swoft.org

  • 文档:

swoole wiki 发展到现在已经 1400+ 页, 确实会有点难啃, 勇敢的少年呀,
加油.

修复

  • 修复ProcessPoolgetProcess问题 (#2522) (@matyhtf)
  • 修复某些特殊情况下异常被忽略的问题(VM陷入了事件循环而没有机会检查异常) (@twose)
  • 修复定时器在进程fork后产生的内存泄漏 (8f3abee7) (@twose)
  • 修复非Linux系统编译时timezone的问题 (#2584) (@devnexen)
  • 修复enable_coroutinetask_enable_coroutine一开一关的问题
    (#2585) (@matyhtf)
  • 修复Http2的trailer方法不输出值为空的头 (#2578) (@twose)
  • 修复CoHttpClient->setCookies在特殊情况下的内存错误
    (#2644) (@Yurunsoft)
  • 修复#2639 (#2656) (@mabu233)
  • 修复arginfo_swoole_process_pool_getProcess (#2658) (@mabu233)
  • 修复static_handler不支持软链接 (@matyhtf)
  • 修复OSX下卡死 (22504dd4) (@matyhtf)
  • 修复启用SSLtask进程使用Server->getClientInfo出错
    (#2639) (@matyhtf)
  • 修复多协程操作同一个Socket的非法操作BUG (#2661) (@twose)

这里再介绍几个次重要的心得:

更新记录

升级提示:

  • SwooleWebSocketServer::push 第四个参数 $finish 在
    swoole 4.4.12 后改为了 int 类型。
  • tcp server 的 TcpServerEvent::CONNECT 事件参数保持跟receive,
    close一致。 $fd, $server 互换位置。

修复(Fixed)

  • 修复 config
    注入时,没有找到值也会使用对应类型的默认值覆盖属性,导致属性默认值被覆盖 d84d50a7
  • 修复 ws server
    中使用message调度时,没有过滤空数据,导致多发出一个响应。避免方法swoft-cloud/swoft#1002 d84d50a7
  • 修复 tcp server
    中使用message调度时,没有过滤空数据,导致多发出一个响应。07a01ba1
  • 修复 独立使用console组件时缺少 swoft/stdlib
    库依赖 c569c81a
  • 修复 ArrayHelper::get 传入key为 integer
    时,报参数错误 a44dcad
  • 修复 console
    渲染使用table,有int值时,计算宽度报类型错误 74a835ab
  • 修复 error
    组件中用户无法自定义设置默认的错误处理级别 4c78aeb
  • 修复 启用和禁用
    组件设置 isEnable() 不生效的问题 da8c51e56
  • 修复 在 cygwin 环境使用 uniqid() 方法必须将第二个参数设置为
    true c7f688f
  • 修复 在 cygwin
    环境不能够设置进程title而导致报错 c466f6a
  • 修复 使用
    http response->delCookie() 无法删除浏览器的cookie数据问题 8eb9241
  • 修复 ws
    server消息调度时,接收到的ext数据不一定是数组导致报错 ff45b35
  • 修复
    日志文件按时间拆分问题c195413
  • 修复
    日志 JSON 格式小问题a3fc6b9
  • 修复 rpc 服务提供者 getList 调用两次问题fd03e71
  • 修复 redis cluster 不支持 auth 参数7a678f
  • 修复 模型查询 json 类型,
    不支持 array 6023a9
  • 修复
    redis multi 操作没有及时是否连接 e5f698
  • 修复 redis
    不支持 expireAtgeoRadius 749241
  • 修复 crontab 时间戳检测偏差问题 eb08a46

更新(Update):

  • 更新 console 在渲染
    help信息之前也会发出事件 ConsoleEvent::SHOW_HELP_BEFORE d3f7bc3
  • 简化和统一 http, ws, tcp, rpc
    server管理命令逻辑 f202c826
  • 更新 ws 和 tcp
    Connection类添加 newFromArray 和 toArray 方法,方便通过第三方存储(redis)时导出信息和恢复连接 a8b0b7c
  • 优化 server 添加统一的 swoole pipe message 事件处理,在 ws, tcp
    中使用swoft事件来处理进程间消息 1c51a8c

增强(Enhancement)

  • 现在 tcp
    请求支持添加全局或对应的方法中间件,流程和使用跟http中间件类似。仅当使用系统调度时有用 6b593877
  • 现在 websocket message
    请求支持添加全局或对应的方法中间件,流程和使用跟http中间件类似。仅当使用系统调度时有用 9739815
  • 事件管理允许设置 destroyAfterFire 在每次事件调度后清理事件中携带的数据 50bf43d3
  • 数据库错误异常新增 code 返回fd306f4
  • 协程文件操作 writeFile 新增写失败异常08c4244
  • RPC
    新增参数验证8646fc5

(文/开源中国)    

swoole 在 swoft 中的应用:

协程调度器?

  • 新增SwooleCoroutineScheduler调度器类作为cli命令行脚本的入口,取代go() + SwooleEvent::wait()的方式
  • 增加SwooleCoroutineRun函数,提供对SwooleCoroutineScheduler的封装
  • go() + SwooleEvent::wait()的运行方式可能被废除
  • 弄明白这个工具擅长干什么, 适合干什么. 这个信息也非常容易获取到,
    工具的文档通常都会显眼标注出来, 可以通过这些 功能/特性, 尝试以点见面
  • 从工程化的角度去看这个项目, 主要和上面的 架构 区分,
    在处理核心业务, 也就是上面的 功能/特性 外, 工程化还涉及到
    安全/测试/编码规范/语言特性 等方面,
    这些也是平时在写业务代码时思考较少并且实践较少的部分
  • 工具的使用, 推荐我现在使用的组合: phpstorm + 百度脑图 +
    Markdown笔记 + blog
  • SwooleServer: swoole2.0 协程 Server

  • SwooleHttpServer: swoole2.0 协程 http Server, 继承自
    SwooleServer

  • SwooleCoroutineClient: 协程客户端, swoole 封装了 tcp / http /
    redis / mysql

  • SwooleCoroutine: 协程工具集, 获取当前协程id,反射调用等能力

  • SwooleProcess: 进程管理模块, 可以在 SwooleServer
    之外扩展更多功能

  • SwooleAsync: 异步文件 IO

  • SwooleTimer: 基于 timerfd + epoll
    实现的异步毫秒定时器,可完美的运行在 EventLoop 中

  • SwooleEvent: 直接操作底层 epoll/kqueue
    事件循环(EventLoop)的接口

  • SwooleLock: 在 PHP 代码中可以很方便地创建一个锁, 用来实现数据同步

  • SwooleTable: 基于共享内存实现的超高性能数据结构

内核

  • 持续的底层代码质量优化工作 (@swoole)
  • 更多的单元测试,
    并使用了基于 webmozart/assert 二次开发而来的断言库 swoole/assert (@twose)
  • 补全内存申请失败检测 (b19bebac) (5a1ddad3) (@matyhtf)
  • 彻底废除Windows支持计划
  • 将协程的一些功能整理划分到SystemScheduler模块, 废除util模块
  • CoHttp2Client底层协程化 (f64874c3) (@matyhtf)
  • 底层全面缓存了开发者注册的函数信息, 调用回调时速度更快 (@twose)

和 php-msf 的渊源等写技术生活相关的 blog 再来和大家八, 直接上菜.

使用 swoole 的 http server 相较 tcp server 还是要简单一些, 只需要关心:

实验性内容

  • 可能在5.0新增的CoServerCoHttpServer
  • CURL Hook(暂时不支持curl_multi

(文/开源中国)    

生命周期 & 架构

官方文档制作了一张非常好的图:
处理请求流程图.
推荐各位同仁, 有闲暇时制作类似的图, 对思维很有的帮助.

根据这张图来思考 生命周期 & 架构, 这里就不赘述了, 这里分析一下 msf
中一些技术点:

  • 协程相关知识
  • msf 中技术点摘录
  • SwooleHttpServer
  • SwooleHttpRequest
  • SwooleHttpResponse

协程

我会用我的方式来讲解, 如果需要深入了解的, 可以看我后面推荐的资源.

类 vs 对象 是一组很重要的概念. 类代表我们对事物的抽象,
这个抽象的能力在我们以后会一直用到, 希望大家有意识的培养这方面的意识,
至少可以起到触类旁通的作用. 对象是 实例化 的类, 是 真正干活的,
我们要讨论的 协程, 就是这样一个 真正干活的 角色.

协程从哪里来, 到哪里去, 它是干什么的?

想一想这几个简单的问题, 也许你对协程的理解就更深刻了, 记住这几个关键词:

  • 产生. 需要有地方来产生协程, 你可能不需要知道细节,
    但是需要知道什么时候发生了
  • 调度. 肯定是有很多协程一起工作的, 所以需要调度, 怎么调度的呢?
  • 销毁. 是否会销毁? 什么时候销毁?

现在, 我们再来看看协程的使用方式对比, 这里注意一下, 我没有用
协程的实现方式对比, 因为很多时候, 需求实际是这样的:

怎么实现我不管, 我选最好用的.

// msf - 单次协程调度
$response = yield $this->getRedisPool('tw')->get('apiCacheForABCoroutine');

// msf - 并发协程调用
$client1 = $this->getObject(Client::class, ['http://www.baidu.com/']);
yield $client1->goDnsLookup();
$client2 = $this->getObject(Client::class, ['http://www.qq.com/']);
yield $client2->goDnsLookup();
$result[] = yield $client1->goGet('/');
$result[] = yield $client2->goGet('/');

大致 是这样的一个等式: 使用协程 = 加上 yield,
所以搞清楚哪些地方需要加上 yield 就好了 — 有阻塞IO的地方, 比如 文件IO,
网络IO(redis/mysql/http) 等.

当然, 大致 就是还有需要注意的地方

  • 协程调度顺序,
    如果不注意, 就可能会退化成同步调用.
  • 调用链: 使用 yield 的调用链上, 都需要加上 yield. 比如下面这样:

function a_test() {
    return yield $this->getRedisPool('tw')->get('apiCacheForABCoroutine');
}
$res = yield a_test(); // 如果不加 yield, 就变成了同步执行

对比一下 swoole2.0 的协程方案:

$server = new SwooleHttpServer("127.0.0.1", 9502, SWOOLE_BASE);

$server->set([
    'worker_num' => 1,
]);

// 需要在协程 server 的异步回调函数中
$server->on('Request', function ($request, $response) {

    $tcpclient = new SwooleCoroutineClient(SWOOLE_SOCK_TCP); // 需要配合使用协程客户端
    $tcpclient->connect('127.0.0.1', 9501,0.5)
    $tcpclient->send("hello worldn");

    $redis = new SwooleCoroutineRedis();
    $redis->connect('127.0.0.1', 6379);
    $redis->setDefer(); // 标注延迟收包, 实现并发调用
    $redis->get('key');

    $mysql = new SwooleCoroutineMySQL();
    $mysql->connect([
        'host' => '127.0.0.1',
        'user' => 'user',
        'password' => 'pass',
        'database' => 'test',
    ]);
    $mysql->setDefer();
    $mysql->query('select sleep(1)');

    $httpclient = new SwooleCoroutineHttpClient('0.0.0.0', 9599);
    $httpclient->setHeaders(['Host' => "api.mp.qq.com"]);
    $httpclient->set([ 'timeout' => 1]);
    $httpclient->setDefer();
    $httpclient->get('/');

    $tcp_res  = $tcpclient->recv();
    $redis_res = $redis->recv();
    $mysql_res = $mysql->recv();
    $http_res  = $httpclient->recv();

    $response->end('Test End');
});
$server->start();

使用 swoole2.0 的协程方案, 好处很明显:

  • 不用加 yield
  • 并发调用不用刻意注意 yield 的顺序了, 使用 defer() 延迟收包即可

但是, 没办法直接用 使用协程 = 加上 yield 这样一个简单的等式了,
上面的例子需要配合使用 swoole 协程 server + swoole 协程 client:

  • server 在异步回调触发时 生成协程
  • client 触发 协程调度
  • 异步回调执行结束时 销毁协程

这就导致了 2 个问题:

  • 不在 swoole 协程 server 的异步回调中怎么办: 使用
    SwooleCoroutine::create() 显式生成协程
  • 需要使用其他的协程 Client 怎么办: 这是 Swoole3 的目标, Swoole2.0
    可以考虑用协程 task 来伪装

这样看起来, 好像 使用协程 = 加上 yield 这样要简单一些? 我不这样认为,
补充一些观点, 大家自己斟酌:

  • 使用 yield 的方式, 基于 php 生成器 + 自己实现 PHP 协程调度器,
    想要用起来不出错, 比如上面 协程调度顺序,
    你还是需要去弄清楚这块的实现
  • Swoole2.0 的原生方式, 理解起来其实更容易, 只需要知道协程
    生成/调度/销毁 的时机就可以用好
  • Swoole2.0 这样异步回调中频繁创建和销毁协程, 是否十分损耗性能? —
    不会的, 实际是一些内存操作, 比进程/对象小很多

想要继续深入了解的同学, 可以继续阅读下面的文章:

php7 下协程实现:
https://segmentfault.com/a/1190000012457145
在PHP中使用协程实现多任务调度 | 鸟哥:
http://www.laruence.com/2015/05/28/3038.html
php-msf doc – 协程原理:
https://pinguo.gitbooks.io/php-msf-docs/chapter-2/2.3-%E5%8D%8F%E7%A8%8B%E5%8E%9F%E7%90%86.html
swoole 底层和 php yield 实现协程, 本质是一样的, 毕竟都是 c 语言实现:
https://github.com/php/php-src/blob/master/Zend/zend_generators.c

有时候 书读百遍其义自见 还是很有道理的. 我希望我使用的
协程生成/调度/销毁 的角度, 能给大家带来帮助.

感谢韩老大, msf 的开发者, swoft 的小伙伴, 在这个过程中,
对我给予的耐心帮助.

先看 http server:

msf 中技术点摘录

msf 在设计上有很多出彩的地方, 很多代码都值得借鉴.

// SwoftServerHttpServerpublic function start(){ // http server $this->server = new SwooleHttpServer($this->httpSetting['host'], $this->httpSetting['port'], $this->httpSetting['model'], $this->httpSetting['type']); // 设置事件监听 $this->server->set($this->setting); $this->server->on('start', [$this, 'onStart']); $this->server->on('workerStart', [$this, 'onWorkerStart']); $this->server->on('managerStart', [$this, 'onManagerStart']); $this->server->on('request', [$this, 'onRequest']); $this->server->on('task', [$this, 'onTask']); $this->server->on('pipeMessage', [$this, 'onPipeMessage']); $this->server->on('finish', [$this, 'onFinish']); // 启动RPC服务 if $this->serverSetting['tcpable'] === 1) { $this->listen = $this->server->listen($this->tcpSetting['host'], $this->tcpSetting['port'], $this->tcpSetting['type']); $tcpSetting = $this->getListenTcpSetting(); $this->listen->set($tcpSetting); $this->listen->on('connect', [$this, 'onConnect']); $this->listen->on('receive', [$this, 'onReceive']); $this->listen->on('close', [$this, 'onClose']); } $this->beforeStart(); $this->server->start();}

请求上下文 Context

这是从 fpm 到 swoole http server 非常重要的概念. fpm 是多进程模式, 虽然
$_POST 等变量, 被称之为超全局变量, 但是, 这些变量在不同 fpm
进程间是隔离的. 但是到了 swoole http server 中, 一个 worker 进程,
会异步处理多个请求, 简单理解就是下面的等式:

fpm worker : http request = 1 : 1
swoole worker : http request = 1 : n

所以, 我们就需要一种新的方式, 来进行 request 间的隔离.

在编程语言里, 有一个专业词汇 scope(作用域). 通常会使用
scope/生命周期, 所以我一直强调的生命周期的概念, 真的很重要.

swoole 本身是实现了隔离的:

$http = new swoole_http_server("127.0.0.1", 9501);
$http->on('request', function ($request, $response) {
    $response->end("<h1>Hello Swoole. #".rand(1000, 9999)."</h1>");
});
$http->start();

msf 在 Context 上还做了一层封装, 让 Context 看起来 为所欲为:

// 你几乎可以用这种方式, 完成任何需要的逻辑
$this->getContext()->xxxModule->xxxModuleFunction();

细节可以查看 src/Helpers/Context.php 文件

使用 swoole server 十分简单:

对象池

对象池这个概念, 大家可能比较陌生, 目的是减少对象的频繁创建与销毁,
以此来提升性能, msf 做了很好的封装, 使用很简单:

// getObject() 就可以了
/** @var DemoModel $demoModel */
$demoModel = $this->getObject(DemoModel::class, [1, 2]);

注意一下这行注释, 加上这个才有代码提示的效果的, 原理可以看我之前的 [blog

  • 聊一聊 php 代码提示]()

对象池的具体代码在 src/Base/Pool.php 下:

  • 底层使用反射来实现对象的动态创建

public function get($class, ...$args)
{
    $poolName = trim($class, '\');

    if (!$poolName) {
        return null;
    }

    $pool     = $this->map[$poolName] ?? null;
    if ($pool == null) {
        $pool = $this->applyNewPool($poolName);
    }

    if ($pool->count()) {
        $obj = $pool->shift();
        $obj->__isConstruct = false;
        return $obj;
    } else {
        // 使用反射
        $reflector         = new ReflectionClass($poolName);
        $obj               = $reflector->newInstanceWithoutConstructor();

        $obj->__useCount   = 0;
        $obj->__genTime    = time();
        $obj->__isConstruct = false;
        $obj->__DSLevel    = Macro::DS_PUBLIC;
        unset($reflector);
        return $obj;
    }
}

感兴趣的同学可以去了解一下
反射,
可以给语言增加很多灵活性

  • 使用 SplStack 来管理对象

private function applyNewPool($poolName)
{
    if (array_key_exists($poolName, $this->map)) {
        throw new Exception('the name is exists in pool map');
    }
    $this->map[$poolName] = new SplStack();

    return $this->map[$poolName];
}

// 管理对象
$pool->push($classInstance);
$obj = $pool->shift();

msf doc 这块的文章非常值得一读, 特别是 php进程内存优化,
对我触动很大:
https://pinguo.gitbooks.io/php-msf-docs/chapter-5/5.6-%E5%AF%B9%E8%B1%A1%E6%B1%A0.html

  • 传入配置 server 配置信息, new 一个 swoole server
  • 设置事件监听, 这一步需要大家对 swoole 的进程模型非常熟悉,
    一定要看懂下面 2 张图
  • 启动服务器

连接池 & 代理

  • 连接池 Pools

连接池的概念就不赘述了, 我们来直接看 msf 中的实现, 代码在
src/Pools/AsynPool.php 下:

public function __construct($config)
{
    $this->callBacks = [];
    $this->commands  = new SplQueue();
    $this->pool      = new SplQueue();
    $this->config    = $config;
}

这里使用的 SplQueue 来管理连接和需要执行的命令. 可以和上面对比一下,
想一想为什么一个使用 SplStack, 一个使用 SplQueue.

  • 代理 Proxy

代理是在连接池的基础上进一步的封装, msf 提供了 2 种封装方式:

  • 主从 master slave
  • 集群 cluster

查看示例 AppControllersRedis 中的代码:

class Redis extends Controller
{
    // Redis连接池读写示例
    public function actionPoolSetGet()
    {
        yield $this->getRedisPool('p1')->set('key1', 'val1');
        $val = yield $this->getRedisPool('p1')->get('key1');

        $this->outputJson($val);
    }

    // Redis代理使用示例(分布式)
    public function actionProxySetGet()
    {
        for ($i = 0; $i <= 100; $i++) {
            yield $this->getRedisProxy('cluster')->set('proxy' . $i, $i);
        }

        $val = yield $this->getRedisProxy('cluster')->get('proxy22');
        $this->outputJson($val);
    }

    // Redis代理使用示例(主从)
    public function actionMaserSlaveSetGet()
    {
        for ($i = 0; $i <= 100; $i++) {
            yield $this->getRedisProxy('master_slave')->set('M' . $i, $i);
        }

        $val = yield $this->getRedisProxy('master_slave')->get('M66');
        $this->outputJson($val);
    }
}

代理就是在连接池的基础上进一步 搞事情. 以 主从 模式为例:

  • 主从策略: 读主库, 写从库

代理做的事情:

  • 判断是读操作还是写操作, 选择相应的库去执行

图片 1进程流程图图片 2进程/线程结构图

公共库

msf 推行 公共库 的做法, 希望不同功能组件可以做到 可插拔,
这一点可以看 laravel 框架和 symfony 框架, 都由框架核心加一个个的 package
组成. 这种思想我是非常推荐的, 但是仔细看 百度脑图 – php-msf
源码解读
这张图的话, 就会发现类与类之间的依赖关系, 分层/边界 做得并不好.
如果看过我之前的 blog –
laravel源码解读 / blog –
yii源码解读,
进行对比就会感受很明显.

但是, 这并不意味着 代码不好, 至少功能正常的代码, 几乎都能算是好代码.
从功能之外建立的 优越感, 更多的是对 美好生活的向往
还可以更好一点.

swoft 在使用 http server 时, 还会根据配置信息, 来判断是否同时新建一个
RPC server, 使用 swoole 的 多端口监听 来实现.

AOP

php AOP 扩展:
http://pecl.php.net/package/aop
PHP-AOP扩展介绍 | rango:
http://rango.swoole.com/archives/83

AOP, 面向切面编程, 韩老大 的 blog – PHP-AOP扩展介绍 |
rango
可以看看.

需不需要了解一个新事物, 先看看这个事物有什么作用:

AOP, 将业务代码和业务无关的代码进行分离, 场景有 日志记录 / 性能统计 /
安全控制 / 事务处理 / 异常处理 / 缓存 等等.

这里引用一段 程序员DD –
翟永超的公众号
文章里的代码, 让大家感受下:

  • 同样是 CRUD, 不使用 AOP

@PostMapping("/delete")
public Map<String, Object> delete(long id, String lang) {
  Map<String, Object> data = new HashMap<String, Object>();

  boolean result = false;
  try {
    // 语言(中英文提示不同)
    Locale local = "zh".equalsIgnoreCase(lang) ? Locale.CHINESE : Locale.ENGLISH;

    result = configService.delete(id, local);

    data.put("code", 0);

  } catch (CheckException e) {
    // 参数等校验出错,这类异常属于已知异常,不需要打印堆栈,返回码为-1
    data.put("code", -1);
    data.put("msg", e.getMessage());
  } catch (Exception e) {
    // 其他未知异常,需要打印堆栈分析用,返回码为99
    log.error(e);

    data.put("code", 99);
    data.put("msg", e.toString());
  }

  data.put("result", result);

  return data;
}
  • 使用 AOP

@PostMapping("/delete")
public ResultBean<Boolean> delete(long id) {
  return new ResultBean<Boolean>(configService.delete(id));
}

代码只用一行, 需要的特性一个没少, 你是不是也想写这样的 CRUD 代码?

再来看 Request 和 Response, 提醒一下, 框架设计的时候, 要记住
规范先行:

配置文件管理

先明确一下配置管理的痛点:

  • 是否支撑热更新, 常驻内存需要考虑
  • 考虑不同环境: dev test production
  • 方便使用

热更其实可以算是常驻内存服务器的整体需求, 目前 php 常用的解决方案是
inotify, 可以参考我之前的 blog – swoft
源码解读 .

msf 使用第三方库来解析处理配置文件, 这里着重提一个 array_merge()
的细节:

$a = ['a' => [
    'a1' => 'a1',
]];

$b = ['a' => [
    'b1' => 'b1',
]];

$arr = array_merge($a, $b); // 注意, array_merge() 并不会循环合并
var_dump($arr);

// 结果
array(1) {
  ["a"]=>
  array(1) {
    ["b1"]=>
    string(2) "b1"
  }
}

msf 中使用配置:

$ids = $this->getConfig()->get('params.mock_ids', []);

// 对比一下 laravel
$ids = cofnig('params.mock_ids', []);

看起来 laravel 中要简单一些, 其实是通过 composer autoload 来加载函数,
这个函数对实际的操作包装了一层. 至于要不要这样做, 就看自己需求了.

PSR-7: HTTP message interfaces

写在最后

msf 最复杂的部分在 服务启动阶段, 继承也很长:

Child -> Server -> HttpServer -> MSFServer -> AppServer,
有兴趣可以挑战一下.

另外一个比较难的点, 是 MongoDbTask 实现原理.

msf 还封装了很多有用的功能, RPC / 消息队列 / restful,
大家根据文档自己探索即可.

SwooleHttpRequest

phper 比较熟悉的应该是 $_GET $_POST $_COOKIE $_FILES $_SERVER
这些全局变量, 这些在 swoole 中都得到了支持, 并且提供了更多方便的功能:

// SwooleHttpRequest $request$request->get(); // -> $_GET$request->post(); // -> $_POST$request->cookie(); // -> $_COOKIE$request->files(); // -> $_FILES$request->server(); // -> $_SERVER// 更方便的方法$request->header(); // 原生 php 需要从 $_SERVER 中取$request->rawContent(); // 获取原始的POST包体

这里强调一下 $request->rawContent(), phper 可能用 $_POST 比较 6,
导致一些知识不知道: post 的数据的格式. 因为这个知识, 所以 $_POST
不是所有时候都能取到数据的, 大家可以网上查找资料, 或者自己使用 postman
这样的工具自己测试验证一下. 在 $_POST 取不到数据的情况下, 会这样处理:

$post = file_get_content('php://input');

$request->rawContent() 和这个等价的.

swoft 封装 Request 对象的方法, 和主流框架差不多, 以 laravel
为例(实际使用 symfony 的方法):

// SymfonyRequest::createFromGlobals()public static function createFromGlobals(){ // With the php's bug #66606, the php's built-in web server // stores the Content-Type and Content-Length header values in // HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH fields. $server = $_SERVER; if ('cli-server' === PHP_SAPI) { if (array_key_exists('HTTP_CONTENT_LENGTH', $_SERVER)) { $server['CONTENT_LENGTH'] = $_SERVER['HTTP_CONTENT_LENGTH']; } if (array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) { $server['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE']; } } $request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $server); // xglobal, if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded') && in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH')) ) { parse_str($request->getContent; $request->request = new ParameterBag; } return $request;}

SwooleHttpResponse

SwooleHttpResponse 也是支持常见功能:

// SwooleHttpResponse $response$response->header($key, $value); // -> header("$key: $valu", $httpCode)$response->cookie(); // -> setcookie()$response->status(); // http 状态码

当然, swoole 还提供了常用的功能:

$response->sendfile(); // 给客户端发送文件$response->gzip(); // nginx + fpm 的场景, nginx 处理掉了这个$response->end(); // 返回数据给客户端$response->write(); // 分段传输数据, 最后调用 end() 表示数据传输结束

phper 注意下这里的 write()end(), 这里有一个 http chunk 的知识点.
需要返回大量数据给客户端时, 需要分段进行发送. 所以先用 write()
发送数据, 最后用 end() 表示结束. 数据量不大时, 直接调用 end
返回就可以了.

在框架具体实现上, 和上面一样, laravel 依旧用的 SymfonyResponse, swoft
也是实现 PSR-7 定义的接口, 对 SwooleHttpResponse 进行封装.

swoft 使用 SwooleServer 来实现 RPC 服务, 其实在上面的多端口监听,
也是为了开启 RPC 服务. 注意一下单独启用中回调函数的区别:

// SwoftServerRpcServerpublic function start(){ // rpc server $this->server = new Server($this->tcpSetting['host'], $this->tcpSetting['port'], $this->tcpSetting['model'], $this->tcpSetting['type']); // 设置回调函数 $listenSetting = $this->getListenTcpSetting(); $setting = array_merge($this->setting, $listenSetting); $this->server->set; $this->server->on('start', [$this, 'onStart']); $this->server->on('workerStart', [$this, 'onWorkerStart']); $this->server->on('managerStart', [$this, 'onManagerStart']); $this->server->on('task', [$this, 'onTask']); $this->server->on('finish', [$this, 'onFinish']); $this->server->on('connect', [$this, 'onConnect']); $this->server->on('receive', [$this, 'onReceive']); $this->server->on('pipeMessage', [$this, 'onPipeMessage']); // 接收管道信息时触发的回调函数 $this->server->on('close', [$this, 'onClose']); // before start $this->beforeStart(); $this->server->start();}

swoole 自带的协程的客户端, swoft 都封装进了连接池, 用来提高性能. 同时,
为了业务使用方便, 既有协程连接, 也有同步连接, 方便业务使用时无缝切换.

同步/协程连接的实现代码:

// RedisConnect -> 使用 swoole 协程客户端public function createConnect(){ // 连接信息 $timeout = $this->connectPool->getTimeout(); $address = $this->connectPool->getConnectAddress(); list($host, $port) = explode(":", $address); // 创建连接 $redis = new SwooleCoroutineRedis(); $result = $redis->connect($host, $port, $timeout); if ($result == false) { App::error("redis连接失败,host=" . $host . " port=" . $port . " timeout=" . $timeout); return; } $this->connect = $redis;}// SyncRedisConnect -> 使用 Redis 同步客户端public function createConnect(){ // 连接信息 $timeout = $this->connectPool->getTimeout(); $address = $this->connectPool->getConnectAddress(); list($host, $port) = explode(":", $address); // 初始化连接 $redis = new Redis(); $redis->connect($host, $port, $timeout); $this->connect = $redis;}

swoft 中实现连接池的代码在 src/Pool 下实现, 由三部分组成:

  • Connect: 即上面代码中的连接
  • Balancer: 负载均衡器, 目前实现了 随机/轮询 2 种方式
  • Pool: 连接池, 调用 Balancer, 返回 Connect

详细内容可以参考之前的 blog – swoft 源码解读

作为首个使用 Swoole2.0 原生协程的框架, swoft
希望将协程的能力扩展到框架的核心设计中. 使用 SwoftBaseCoroutine
进行封装, 方便整个应用中使用:

public static function id(){ $cid = SwCoroutine::getuid(); // swoole 协程 $context = ApplicationContext::getContext(); if ($context == ApplicationContext::WORKER || $cid !== -1) { return $cid; } if ($context == ApplicationContext::TASK) { return Task::getId(); } if($context == ApplicationContext::CONSOLE){ return Console::id(); } return Process::getId();}

如同这段代码所示, Swoft 希望将方便易用的协程的能力, 扩展到
Console/Worker/Task/Process 等等不同的应用场景中

原生的 call_user_func() / call_user_func_array() 中无法使用协程
client, 所以 swoole 在协程组件中也封装的了相应的实现, swoft
中也有使用到, 请自行阅读源码.

进程管理模块, 适合处理和 Server 比较独立的常驻进程任务, 在 swoft 中,
在以下场景中使用到:

  • 协程定时器 CronTimerProcess
  • 协程执行命令 CronExecProcess
  • 热更新进程 ReloadProcess

swoft 使用 SwoftProcessSwooleProcess 进行了封装:

// SwoftProcesspublic static function create( AbstractServer $server, string $processName, string $processClassName) { ... // 创建进程 $process = new SwooleProcess(function (SwooleProcess $process) use ($processClass, $processName) { // reload BeanFactory::reload(); $initApplicationContext = new InitApplicationContext(); $initApplicationContext->init(); App::trigger(AppEvent::BEFORE_PROCESS, null, $processName, $process, null); PhpHelper::call([$processClass, 'run'], [$process]); App::trigger(AppEvent::AFTER_PROCESS); }, $iout, $pipe); // 启动 SwooleProcess 并绑定回调函数即可 return $process;}

swoft 在日志场景下使用 SwooleAsync 来提高性能,
同时保留了原有的同步方式, 方便进行切换

// SwoftLogFileHandlerprivate function aysncWrite(string $logFile, string $messageText){ while  { $result = SwooleAsync::writeFile($logFile, $messageText, null, FILE_APPEND); // 使用起来很简单 if ($result == true) { break; } }}

服务器出于性能考虑, 通常都是 常驻内存 的, 传统的 php-fpm 也是,
修改了配置需要 reload 服务器才能生效. 也因为此, 服务器领域出现了新的需求
热更新. swoole 在进程管理上已经做了很多优化, 这里摘抄部分 wiki
内容:

Swoole提供了柔性终止/重启的机制SIGTERM: 向主进程/管理进程发送此信号服务器将安全终止SIGUSR1: 向主进程/管理进程发送SIGUSR1信号,将平稳地restart所有worker进程

目前大家采用的, 比较常见的方案, 是基于 Linux Inotify 特性,
通过监测文件变更来触发 swoole server reload. PHP 中有 Inotify 扩展,
方便使用, 具体实现在 SwoftBaseInotify 中:

public function run(){ $inotify = inotify_init(); // 设置为非阻塞 stream_set_blocking($inotify, 0); $tempFiles = []; $iterator = new RecursiveDirectoryIterator($this->watchDir); $files = new RecursiveIteratorIterator($iterator); foreach ($files as $file) { $path = dirname; // 只监听目录 if (!isset($tempFiles[$path])) { $wd = inotify_add_watch($inotify, $path, IN_MODIFY | IN_CREATE | IN_IGNORED | IN_DELETE); $tempFiles[$path] = $wd; $this->watchFiles[$wd] = $path; } } // swoole Event add $this->addSwooleEvent;}private function addSwooleEvent{ // swoole Event add Event::add($inotify, function  { // 使用 SwooleEvent // 读取有事件变化的文件 $events = inotify_read; if  { $this->reloadFiles($inotify, $events); } }, null, SWOOLE_EVENT_READ);}

swoft 在 CircuitBreaker 中的 HalfOpenState 使用到了,
并且这块的实现比较复杂, 推荐阅读源码:

// CircuitBreakerpublic function init(){ // 状态初始化 $this->circuitState = new CloseState; $this->halfOpenLock = new SwooleLock(SWOOLE_MUTEX); // 初始化互斥锁}// HalfOpenStatepublic function doCall($callback, $params = [], $fallback = null){ // 加锁 $lock = $this->circuitBreaker->getHalfOpenLock(); $lock->lock(); list($class ,$method) = $callback; .... // 释放锁 $lock->unlock(); ...}

锁的使用, 难点主要在了解各种不同锁使用的场景, 目前 swoole 支持:

  • 文件锁 SWOOLE_FILELOCK
  • 读写锁 SWOOLE_RWLOCK
  • 信号量 SWOOLE_SEM
  • 互斥锁 SWOOLE_MUTEX
  • 自旋锁 SWOOLE_SPINLOCK

定时器基本都会使用到, phper 用的比较多的应该是 crontab 了. 基于这个考虑,
swoft 对 Timer 进行了封装, 方便 phper 用 熟悉的姿势 继续使用.

swoft 对 SwooleTimer 进行了简单的封装, 代码在 BaseTimer 中:

// 设置定时器public function addTickTimer(string $name, int $time, $callback, $params = []){ array_unshift($params, $name, $callback); $tid = SwooleTimer::tick($time, [$this, 'timerCallback'], $params); $this->timers[$name][$tid] = $tid; return $tid;}// 清除定时器public function clearTimerByName(string $name){ if (!isset($this->timers[$name])) { return true; } foreach ($this->timers[$name] as $tid => $tidVal) { SwooleTimer::clear; } unset($this->timers[$name]); return true;}

SwooleTable 是在内存中开辟一块区域,
实现类似关系型数据库表这样的数据结构, 关于 SwooleTable 的实现原理,
rango 写过专门的文章 swoole_table 实现原理剖析, 推荐阅读.

SwooleTable 在使用上需要注意以下几点:

  • 类似关系型数据库, 需要提前定义好 表结构
  • 需要预先判断数据的大小
  • 注意内存, swoole 会更根据上面 2 个定义, 在调用
    SwooleTable->create() 时分配掉这些内存

swoft 中则是使用这一功能, 来实现 crontab 方式的任务调度:

private $originTable;private $runTimeTable;private $originStruct = [ 'rule' => [SwooleTable::TYPE_STRING, 100], 'taskClass' => [SwooleTable::TYPE_STRING, 255], 'taskMethod' => [SwooleTable::TYPE_STRING, 255], 'add_time' => [SwooleTable::TYPE_STRING, 11],];private $runTimeStruct = [ 'taskClass' => [SwooleTable::TYPE_STRING, 255], 'taskMethod' => [SwooleTable::TYPE_STRING, 255], 'minte' => [SwooleTable::TYPE_STRING, 20], 'sec' => [SwooleTable::TYPE_STRING, 20], 'runStatus' => [SwooleTABLE::TYPE_INT, 4],];// 使用 SwooleTableprivate function createOriginTable(): bool{ $this->setOriginTable(new SwooleTable('origin', self::TABLE_SIZE, $this->originStruct)); return $this->getOriginTable()->create();}

老生常谈了, 很多人吐槽 swoole 坑, 文档不好. 说句实话,
要敢于直面自己服务器开发能力不足的现实. 我经常提的一句话:

要把 swoole 的 wiki 看 3 遍.

写这篇 blog 的初衷是给大家介绍一下 swoole 在 swoft 中的应用场景,
帮助大家尝试进行 swoole 落地. 希望这篇 blog 能对你有所帮助,
也希望你能多多关注 swoole 社区, 关注 swoft 框架,
能感受到服务器开发带来的乐趣.