匿名函数
匿名函数(Anonymous functions),也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数。最经常用作回调函数 callable参数的值。
匿名函数目前是通过 Closure 类来实现的。
闭包可以从父作用域中继承变量。 任何此类变量都应该用 use
语言结构传递进去。例如:
<?php
$message = 'hello';
/* 继承 $message */
$example = function () use ($message) {
var_dump($message);
};
继承之后的参数,是按值传递的,对它的修改是不影响原变量的,如果需要,可以通过引用传递参数,或者在函数代码块内使用 global声明全局变量进行使用。
在类的方法中使用匿名函数,5.4以上的版本无需使用use引入this , 直接可以在匿名函数中使用this,直接可以在匿名函数中使用this,直接可以在匿名函数中使用this来调用当前对象的方法。在swoole编程中,可以利用此特性减少$serv对象的use引入传递。
如果希望在闭包函数中修改外部变量,可以在use时为变量增加&引用符号即可。注意对象类型不需要加&,因为在PHP中对象默认就是传引用而非传值。
普通函数不能使用use,子函数获取父函数的变量,只能通过匿名函数实现,use只能传递所在作用域的变量;
代码语言:javascript复制$sortFun = function ($a, $b) use ($key) {}
PHP对象可以直接通过指定一个属性进行赋值来给对象创建一个新属性。
相关文章:https://nicen.cn/thread.html
Swoole基础
必须每个进程单独创建 Redis、MySQL、PDO 连接,其他的存储客户端同样也是如此。原因是如果共用 1 个连接,那么返回的结果无法保证被哪个进程处理,持有连接的进程理论上都可以对这个连接进行读写,这样数据就发生错乱了。
在 Swoole 内,无法 通过 _GET/_POST/_REQUEST/_SESSION/_COOKIE/_SERVER 等
1.swoole
Swoole的进程不同于平常的PHP脚本,它是常驻内存的。这意味着程序是一直运行,变量也可以一直存在。例如Swoole提供的异步Websocket服务器。
Swoole定时器:https://wiki.swoole.com/#/timer?id=定时器-timer
Swoole内存管理:https://wiki.swoole.com/#/getting_started/notice?id=内存管理
2.swoole_server中对象的4层生命周期
- 程序全局期
- 进程全局期
- 会话期
- 请求期
2.1 程序全局期
在swoole_server->start之前就创建好的对象,我们称之为程序全局生命周期。这些变量在程序启动后就会一直存在,直到整个程序结束运行才会销毁。
变量在Worker进程内对这些对象进行写操作时,会自动从共享内存中分离,变为进程全局对象。进程操作的对象是原对象的拷贝,对该对象的操作不影响原对象;
注意
程序全局期include/require的代码,必须在整个程序shutdown时才会释放,reload无效
2.2 进程全局期
swoole拥有进程生命周期控制的机制,一个Worker子进程处理的请求数超过max_request配置后,就会自动销毁。Worker进程启动后创建的对象(onWorkerStart中创建的对象),在这个子进程存活周期之内,是常驻内存的。onConnect/onReceive/onClose 中都可以去访问它。
提示
进程全局对象所占用的内存是在当前子进程内存堆的,并非共享内存。对此对象的修改仅在当前Worker进程中有效 进程期include/require的文件,在reload后就会重新加载
相关文档:https://www.easyswoole.com/NoobCourse/Swoole/lifecycle.html
2.3 会话期
onConnect到onClose是一次TCP的会话周期,http keep-alive时,一个连接可能会有多个request。 http是无状态的,一个用户可能也不止一个连接,可以通过创建一个session来关联同一个用户的不同请求。
2.4 请求期
请求期就是指一个完整的请求发来,也就是onReceive收到请求开始处理,直到返回结果发送response。这个周期所创建的对象,会在请求完成后销毁。
swoole中请求期对象与普通PHP程序中的对象就是一样的。请求到来时创建,请求结束后销毁。
提示
在Swoole中,一个work进程处理完请求后并不会销毁(甚至可能同时处理多个请求),所以务必要明确你创建的变量的生命周期,以防止出现逻辑上的问题。
3.进程隔离
原因就是全局变量在不同的进程,内存空间是隔离的,所以修改全局变量的值是无效的。
所以使用 Swoole 开发 Server 程序需要了解进程隔离问题,SwooleServer 程序的不同 Worker 进程之间是隔离的,在编程时操作全局变量、定时器、事件监听,仅在当前进程内有效。
不同的进程中 PHP 变量不是共享,即使是全局变量,在 A 进程内修改了它的值,在 B 进程内是无效的
如果需要在不同的 Worker 进程内共享数据,可以用 Redis、MySQL、文件、SwooleTable、APCu、shmget 等工具实现
不同进程的文件句柄是隔离的,所以在 A 进程创建的 Socket 连接或打开的文件,在 B 进程内是无效,即使是将它的 fd 发送到 B 进程也是不可用的
4.start()干了些什么
- start()运行之后会创建Master 进程 Manager 进程 serv->worker_num 个 Worker 进程。
- 启动失败会立即返回 false,启动成功后将进入事件循环,等待客户端连接请求。start 方法之后的代码不会执行。
- 服务器关闭后,start 函数返回 true,并继续向下执行
- 设置了 task_worker_num 会增加相应数量的 Task 进程
- 方法列表中 start 之前的方法仅可在 start 调用前使用,在 start 之后的方法仅可在 onWorkerStart、onReceive 等事件回调函数中使用
5.运行时进程
- Master 主进程,主进程内有多个 Reactor 线程,基于 epoll/kqueue 进行网络事件轮询。收到数据后转发到 Worker 进程去处理;
- Manager 进程,对所有 Worker 进程进行管理,Worker 进程生命周期结束或者发生异常时自动回收,并创建新的 Worker 进程;
- Worker 进程,对收到的数据进行处理,包括协议解析和响应请求;
- Reactor 线程,是在 Master 进程中创建的线程,负责维护客户端 TCP 连接、处理网络 IO、处理协议、收发数据,不执行任何 PHP 代码,将 TCP 客户端发来的数据缓冲、拼接、拆分成完整的一个请求数据包;
- Task 进程以及Task Worker进程,是独立于worker进程当中的一个工作进程,用于处理一些耗时较长的逻辑,这些逻辑如果在task 进程当中处理时并不会影响worker 进程处理来自客户端的请求,由此大大提高了swoole处理并发的能力
假设 Server 就是一个工厂,那 Reactor 就是销售,接受客户订单。而 Worker 就是工人,当销售接到订单后,Worker 去工作生产出客户要的东西。而 TaskWorker 可以理解为行政人员,可以帮助 Worker 干些杂事,让 Worker 专心工作。
6.Server 的两种运行模式
SWOOLE_PROCESS 模式的 Server 所有客户端的 TCP 连接都是和主进程建立的,内部实现比较复杂,用了大量的进程间通信、进程管理机制。适合业务逻辑非常复杂的场景。Swoole 提供了完善的进程管理、内存保护机制。 在业务逻辑非常复杂的情况下,也可以长期稳定运行。
SWOOLE_BASE 这种模式就是传统的异步非阻塞 Server。与 Nginx 和 Node.js 等程序是完全一致的。worker_num 参数对于 BASE 模式仍然有效,会启动多个 Worker 进程。当有 TCP 连接请求进来的时候,所有的 Worker 进程去争抢这一个连接,并最终会有一个 worker 进程成功直接和客户端建立 TCP 连接,之后这个连接的所有数据收发直接和这个 worker 通讯,不经过主进程的 Reactor 线程转发。
高性能定时器
官方文档:https://wiki.swoole.com/#/timer
webscoket
wss启用,https://wiki.swoole.com/#/server/methods?id=__construct
参考:https://my.oschina.net/u/125977/blog/1816423
提示
websocket类继承与tcp/udp服务器类,支持的如下事件:https://wiki.swoole.com/#/server/events
1.创建websocket服务器(异步)
代码语言:javascript复制SwooleWebsocketServer::__construct(string $host = '0.0.0.0', int $port = 0, int $mode = SWOOLE_PROCESS, int $sockType = SWOOLE_SOCK_TCP): SwooleServer
构造方法
https://wiki.swoole.com/#/server/methods?id=__construct
代码语言:javascript复制<?php
/*创建websocket服务器对象,监听0.0.0.0:9501端口,开启SSL隧道*/
$ws = new swoole_websocket_server("0.0.0.0", 9501, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
/*配置参数*/
$ws ->set([
'max_conn'=>1000, /*最多连接数量。*/
'task_worker_num' => 2,/*TaskWorker 进程数量。*/
'daemonize' => false, /*守护进程化。*/
/*配置SSL证书和密钥路径*/
'ssl_cert_file' => "/etc/nginx/cert/socket.yuhal.com.pem",
'ssl_key_file' => "/etc/nginx/cert/socket.yuhal.com.key"
]);
/*监听WebSocket连接打开事件*/
$ws->on('open', function ($ws, $request) {
echo "client-{$request->fd} is openn";
});
/*监听WebSocket消息事件*/
$ws->on('message', function ($ws, $frame) {
echo "Message: {$frame->data}n";
$ws->push($frame->fd, "server: {$frame->data}");
});
/*监听WebSocket连接关闭事件*/
$ws->on('close', function ($ws, $fd) {
echo "client-{$fd} is closedn";
});
$ws->start();
2.swoole类短名介绍
例如swoole_websocket_server
等同于SwooleWebsocketServer:https://wiki.swoole.com/#/other/alias?id=类短别名映射关系
3.task_worker_num
配置此参数后将会启用 task 功能。所以 Server 务必要注册 onTask、onFinish 2 个事件回调函数。如果没有注册,服务器程序将无法启动。
4.swoole的wss配置了一晚上,怎么都不行,还是Nginx好
代码语言:javascript复制location /websocket {
proxy_pass http://s.nicen.cn:5703;
proxy_http_version 1.1;
proxy_read_timeout 3600s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
使用腾讯云CDN时,进行websokect反向代理时,由于cdn链接最多保持10s,将会导致websokect中断。
5.事件执行顺序
- 所有事件回调均在 $server->start 后发生
- 服务器关闭程序终止时最后一次事件是 onShutdown
- 服务器启动成功后,onStart/onManagerStart/onWorkerStart 会在不同的进程内并发执行
- onReceive/onConnect/onClose 在 Worker 进程中触发
- Worker/Task 进程启动 / 结束时会分别调用一次 onWorkerStart/onWorkerStop
- onTask 事件仅在 task 进程中发生
- onFinish 事件仅在 worker 进程中发生
- onStart/onManagerStart/onWorkerStart 3 个事件的执行顺序是不确定的
5.其他
错误码大全:https://wiki.swoole.com/#/other/errno
函数别名汇总:https://wiki.swoole.com/#/other/alias
协程入门
参考
https://wiki.swoole.com/#/coroutine
1.什么是协程
协程就是《操作系统原理》所说的用户态线程,协程内的代码被阻塞时会自动切换运行其他协程。
与常说的线程相比,协程在用户态,调度由程序自身完成,线程在系统态,调度由操作系统完成。
举例
假设某个场景我们不需要考虑回写数据库时失败的可能,那么进行数据库操作时,可以先给用户发送响应,回写数据交给协程去完成。相较于传统的同步代码,速度就更快了。
2.使用须知
- Swoole4 或更高版本拥有高可用性的内置协程,可以使用完全同步的代码来实现异步 IO,PHP 代码没有任何额外的关键字,底层会自动进行协程调度。
- 所有的协程必须在协程容器里面创建,Swoole 程序启动的时候大部分情况会自动创建协程容器,Server的
enable_coroutine
控制事件回调是否自动创建协程。 - 防止多协程同时操作数据,导致运行混乱,协程内部禁止使用全局变量,协程使用 use 关键字引入外部变量到当前作用域禁止使用引用,协程之间通讯必须使用 Channel。
- 在协程编程中可直接使用 try/catch 处理异常。但必须在协程内捕获,不得跨协程捕获异常。当协程退出时,发现有未捕获的异常,将引起致命错误。
- 在一键协程化里面使用原生 PHP 提供的方法,它们的超时时间受 default_socket_timeout 配置影响,开发者可以通过 ini_set('default_socket_timeout', 60) 这样来单独设置它,它的默认值是 60。
- 使用 Coroutine::create 或 go 方法创建协程 ,在创建的协程中才能使用协程 API,而协程必须创建在协程容器里面。
- 在一个协程中可以使用 go 嵌套创建新的协程。因为 Swoole 的协程是单进程单线程模型,使用 go 创建的子协程会优先执行,子协程执行完毕或挂起时,将重新回到父协程向下执行代码,如果子协程挂起后,父协程退出,不影响子协程的执行,
- Swoole 的协程是单进程单线程模型。
3.协程HTTP服务端
- 对连接的处理是在单独的子协程中完成,客户端连接的 Connect、Request、Response、Close 是完全串行的。
- 监听的地址若是本地 UNIXSocket 则应以形如 unix://tmp/your_file.sock 的格式填写 。
3.1 websocket处理流程
- $ws->upgrade():向客户端发送 WebSocket 握手消息
- while(true) 循环处理消息的接收和发送
- $ws->recv() 接收 WebSocket 消息帧
- $ws->push() 向对端发送数据帧
- $ws->close() 关闭连接
4.协程设置
协程设置,设置协程相关选项。参考:https://wiki.swoole.com/#/coroutine/coroutine?id=set
代码语言:javascript复制<?php
SwooleCoroutine::set(array $options);
5.退出协程
5.1 defer
defer 用于资源的释放,会在协程关闭之前 (即协程函数执行完毕时) 进行调用,就算抛出了异常,已注册的 defer 也会被执行。
需要注意的是,它的调用顺序是逆序的(先进后出), 也就是先注册 defer 的后执行,先进后出。逆序符合资源释放的正确逻辑,后申请的资源可能是基于先申请的资源的,如先释放先申请的资源,后申请的资源可能就难以释放。
5.2 主动退出
在 Swoole 低版本中,协程中使用 exit 强行退出脚本会导致内存错误导致不可预期的结果或 coredump,在 Swoole 服务中使用 exit 会使整个服务进程退出且内部的协程全部异常终止导致严重问题,Swoole 长期以来一直禁止开发者使用 exit,但开发者可以使用抛出异常这种非常规的方式,在顶层 catch 来实现和 exit 相同的退出逻辑。
Swoole v4.1.0 版本及以上直接支持了在协程、服务事件循环中使用 PHP 的 exit,此时底层会自动抛出一个可捕获的 SwooleExitException,开发者可以在需要的位置捕获并实现与原生 PHP 一样的退出逻辑。
5.3 cancel()
可以用于取消某个协程,但不能对当前协程发起取消操作。协程被取消后触发defer回调,然后运行结束。
目前基本支持了绝大部分的协程 API 的取消,包括:
- socket
- AsyncIO (fread, gethostbyname ...)
- sleep
- waitSignal
- wait/waitpid
- waitEvent
- Co::suspend/Co::yield
- channel
- native curl (SWOOLE_HOOK_NATIVE_CURL)
有两个不可中断的场景
- 被 CPU 中断调度器强制切换的协程
- 文件锁操作期间
相关说明:https://zhuanlan.zhihu.com/p/378795262
6.协程化API
系统API:https://wiki.swoole.com/#/coroutine/system
协程通信:https://wiki.swoole.com/#/coroutine/channel
Redis消息订阅、发布
Redis订阅发布:https://www.runoob.com/redis/redis-pub-sub.html
1.在swoole中应用
代码语言:javascript复制<?php
use SwooleHttpRequest;
use SwooleHttpResponse;
use SwooleWebSocketCloseFrame;
use SwooleCoroutineHttpServer;
use function SwooleCoroutinerun;
use SwooleCoroutine;
/*
* 设置协程运行相关的参数
* */
Co::set([
'socket_timeout' => -1, //tcp超时
'hook_flags' => SWOOLE_HOOK_ALL //HOOK函数范围
]);
/*
* 创建协程容器
* */
run(function () {
$server = new Server('0.0.0.0', 5705, false);
$server->handle('/ws', function (Request $request, Response $ws) {
/*websocket协议*/
$ws->upgrade();
/*
* 创建协程,并获取Guid
* */
$ws->Gid = go(function () use ($ws) {
$redis = new Redis();
$redis->connect("/tmp/redis.sock");
$redis->setOption(3, -1);
/*
* 协程退出时清理
* */
defer(function () use ($redis) {
$redis->rawCommand("UNSUBSCRIBE", "TEST");
$redis->close();
});
/*
* close时,将抛出异常
* */
try {
$redis->subscribe(['TEST'], function ($redis, $chan, $msg) use ($ws) {
$ws->push("Hello {$msg}!");
});
} catch (Throwable $e) {
echo "订阅已经关闭";
}
});
while (true) {
$frame = $ws->recv();
if ($frame === '') {
$ws->close();
echo "空数据导致关闭n";
break;
} else if ($frame === false) {
/*
* 其他异常导致中断
* */
echo 'errorCode: ' . swoole_last_error() . "n";
$ws->close();
break;
} else {
if ($frame->data == 'close' || get_class($frame) === CloseFrame::class) {
echo "用户主动关闭n";
$ws->close();
Coroutine::cancel($ws->Gid); //关闭协程
break;
}
echo $frame->data . "n";
}
}
});
$server->start();
});