使用 Swoole 扩展
PHP Swoole 扩展于 2013 年 12 月首次在 PHP 扩展 C 库网站( https://pecl.php.net/ )上发布。从那时起,它就获得了相当大的关注。随着 PHP 8 引入 JIT 编译器,人们对 Swoole 扩展再次产生了浓厚的兴趣,因为它快速、稳定,能够让 PHP 应用程序运行得更快。总下载量接近 600 万次,平均每月约为 5 万次。
在本节中,你将了解到该扩展的相关信息、安装方式和使用方法。让我们先来了解一下该扩展的概况。
检查 Swoole 扩展
由于该扩展是用 C 语言编写的,因此一旦编译、安装并启用,就会在当前的 PHP 安装中添加一组函数和类。不过,该扩展利用了某些仅适用于 UNIX 衍生操作系统的底层功能。这意味着,如果运行的是 Windows 服务器,要运行使用 Swoole 扩展的 PHP 异步应用程序,唯一的办法就是安装 Windows Services for Linux (WSL),或者将应用程序设置为在 Windows 服务器上的 Docker 容器中运行。
|
如果想在 Windows 服务器上尝试使用 PHP 异步,可以考虑使用 ReactPHP(在 使用 ReactPHP 部分中讨论),它没有 Swoole 扩展所需的操作系统依赖性。 |
PHP 异步的一大优势是,初始代码块会立即加载并保留在内存中,直到异步服务器实例停止。使用 Swoole 扩展时就是这种情况。在你的代码中,你创建了一个异步服务器实例,它有效地将 PHP 变成了一个在指定端口监听的持续运行的守护进程。但这也有一个缺点,那就是如果你修改了程序代码,异步服务器实例无法识别这些修改,直到你重新加载它。
Swoole 扩展的一大特色是它对 协程的支持。在现实生活中,这意味着我们不必对使用同步编程模型编写的应用程序进行大手术。Swoole 会自动挑选出阻塞操作,如文件系统访问和数据库查询,并允许在应用程序的其他部分继续进行时暂停这些操作。有了这种支持,您通常只需使用 Swoole 运行同步应用程序,就能立即提高性能。
Swoole 扩展的另一个非常棒的功能是 Swoole/Table。该功能可让您完全在内存中创建一个相当于数据库表的数据表,该数据表可在多个进程之间共享。这种结构有许多可能的用途,其潜在的性能增益确实惊人。
Swoole 扩展程序能够监听用户数据报协议(UDP)而非传输控制协议(TDP)的传输。这是一个非常有趣的可能性,因为 UDP 比 TCP 快得多。Swoole 还包括精确到毫秒的计时器实现,以及 MySQL、PostgreSQL、Redis 和 cURL 的同步客户端。Swoole 扩展还能让你使用 Golang 风格的通道设置进程间通信(IPC)。现在让我们来看看如何安装 Swoole。
安装 Swoole 扩展
安装 Swoole 扩展的方法与安装任何用 C 语言编写的 PHP 扩展的方法相同。一种方法是使用操作系统软件包管理器。例如 Debian 或 Ubuntu Linux 的 apt(或其不太友好的表亲 apt-get),以及 Red Hat、CentOS 或 Fedora 的 yum 或 dnf。使用操作系统软件包管理器时,Swoole 扩展会以预编译二进制文件的形式提供。
不过,建议使用 pecl 命令。如果您安装的操作系统上没有该命令,则可以在 Ubuntu 或 Debian 操作系统上安装 pecl 命令(以 root 用户身份登录),方法如下:apt install php-pear。如果安装的是 Red Hat、CentOS 或 Fedora 操作系统,则可使用以下方法:yum install php-pear。
使用 pecl 安装 Swoole 扩展时,可以指定一些选项。这里总结了这些选项:
有关这些选项的更多信息以及安装过程的概述,请点击此处: https://www.swoole.co.uk/docs/get-started/installation 现在让我们来看看包含 Swoole 套接字、JavaScript Object Notation (JSON) 和 cURL 支持的安装示例,如下所示:
-
我们首先要做的是更新
pecl频道。这是 PHP 扩展源代码库和函数的列表,很像apt或yum软件包管理器使用的源列表。下面是执行此操作的代码:pecl channel-update pecl.php.net -
接下来,我们指定安装命令,并使用
-D标志添加选项,如下所示:pecl install -D \ 'enable-sockets="yes" \ enable-openssl="no" \ enable-http2="no" \ enable-mysqlnd="no" \ enable-swoole-json="yes" \ enable-swoole-curl="yes"' \ swoole -
这将启动扩展的安装过程。现在,你将看到各种 C 语言代码文件和头文件被下载,然后本地 C 编译器将用于编译扩展。下面是编译过程的部分视图:
root@php8_tips_php8 [ / ]# pecl install swoole downloading swoole-4.6.7.tgz ... Starting to download swoole-4.6.7.tgz (1,649,407 bytes) ........................................................ ......................................................... ........................................................ ......................................................... ......................................................... ..........................................done: 1,649,407 bytes 364 source files, building running: phpize Configuring for: PHP Api Version: 20200930 Zend Module Api No: 20200930 Zend Extension Api No: 420200930 building in /tmp/pear/temp/pear-build-defaultuserQakGt8/ swoole-4.6.7 running: /tmp/pear/temp/swoole/configure --with-phpconfig=/usr/bin/php-config --enable-sockets=no --enableopenssl=no --enable-http2=no --enable-mysqlnd=yes --enable-swoole-json=yes --enable-swoole-curl=yes ... Build process completed successfully Installing '/usr/include/php/ext/swoole/config.h' Installing '/usr/lib/php/extensions/no-debug-nonzts-20200930/swoole.so' install ok: channel://pecl.php.net/swoole-4.6.7 configuration option "php_ini" is not set to php.ini location You should add "extension=swoole.so" to php.ini -
如果找不到 C 编译器,系统会发出警告。此外,可能还需要为操作系统安装 PHP 开发库。如果出现这种情况,警告信息会提供进一步的指导。
-
完成后,您需要启用扩展。这可以通过在
php.ini文件中添加extension=swoole来实现。如果不确定其位置,请使用php -i命令查找php.ini文件的位置。以下是您可以在命令行中发出的添加该指令的命令:echo "extension=swoole" >>/etc/php.ini -
然后,您可以使用以下命令确认 Swoole 扩展是否可用:
php --ri swoole
至此,Swoole 扩展的安装完成。如果是自定义编译 PHP,也可以在编译前运行 configure 时添加 --enable-swoole 选项。这将使 Swoole 扩展与核心 PHP 安装一起编译和启用(并允许你绕过刚才概述的安装步骤)。现在我们来看一个从文档中摘录的 Hello World 示例,以测试安装情况。
测试安装
Swoole 文档提供了一个简单的示例,您可以用它来快速测试安装是否成功。示例代码位于 Swoole 文档主页 ( https://www.swoole.co.uk/docs/ )。出于版权原因,我们不在此复制。以下是运行 Hello World 测试的步骤:
-
首先,我们将 Hello World 示例从 https://www.swoole.co.uk/docs/ 复制到 /path/to/repo/ch12/php8_swoole_hello_world.php 文件。
接下来,我们修改了演示程序,将
$server = new Swoole\ HTTP\Server("127.0.0.1", 9501);改为$server = new Swoole\ HTTP\Server("0.0.0.0", 9501);。这一修改允许 Swoole 服务器监听任意互联网协议(IP)地址的 9501 端口。
-
然后,我们修改了
/repo/ch12/docker-compose.yml文件,使端口 9501 在 Docker 容器外可用,如下所示:version: "3" services: ... php8-tips-php8: ... ports: - 8888:80 - 9501:9501 ... -
为了使这一更改生效,我们必须先关闭服务,然后再重新启动。在本地计算机的命令提示符/终端窗口中,使用这两条命令:
/path/to/repo/init.sh down /path/to/repo/init.sh up -
请注意,如果您运行的是 Windows,请删除
.sh。 -
然后,我们在 PHP 8 Docker 容器中打开一个 shell,运行 Hello World 程序,如下所示:
$ docker exec -it php8_tips_php8 /bin/bash # cd /repo/ch12 # php php8_swoole_hello_world.php -
最后,我们从 Docker 容器外部打开浏览器,IP 地址和端口是:http://172.16.0.88:9501。
下面的截图显示了 Swoole Hello World 程序的结果:
在详细了解如何使用 Swoole 扩展来提高应用程序性能之前,我们需要检查一个示例应用程序,它是 PHP 异步模型的主要候选程序。
检查示例 I/O 密集型应用程序
为了便于说明,我们创建了一个示例应用程序,该示例应用程序是以呈现状态传输(REST)API 的形式编写的,设计在 PHP 8 中运行。该示例应用程序提供了一个聊天或即时消息 API,具有以下简单功能:
-
使用超文本传输协议(HTTP)POST 方法向特定用户或所有用户发布信息。发布成功后,API 会返回刚刚发布的信息。
-
带有
from=username参数的 HTTPGET方法会返回与该用户名往来的所有信息,以及发给所有用户的信息。如果设置了all=1参数,则会返回所有用户名的列表。 -
HTTP DELETE 方法会删除 messages 表中的所有消息。
本节仅显示部分程序代码。如果您对整个聊天应用程序感兴趣,源代码位于 /path/to/repo/src/Chat 下。这里提供了主要的 API 端点:http://172.16.0.81/ch12/php8_ chat_ajax.php。
以下示例在 PHP 8.1 Docker 容器中执行。请确保在 Windows 计算机上通过本地计算机上的命令提示符,按如下步骤关闭现有容器: C:\path\to\repo\init down。对于 Linux 或 Mac,从终端窗口:/path/to/repo/init.sh down。从 Windows 计算机启动 PHP 8.1 容器:C:path\to/repo\ch12\init up。从 Linux 或 Mac 终端窗口 /path/to/repo/ch12/init.sh up。
下面的示例在 PHP 8.1 Docker 容器中执行。请确保在 Windows 计算机上通过本地计算机上的命令提示符,按如下方式调用现有容器: C:path\to\repo\init down
对于 Linux 或 Mac,可从终端窗口进行操作:
/path/to/repo/init.sh down
要从 Windows 计算机启动 PHP 8.1 容器:
C:\path\to\repo\ch12\init up
从 Linux 或 Mac 终端窗口:
/path/to/repo/ch12/init.sh up
我们现在看一下核心 API 程序本身的源代码,如下:
-
首先,我们定义一个
Chat\Message\Pipe类,确定我们需要使用的所有外部类,就像这样:// /repo/src/Chat/Messsage/Api.php; namespace Chat\Message; use Chat\Handler\ {GetHandler, PostHandler, NextHandler,GetAllNamesHandler,DeleteHandler}; use Chat\Middleware\ {Access,Validate,ValidatePost}; use Chat\Message\Render; use Psr\Http\Message\ServerRequestInterface; class Pipe { -
然后,我们定义了一个
exec()静态方法,用于调用一组符合 PHP 标准建议 15(PSR-15)的处理程序。我们还通过调用Chat\Middleware\Access中间件类的process方法来调用管道的第一阶段。NextHandler的返回值将被忽略:public static function exec( ServerRequestInterface $request) { $params = $request->getQueryParams(); $method = strtolower($request->getMethod()); $dontcare = (new Access()) ->process($request, new NextHandler()); -
还是在同一个方法中,我们使用
match()结构来检查 HTTPGET、POST和DELETE方法调用。如果方法是POST,我们就使用Chat\Middleware\ValidatePost验证中间件类来验证POST参数。如果验证成功,经过处理的数据就会传递给Chat\ Handler\PostHandler。如果 HTTP 方法是DELETE,我们就直接调用Chat\Handler\DeleteHandler:$response = match ($method) { 'post' => (new ValidatePost()) ->process($request, new PostHandler()), 'delete' => (new DeleteHandler()) ->handle($request), -
如果 HTTP 方法是
GET,我们首先检查是否设置了all参数。如果是,我们就调用Chat\Handler\GetAllNamesHandler。否则,默认子句将通过Chat\MiddleWare\Validate传递数据。如果验证成功,经过消毒的数据将传递给Chat\Handler\GetHandler:'get' => (!empty($params['all']) ? (new GetAllNamesHandler())->handle($request) : (new Validate())->process($request, new GetHandler())), default => (new Validate()) ->process($request, new GetHandler())}; return Render::output($request, $response); } } -
然后就可以使用一个简短的常规程序来调用核心 API 类,如图所示。在这个调用程序中,我们使用
Laminas\Diactoros\ ServerRequestFactory建立了一个符合 PSR-7 标准的Psr\Http\Message\ServerRequestInterface实例。然后通过管道类传递请求并产生响应:// /repo/ch12/php8_chat_ajax.php include __DIR__ . '/vendor/autoload.php'; use Laminas\Diactoros\ServerRequestFactory; use Chat\Message\Pipe; $request = ServerRequestFactory::fromGlobals(); $response = Pipe::exec($request); echo $response;
我们还创建了一个测试程序(/repo/ch12/php8_chat_test.php-未显示),它调用 API 端点的次数是设定好的(默认为 100)。每次迭代时,测试程序都会发布一条随机消息,包括一个随机的收件人用户名、一个随机的日期,以及 /repo/sample_data/geonames.db 数据库中的一个连续条目。测试程序需要两个参数。第一个参数是表示 API 的 URL。第二个参数(可选)表示迭代次数。
下面是从命令 shell 到 PHP 8.1 Docker 容器运行 /ch12/php8_chat_test.php 的示例结果:
root@php8_tips_php8_1 [ /repo/ch12 ]# php php8_chat_test.php \
http://localhost/ch12/php8_chat_ajax.php 10000 bboyer :
Dubai:AE:2956587 : 2021-01-01 00:00:00
1 fcompton : Sharjah:AE:1324473 : 2022-02-02 01:01:01
...
998 hrivas : Caloocan City:PH:1500000 : 2023-03-19 09:54:54
999 lpena : Budta:PH:1273715 : 2021-04-20 10:55:55
From User: dwallace
Elapsed Time: 3.3177478313446
从输出结果中,请注意所花费的时间。在下一节中,通过使用 Swoole,我们可以将时间缩短一半!不过,在使用 Swoole 之前,我们必须加入 JIT 编译器。我们使用以下命令启用 JIT:
# php /repo/ch10/php8_jit_reset.php on
在 PHP 8.0.0 中,您可能会遇到一些错误,甚至可能出现分段错误。但在 PHP 8.1 中,启用 JIT 编译器后,API 应能按预期运行。不过,JIT 编译器能否提高性能还很值得怀疑,因为频繁的 API 调用会导致应用程序等待。任何频繁阻塞 I/O 操作的应用程序都是异步编程模型的最佳候选。不过,在继续之前,我们需要关闭 JIT,使用与之前相同的实用程序,如下所示:
# php /repo/ch10/php8_jit_reset.php off
现在让我们来看看如何使用 Swoole 扩展来提高这个 I/O 密集型应用程序的性能。
使用 Swoole 扩展提高应用程序性能
鉴于 Swoole 提供了例程支持,为了提高聊天应用程序的性能,我们真正需要做的就是重写 /repo/ch12/php8_chat_ajax.php 调用程序,将其转化为一个 API,作为 Swoole 服务器实例监听 9501 端口。以下是重写主 API 调用程序的步骤:
-
首先,我们启用自动加载并确定所需的外部类:
// /repo/ch12/php8_chat_swoole.php include __DIR__ . '/vendor/autoload.php'; use Chat\Message\Pipe; use Chat\Http\SwooleToPsr7; use Swoole\Http\Server; use Swoole\Http\Request; use Swoole\Http\Response; -
接下来,我们启动一个 PHP 会话,并创建一个
Swoole\HTTP\Server实例,该实例监听 9501 端口上的任何 IP 地址:session_start(); $server = new Swoole\HTTP\Server('0.0.0.0', 9501); -
然后,我们调用
on()方法,并将其与启动事件相关联。在这种情况下,我们会记录一条日志,以确定 Swoole 服务器何时启动。其他服务器事件记录在这里: https://www.swoole.co.uk/docs/modules/swoole-http-server-doc :$server->on("start", function (Server $server) { error_log('Swoole http server is started at ' . 'http://0.0.0.0:9501'); }); -
最后,我们定义了一个主服务器事件
$server->on('request',function () {}),用于处理接收到的请求。下面是实现这一功能的代码:$server->on("request", function ( Request $swoole_request, Response $swoole_response){ $request = SwooleToPsr7:: swooleRequestToServerRequest($swoole_request); $swoole_response->header( "Content-Type", "text/plain"); $response = Pipe::exec($request); $swoole_response->end($response); }); $server->start();
不幸的是,传递给与 on() 方法相关的回调的 Swoole\Http\Request 实例不符合 PSR-7 标准!因此,我们需要定义一个 Chat\Http\SwooleToPsr7 类和一个使用静态调用执行转换的 swooleRequestToServerRequest() 方法。然后,我们在 Swoole\Http|Response 实例上设置标头,并从管道返回一个值,以完成整个电路。
需要注意的是,标准的 PHP 超级全局变量(如 $_GET 和 $_POST)并不能像运行中的 Swoole 服务器实例所期望的那样工作。主要的入口点是您用来从命令行启动 Swoole 服务器的初始程序。唯一的输入请求参数是实际的初始程序文件名。任何后续输入都必须通过传递给 on() 函数的 Swoole\Http\Request 实例来捕获。
在 https://php.net/swoole 上找到的文档并没有显示 Swoole\HTTP\Request 和 Swoole\HTTP\Response 类的所有可用方法。不过,您可以在 Swoole 网站上找到相关文档,这些文档也列在此处:
还值得注意的是,Swoole\HTTP\Request 对象的属性与 PHP 的 superglobals 大致对应,如图所示:
另一个需要考虑的问题是,在 Swoole 例程中使用 Xdebug 可能会导致分段故障和其他问题,甚至包括 内核转储。最佳做法是在首次使用 pecl 安装 Swoole 时使用 --enable-debug 标志启用 Swoole 调试。测试应用程序的步骤如下:
-
通过命令 shell 进入 PHP 8.1 Docker 容器,我们运行了 Swoole 版本的聊天 API,如下所示。立即显示的消息是
$server->on("start", function() {})的结果:# cd /repo/ch12 # php php8_chat_swoole.php Swoole http server is started at http://0.0.0.0:9501 -
然后,我们在主机上打开另一个终端窗口,并在 PHP 8.1 Docker 容器中打开另一个 shell。在这里,我们可以运行 /repo/ ch12/php8_chat_test.php 测试程序,如下所示:
# cd /repo/ch12 # php php8_chat_test.php http://localhost:9501 1000 -
请注意两个附加参数。第一个参数告诉测试程序使用 Swoole 版本的 API,而不是使用 Apache 网络服务器的旧版本。最后一个参数告诉测试程序运行 1000 次迭代。
下面让我们来看看输出结果:
root@php8_tips_php8_1 [ /repo/ch12 ]# php php8_chat_test.php \
http://localhost:9501 1000
0 coconnel : Dubai:AE:2956587 : 2021-01-01 00:00:00
1 htyler : Sharjah:AE:1324473 : 2022-02-02 01:01:01
...
998 cvalenci : Caloocan City:PH:1500 : 2023-03-19 09:54:54
999 smccormi : Budta:PH:1273715 : 2021-04-20 10:55:55
From User: ajenkins
Elapsed Time: 1.8595671653748
输出中最显著的特征是耗时。如果您回顾上一节,就会发现使用 Apache 作为传统 PHP 应用程序运行的应用程序接口完成 1,000 次迭代需要大约 3.35 秒,而在 Swoole 下运行的相同应用程序接口只需大约 1.86 秒即可完成:几乎是原来时间的一半!
请注意,这是在没有任何额外优化的情况下。Swoole 还有许多其他功能可供我们使用,包括定义内存表、从额外的工作线程中生成任务、使用事件循环促进缓存等。正如您所看到的,Swoole 可以立即提升性能,作为从现有应用程序中获得更高性能的一种可能方法,它非常值得研究。
现在,您已经了解了如何使用 Swoole 来提高应用程序性能,让我们来看看其他潜在的 PHP 异步解决方案。