PHP Swoole学习笔记,持续记录

2023-02-17 14:20:27 浏览数 (2)

匿名函数

匿名函数(Anonymous functions),也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数。最经常用作回调函数 callable参数的值。

匿名函数目前是通过 Closure 类来实现的。

闭包可以从父作用域中继承变量。 任何此类变量都应该用 use 语言结构传递进去。例如:

代码语言:javascript复制
<?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()干了些什么

  1. start()运行之后会创建Master 进程 Manager 进程 serv->worker_num 个 Worker 进程。
  2. 启动失败会立即返回 false,启动成功后将进入事件循环,等待客户端连接请求。start 方法之后的代码不会执行。
  3. 服务器关闭后,start 函数返回 true,并继续向下执行
  4. 设置了 task_worker_num 会增加相应数量的 Task 进程
  5. 方法列表中 start 之前的方法仅可在 start 调用前使用,在 start 之后的方法仅可在 onWorkerStart、onReceive 等事件回调函数中使用

5.运行时进程

  1. Master 主进程,主进程内有多个 Reactor 线程,基于 epoll/kqueue 进行网络事件轮询。收到数据后转发到 Worker 进程去处理;
  2. Manager 进程,对所有 Worker 进程进行管理,Worker 进程生命周期结束或者发生异常时自动回收,并创建新的 Worker 进程;
  3. Worker 进程,对收到的数据进行处理,包括协议解析和响应请求;
  4. Reactor 线程,是在 Master 进程中创建的线程,负责维护客户端 TCP 连接、处理网络 IO、处理协议、收发数据,不执行任何 PHP 代码,将 TCP 客户端发来的数据缓冲、拼接、拆分成完整的一个请求数据包;
  5. 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.使用须知

  1. Swoole4 或更高版本拥有高可用性的内置协程,可以使用完全同步的代码来实现异步 IO,PHP 代码没有任何额外的关键字,底层会自动进行协程调度。
  2. 所有的协程必须在协程容器里面创建,Swoole 程序启动的时候大部分情况会自动创建协程容器,Server的enable_coroutine 控制事件回调是否自动创建协程。
  3. 防止多协程同时操作数据,导致运行混乱,协程内部禁止使用全局变量,协程使用 use 关键字引入外部变量到当前作用域禁止使用引用,协程之间通讯必须使用 Channel。
  4. 在协程编程中可直接使用 try/catch 处理异常。但必须在协程内捕获,不得跨协程捕获异常。当协程退出时,发现有未捕获的异常,将引起致命错误。
  5. 在一键协程化里面使用原生 PHP 提供的方法,它们的超时时间受 default_socket_timeout 配置影响,开发者可以通过 ini_set('default_socket_timeout', 60) 这样来单独设置它,它的默认值是 60。
  6. 使用 Coroutine::create 或 go 方法创建协程 ,在创建的协程中才能使用协程 API,而协程必须创建在协程容器里面。
  7. 在一个协程中可以使用 go 嵌套创建新的协程。因为 Swoole 的协程是单进程单线程模型,使用 go 创建的子协程会优先执行,子协程执行完毕或挂起时,将重新回到父协程向下执行代码,如果子协程挂起后,父协程退出,不影响子协程的执行,
  8. Swoole 的协程是单进程单线程模型。

3.协程HTTP服务端

  1. 对连接的处理是在单独的子协程中完成,客户端连接的 Connect、Request、Response、Close 是完全串行的。
  2. 监听的地址若是本地 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();
});

0 人点赞