Swoole
其实超出浏览器,PHP仍有用武之地,Linux上PHP通过pcntl、posix、sysvshm、sysvmsg、libevent、 swoole等PECL扩展支持,用纯PHP就可以编写不依赖Nginx/Apache的高性能Socket应用服务器(独立控制进程,以守护进程运行)
Swoole PHP语言的异步、并行、高性能网络通信框架, 使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,数据库连接池,AsyncTask,消息队列, 毫秒定时器,异步文件读写,异步DNS查询。Swoole可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网、车联网、智能家居等领域。 使用PHP+Swoole作为网络通信框架,可以使企业IT研发团队的效率大大提升,更加专注于开发创新产品。
安装swoole扩展
下载源代码包后,在终端进入源码目录,执行下面的命令进行编译和安装
cd swoole
phpize
./configure
make
sudo make install
安装成功
编译安装成功后,修改php.ini加入
extension=swoole.so
swoole.use_shortname='off'
方法1:通过命令查看swoole
方法2:通过命令查看swoole
php --ri swoole
swoole
Swoole => enabled
Author => Swoole Team <team@swoole.com>
通过php -m来查看是否成功加载了swoole.so,如果没有可能是php.ini的路径不对,可以使用php --ini来定位到php.ini的绝对路径
安装ext-async扩展
安装ext-async扩展:https://github.com/swoole/ext-async/releases 下载版本号必须跟swoole一致
tar –xzf ext-async-4.4.16.tar.gz
cd ext-async-4.4.16
./configure
make
TCP Server
<?php //server.php
$serv = new swoole_server("127.0.0.1", 9501);
$serv->set(array(
'worker_num' => 8, //工作进程数量
'daemonize' => true, //是否作为守护进程
));
$serv->on('connect', function ($serv, $fd){
echo "Client:Connect.\n";
});
$serv->on('receive', function ($serv, $fd, $from_id, $data) {
$serv->send($fd, 'Swoole: '.$data);
$serv->close($fd);
});
$serv->on('close', function ($serv, $fd) {
echo "Client: Close.\n";
});
$serv->start();
TCP Client
<?php
$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
//设置事件回调函数
$client->on("connect", function($cli) {
$cli->send("hello world\n");
});
$client->on("receive", function($cli, $data){
echo "Received: ".$data."\n";
});
$client->on("error", function($cli){
echo "Connect failed\n";
});
$client->on("close", function($cli){
echo "Connection close\n";
});
//发起网络连接
$client->connect('127.0.0.1', 9501, 0.5);
php命令行开启监听
php server.php
客户端访问
浏览器运行代码
<?php
$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_SYNC); //同步阻塞
$client->connect('127.0.0.1', 9503, 0.5);发起网络连接
$client->send("hello world\n");
$data = $client->recv(1024);
echo $data;
unset($client);
Hyperf框架介绍
Hyperf 是基于 Swoole 扩展的高性能协程框架,使用 PHP 语言开发。它具有以下特点:
- 高性能:利用 Swoole 的协程特性,Hyperf 能够实现高并发处理能力,相比传统的 PHP-FPM 模式性能有显著提升。
- 微服务支持:内置了多种微服务治理功能,如服务发现、负载均衡、熔断降级等,方便构建分布式系统。
- 丰富的组件:提供了众多开箱即用的组件,涵盖数据库访问、缓存、消息队列、任务调度等多个方面。
- 灵活扩展:通过依赖注入容器和事件机制,可以轻松扩展或替换现有功能。
安装Hyperf
- 1》安装hyperf:通过composer来安装composer create-project hyperf/hyperf-skeleton;
- 2》因为hyperf是依赖composer的,所以安装完需要在对应的项目目录composer install,安装composer的依赖包,不然无法启动;
- 3》启动服务:php bin/hyperf.php start,服务启动之后当修改代码并不会自动重新启动服务,需要手动重启;
- 4》然后访问,hyperf默认访问的是index首页;http://localhost:9501/或者另开一个命令窗口,执行:curl 127.0.0.1:9501,返回如下图所示:
Repository层
Repository 层是设计模式中的一种,用于封装对数据库的操作,使得业务逻辑与数据访问逻辑分离。
php bin/hyperf.php gen:repository User
表单请求验证
对于复杂的验证场景,您可以创建一个 表单请求(FormRequest)
,表单请求是包含验证逻辑的一个自定义请求类,您可以通过执行下面的命令
php bin/hyperf.php gen:request Menu/CreateRequest
创建一个名为CreateRequest 的表单验证类:
<?php
declare(strict_types=1);
namespace App\Request\Menu;
use Hyperf\Validation\Request\FormRequest;
class CreateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'status' => ['required', 'integer'],
'visibility' => ['array'],
'remark' => ['string'],
];
}
}
表单验证类会生成于 app\Request
目录下,如果该目录不存在,运行命令时会自动创建目录。
<?php
declare(strict_types=1);
namespace App\Request\Menu;
class UpdateRequest extends CreateRequest
{
public function rules(): array
{
$rules = parent::rules();
$rules['id'] = ['required', 'integer'];
return $rules;
}
}
那么,验证规则要如何生效呢?您所要做的就是在控制器方法中通过类型提示声明该请求类为参数。这样在控制器方法被调用之前会验证传入的表单请求,这意味着你不需要在控制器中写任何验证逻辑并很好的解耦了这两部分的代码:
<?php
namespace App\Controller;
use App\Request\Menu\CreateRequest;
class IndexController
{
public function index(CreateRequest $request)
{
$all = $request->all();
// 获取指定输入值
$page = (int) $request->input('page', 1);
// 获取通过验证的数据...
$validated = $request->validated();
}
}
删除缓存
在 Hyperf 项目中,删除缓存可以帮助解决一些常见的问题,例如配置更改未生效、路由缓存问题等。以下是删除 Hyperf 项目缓存的几种常见方法
php bin/hyperf.php clear:all
缓存文件通常位于 runtime 目录下。也可以手动删除
创建模型命令
php bin/hyperf.php gen:model table_name
模型添加数据库中不存在的属性
class Job extends Model
{
// 添加虚拟字段到 $appends 数组中
protected array $appends = ['grade_name'];
// 定义访问器
public function getGradeNameAttribute()
{
return JobGrade::where('id', $this->grade_id)->value('grade_name');
}
}
文件日志
在config/autoload/logger.php中增加一项对日志的配置:
return [
'default' => [
'handler' => [
'class' => Monolog\Handler\RotatingFileHandler::class,
'constructor' => [
'filename' => BASE_PATH . '/runtime/logs/hyperf.log',
'level' => Monolog\Logger::DEBUG,
'maxFiles' => 14,
],
],
'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class,
'constructor' => [
'format' => "[%datetime%] %channel%.%level_name%: %message%\n",
'dateFormat' => 'Y-m-d H:i:s',
'allowInlineLineBreaks' => true,
],
],
],
开发环境,记录sql查询日志
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Listener;
use Hyperf\Collection\Arr;
use Hyperf\Database\Events\QueryExecuted;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
#[Listener]
class DbQueryExecutedListener implements ListenerInterface
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(ContainerInterface $container)
{
$this->logger = $container->get(LoggerFactory::class)->get('sql', 'sql');
}
public function listen(): array
{
return [
QueryExecuted::class,
];
}
/**
* @param QueryExecuted $event
*/
public function process(object $event): void
{
$appEnv = \Hyperf\Support\env('APP_ENV', 'local');
if (in_array($appEnv, ['local', 'dev']) && $event instanceof QueryExecuted) {
$sql = $event->sql;
if (!Arr::isAssoc($event->bindings)) {
$position = 0;
foreach ($event->bindings as $value) {
$position = strpos($sql, '?', $position);
if ($position === false) {
break;
}
$value = "'{$value}'";
$sql = substr_replace($sql, $value, $position, 1);
$position += strlen($value);
}
}
$this->logger->info(sprintf('[%s] %s', $event->time, $sql));
}
}
}
禁用 AMQP
在 Hyperf 项目中,如果你希望禁用 AMQP(RabbitMQ)功能,可以通过修改配置文件 config/autoload/amqp.php 中的 enable 配置项实现。
配置读取
只需在 config/config.php
与 config/autoload/*.php
与 autoload
文件夹内的配置。Config 组件提供了三种方式获取配置,通过 Hyperf\Config\Config
对象获取、通过 #[Value]
注解获取和通过 config(string $key, $default)
函数获取。在你的项目中,.env 文件定义了 APP_ENV=dev,而 config('app.env') 的值通常会通过 env('APP_ENV') 加载到配置文件中。因此,在大多数情况下,config('app.env') 和 env('APP_ENV') 的值是相同的,但它们的加载机制和适用场景有所不同。使用 config() 是更优的选择。它利用了 Hyperf 的配置缓存机制,性能更高。
env配置不能正常读取
The "WCadbK9oDn" variable is not set. Defaulting to a blank string.
加引号解决
ADB_PASSWORD='9$WCadbK9oDn'
热重启组件
由于 Hyperf
是持久化的 CLI
应用,也就意味着一旦进程启动,已解析的 PHP
代码会持久化在进程中,也就意味着启动服务后您再修改的 PHP
代码不会改变已启动的服务,如您希望服务重新加载您修改后的代码,您需要通过在启动的 Console
中键入 CTRL + C
终止服务,再重新执行启动命令 php bin/hyperf.php start
完成启动和重新加载。这里官方也给出了解决方案:安装一个热重启/热重载的组件(Watcher提供了文件修改后立马重启的功能,适用于开发环境提供便利)这个组件会监控文件的变化,并在检测到变化时自动重启服务,从而实现热重启功能
composer require hyperf/watcher --dev
发布配置
php bin/hyperf.php vendor:publish hyperf/watcher
将原来正在运行的项目 ctrl+c 给结束掉进程。在项目的根目录运行
#php bin/hyperf.php start 废弃
php bin/hyperf.php server:watch
这个时候就会重新运行起来
<?php
declare(strict_types=1);
use Hyperf\Watcher\Driver\ScanFileDriver;
return [
'driver' => ScanFileDriver::class,
'bin' => 'php',
'watch' => [
'dir' => ['app', 'config', 'routes', 'storage'],
'file' => ['.env'],
'scan_interval' => 2000,
],
];
动态修改或追加配置数据库
对于按月分库的数据库链接,不可能在config/autoload/databases.php中配置无数个数据库。你可能需要根据某些条件动态地设置或修改配置项。Hyperf 框架提供了灵活的配置管理方式,允许你根据需要在运行时修改配置。ConfigInterface
是一个用于读取和设置配置项的接口。
<?php
declare(strict_types=1);
namespace App\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Framework\Event\AfterWorkerStart;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Di\Annotation\Inject;
/**
* 动态切换数据库 按月选择
* @Listener
*/
class BillDataBaseListener implements ListenerInterface
{
/**
* @Inject
* @var \Hyperf\Contract\ConfigInterface
*/
public ConfigInterface $configInterface;
public function listen(): array
{
// 事件
return [
AfterWorkerStart::class,
];
}
public function process(object $event)
{
// 读取组件配置
$config = config('databases.bill');
// 当前月数据库名
$config['database'] = 'bill' . date('Ym');
// 写入配置
$this->configInterface->set('databases.bill', $config);
}
}
根据查询参数切换数据库
<?php
declare(strict_types=1);
namespace App\Service\Tools;
use App\Util\Logger;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Di\Annotation\Inject;
use InvalidArgumentException;
/**
* 动态数据库服务类,传参修改数据库配置
*/
class DynamicDataBaseService
{
/**
* 数据库配置文件键名
*/
public const DATABASES_FILE_KEY = 'databases.';
/**
* @Inject
* @var \Hyperf\Contract\ConfigInterface
*/
protected ConfigInterface $config;
/**
* 构造函数,注入配置接口
*
* @param ConfigInterface $config 配置接口,用于获取数据库配置
*/
public function __construct(ConfigInterface $config)
{
$this->config = $config;
}
/**
* 切换数据库配置
*
* 本函数用于动态切换数据库配置,主要目的是为了适应系统在运行时需要连接到不同数据库的需求
* 它通过更新配置文件中指定数据库的配置信息来实现数据库的切换
*
* @param string $dbKey 数据库键,用于在配置文件中唯一标识一个数据库配置
* @param string $dbName 新的数据库名称,用于更新数据库配置
*
* @return void
*/
public function switchDataBase(string $dbKey, string $dbName): void
{
// 获取指定数据库键的配置信息
$config = config(self::DATABASES_FILE_KEY . $dbKey);
// 将数据库名称注入到配置中
$config['database'] = $dbName;
// 记录日志,记录数据库配置切换信息
Logger::info("---->切换数据库[{$dbKey}]后新配置:" . var_export($config, true));
// 检查更新后的数据库配置是否有效
$this->checkDataBaseConfig($config);
// 设置数据库配置
$this->config->set(self::DATABASES_FILE_KEY . $dbName, $config);
// 清除配置缓存
$this->clearConfigCache();
}
/**
* 检查数据库配置是否完整
*
* 此方法确保提供的数据库配置数组中包含所有必要的键如果缺少任何一个关键配置,
* 方法将抛出一个InvalidArgumentException异常这是为了确保程序在尝试连接到数据库之前,
* 已经正确设置了所有必需的参数
*
* @param array $config 数据库配置数组,应包含连接到数据库所需的所有参数
* @throws InvalidArgumentException 如果配置数组中缺少任何一个必需的键
*/
protected function checkDataBaseConfig(array $config): void
{
// 校验配置是否包含必要字段
$requiredKeys = ['driver', 'host', 'port', 'username', 'password', 'database'];
foreach ($requiredKeys as $key) {
if (!array_key_exists($key, $config)) {
throw new InvalidArgumentException("数据库配置中缺少必要字段 '{$key}'.");
}
}
}
private function clearConfigCache(): void
{
$command = BASE_PATH . '/bin/hyperf.php config:clear';
shell_exec($command);
}
}
BillDataBaseListener会默认使用当月数据库(热点数据)历史月份使用方法switchDataBase来切换。使用的config方法可能存在缓存刷新不及时问题
<?php
declare(strict_types=1);
namespace App\Repository\Bill;
use App\Repository\AbstractRepository;
use App\Model\Entity\Bill\LlmTokens;
use App\Util\QueryBuilder\LlmTokensBuilder;
use App\Service\Tools\DynamicDataBaseService;
use Hyperf\Contract\LengthAwarePaginatorInterface;
use Hyperf\Database\Model\Builder;
use Hyperf\Di\Annotation\Inject;
class LlmTokensRepository extends AbstractRepository
{
/**
* @Inject
* @var DynamicDataBaseService
*/
protected DynamicDataBaseService $dynamicDataBaseService;
public function __construct()
{
parent::__construct(LlmTokens::class);
}
/**
* @param array $conditions
* @param int|null $perPage
* @param array|null $sorts
* @param array $columns
* @param Builder|null $builder
* @return LengthAwarePaginatorInterface
*/
public function paginate(array $conditions, ?int $perPage = null, ?array $sorts = null, array $columns = ['*'], ?Builder $builder = null, $pageName = 'page'): LengthAwarePaginatorInterface
{
try {
$formatMonth = str_replace('-', '', substr($conditions['start_time'], 0, 7));
$connectionName = 'bill' . $formatMonth;
$this->entity->setConnection($connectionName);
$this->dynamicDataBaseService->switchDataBase('bill', $connectionName);
$builder = LlmTokensBuilder::build($this->entity->newQuery(), $conditions);
$result = parent::paginate($conditions, $perPage, $sorts, $columns, $builder, $pageName);
} catch (\Exception $e) {
Logger::info('查询异常:' . $e->getMessage());
// 创建一个空的 LengthAwarePaginator 实例
$result = new \Hyperf\Paginator\LengthAwarePaginator([], 0, $perPage ?? 15, 1);
}
return $result;
}
}
动态更改Model连接名称的Trait
<?php
declare(strict_types=1);
namespace App\Traits;
use Hyperf\Database\Model\Builder;
trait HasDynamicConnection
{
/**
* 创建使用指定连接的查询构造器
* @param string $name
* @return Builder
*/
public static function usingConnection(string $name): Builder
{
$model = new static();
$model->setConnection($name);
return $model->newQuery();
}
}
使用方法
$user = new User();
$user->setConnection('mysql2')->save(['name' => 'John']);
// 使用指定连接的查询构造器
$users = User::usingConnection('mysql2')
->where('age', '>', 18)
->get();
这边可以看hyperf的官网:hyperf官网链接