跳至主要內容

hyperf记录(一)

Mr.Lexon大约 17 分钟back-end

hyperf记录(一)

核心架构

生命周期

因为hyperf是基于swoole的,swoole是基于php的,所以了解hyperf生命周期要从php的生命周期开始

php生命周期:

现在有一个index.php,我们通过命令行运行以下命令:

# php -f index.php

php首先会进入minit阶段,即模块初始初始化阶段,php在这个阶段会做以下操作:

  1. 初始化所有已安装的模块

  2. 初始化内核,Zend引擎

以上工作完成后php开始处理请求,此时我们的请求只有一个就是处理index.php

处理请求的时候php会进入请求启动阶段RINIT,php在这个阶段会做以下操作:

初始化index.php运行的基本环境,然后完成index.php的编译。

RINIT阶段完成之后,开始运行index.php的内容。

当index.php运行完毕之后就会php进入RSHUTDOWN(请求关闭)阶段,php将会把index.php运行之后所返回内容返回给命令行,并清理index.php所占用的内存,关闭php执行器。

因为我们的请求就只有一个,所以接下来php就会进入MSHUTDOWN(模块关闭)阶段,在这个阶段里面,php就会把所有的模块全部关闭,并清理模块所运行的程序,清理完之后,php就把自己一同关闭掉,整个php生命周期就完成了。

swoole生命周期:

swoole生命周期则是在phpRINIT阶段时开始运行,首先swoole进行初始化,创建Manager管理进程,创建Worker子进程,监听所有TCP/UDP端口,监听定时器Timer,接着进入onStart阶段,创建主进程,然后开始用Manager管理worker,在这个阶段的同时开始启动worker,当这两个任务完成之后,此时swoole开始监听连接,当客户端请求连入时,manager就会向woker派发task,并尝试建立连接,建立连接成功之后,开始监听客户端请求,当客户端请求发送之后接受请求然后worker开始处理请求直到向客户端发送response,这个周期所创建的对象,会在请求完成后销毁。

协程

什么是协程(官方定义):

协程是一种轻量级的线程,由用户代码来调度和管理,而不是由操作系统内核来进行调度,也就是在用户态进行

理解:

内核在处理线程请求时,如果遇到阻塞任务,则会将这个请求挂起并且将内核切换到没有挂起的请求工作

使用协程的两个条件:

  1. 不能存在阻塞代码

  2. 不能通过全局变量储存状态

    一个协程是在一个worker里面运行的,而一个worker存在多个协程,所以一旦通过全局去存储协程状态,那么就会造成状态混乱。

创建协程:

只需通过 co(callable $callable) 或 go(callable $callable) 函数或 Hyperf\Utils\Coroutine::create(callable $callable) 即可创建一个协程,协程内可以使用协程相关的方法和客户端。

判断是否在协程环境内:

调用Hyperf\Utils\Coroutine::inCoroutine(): bool

获取当前协程ID:

调用Hyperf\Utils\Coroutine::id(): int

如果不在协程环境内则函数返回 -1

Channel通道:

官方定义:

类似于 Go 语言的 chanChannel 可为多生产者协程和多消费者协程模式提供支持。底层自动实现了协程的切换和调度。 Channel 与 PHP 的数组类似,仅占用内存,没有其他额外的资源申请,所有操作均为内存操作,无 I/O 消耗,使用方法与 SplQueue 队列类似。
Channel 主要用于协程间通讯,当我们希望从一个协程里返回一些数据到另一个协程时,就可通过 Channel 来进行传递。

理解:

是各个协程之间的通讯工具

Defer特性:

官方定义:

当我们希望在协程结束时运行一些代码时,可以通过 defer(callable $callable) 函数或 Hyperf\Coroutine::defer(callable $callable) 将一段函数以 栈(stack) 的形式储存起来,栈(stack) 内的函数会在当前协程结束时以 先进后出 的流程逐个执行。

理解:

可充当协程的生命周期结束前的钩子函数或者是函数调用栈,在协程结束之后对栈中函数出栈调用

WaitGroup特性:

官方定义:

WaitGroup 是基于 Channel 衍生出来的一个特性,如果接触过 Go 语言,我们都会知道 WaitGroup 这一特性,在 Hyperf 里,WaitGroup 的用途是使得主协程一直阻塞等待直到所有相关的子协程都已经完成了任务后再继续运行,这里说到的阻塞等待是仅对于主协程(即当前协程)来说的,并不会阻塞当前进程。
我们通过一段代码来演示该特性:

理解:

子协程如果存在阻塞代码则可通过WaitGroup对主协程阻塞直到所有子协程处理完成。

Parallel特性:

官方定义:

Parallel 特性是 Hyperf 基于 WaitGroup 特性抽象出来的一个更便捷的使用方法

理解:

从用法上来看,是将原本阻塞的子协程并行处理。Parallel是把WaitGroup做成一个并行队列

协程上下文:

官方定义:

由于同一个进程内协程间是内存共享的,但协程的执行/切换是非顺序的,也就意味着我们很难掌控当前的协程是哪一个**(事实上可以,但通常没人这么干)**,所以我们需要在发生协程切换时能够同时切换对应的上下文。
在 Hyperf 里实现协程的上下文管理将非常简单,基于 Hyperf\Utils\Context 类的 set(string $id, $value)get(string $id, $default = null)has(string $id)override(string $id, \Closure $closure) 静态方法即可完成上下文数据的管理,通过这些方法设置和获取的值,都仅限于当前的协程,在协程结束时,对应的上下文也会自动跟随释放掉,无需手动管理,无需担忧内存泄漏的风险。

理解:

可以通过协程上下文对协程处理的数据进行管理

注解

官方定义:

注解是给应用程序看,用于元数据的定义,单独使用时没有任何作用,需配合应用程序对其元数据进行利用才有作用。

理解:

注解是用于数据定义和一些数据配置,方便其他类进行调用

创建注解:

namespace App\Annotation;

use Hyperf\Di\Annotation\AbstractAnnotation;

/**
 * @Annotation
 * @Target("ALL")
 */
class User extends AbstractAnnotation
{
    /**
     * @var string
     */
    public $name;
}

这里举例了一个User注解,从上得知创建注解需要两个要素:

  1. 注解类要继承AbstractAnnotation类

  2. 要打上@Annotation和@Target("ALL")注解

注意:命名空间App\Annotation在新创建的项目是没有的,所以要手动创建

细节分析:

@Annotation

表明这是一个注解类

@Target

表明可以注解到哪一个地方,

参数:

  1. PROPERTY,可以注解到类属性

  2. METHOD,可以注解到类方法

  3. CLASS,可以注解到类

  4. ALL,以上三个地方都可以注解

public $name:

这个是注解所需的参数名称

使用注解:

使用注解有两种方式:

  • PHP 版本低于 8.0 时
/**
 * @Annotationn
 */
  • PHP 版本大于等于 8.0 时
#[Annotation]

用例:

namespace App\Controller;

use App\Annotation\User;
use Hyperf\Di\Annotation\AnnotationCollector;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;

//类注解使用
/**
 * @User(name="abc")
 */
class annotationController
{
    //类属性注解使用
    /**
     * @User(name="bcd")
     */
        public $a;
    //类方法注解使用
    /**
     * @User(name="dfg")
     */
    public function getAnnotation()
    {
        //获取类注解内容
        var_dump(AnnotationCollector::getClassesByAnnotation(User::class));
        //获取类方法注解内容
        var_dump(AnnotationCollector::getMethodsByAnnotation(User::class));
        return 'getAnnotation';
    }
}

注解内容是通过AnnotationCollector获取,官方解释:

在没有自定义注解收集方法时,默认会将注解的元数据统一收集在 Hyperf\Di\Annotation\AnnotationCollector 类内,通过该类的静态方法可以方便的获取对应的元数据用于逻辑判断或实现。

配置

文件结构:

config ├── autoload // 此文件夹内的配置文件会被配置组件自己加载,并以文件夹内的文件名作为第一个键值 │ ├── amqp.php // 用于管理 AMQP 组件 │ ├── annotations.php // 用于管理注解 │ ├── apollo.php // 用于管理基于 Apollo 实现的配置中心 │ ├── aspects.php // 用于管理 AOP 切面 │ ├── async_queue.php // 用于管理基于 Redis 实现的简易队列服务 │ ├── cache.php // 用于管理缓存组件 │ ├── commands.php // 用于管理自定义命令 │ ├── consul.php // 用于管理 Consul 客户端 │ ├── databases.php // 用于管理数据库客户端 │ ├── dependencies.php // 用于管理 DI 的依赖关系和类对应关系 │ ├── devtool.php // 用于管理开发者工具 │ ├── exceptions.php // 用于管理异常处理器 │ ├── listeners.php // 用于管理事件监听者 │ ├── logger.php // 用于管理日志 │ ├── middlewares.php // 用于管理中间件 │ ├── opentracing.php // 用于管理调用链追踪 │ ├── processes.php // 用于管理自定义进程 │ ├── redis.php // 用于管理 Redis 客户端 │ └── server.php // 用于管理 Server 服务 ├── config.php // 用于管理用户或框架的配置,如配置相对独立亦可放于 autoload 文件夹内 ├── container.php // 负责容器的初始化,作为一个配置文件运行并最终返回一个 Psr\Container\ContainerInterface 对象 └── routes.php // 用于管理路由

server.php配置说明
配置解析:
<?php
declare(strict_types=1);

use Hyperf\Server\Server;
use Hyperf\Server\Event;

return [
    // 这里省略了该文件的其它配置
    'settings' => [
        'enable_coroutine' => true, // 开启内置协程
        'worker_num' => swoole_cpu_num(), // 设置启动的 Worker 进程数
        'pid_file' => BASE_PATH . '/runtime/hyperf.pid', // master 进程的 PID
        'open_tcp_nodelay' => true, // TCP 连接发送数据时会关闭 Nagle 合并算法,立即发往客户端连接
        'max_coroutine' => 100000, // 设置当前工作进程最大协程数量
        'open_http2_protocol' => true, // 启用 HTTP2 协议解析
        'max_request' => 100000, // 设置 worker 进程的最大任务数
        'socket_buffer_size' => 2 * 1024 * 1024, // 配置客户端连接的缓存区长度
    ],
];

注意事项:

如需要设置守护进程化,可在 settings 中增加 'daemonize' => true,执行 php bin/hyperf.php start后,程序将转入后台作为守护进程运行

单独的 Server 配置需要添加在对应 servers 的 settings 当中,如 jsonrpc 协议的 TCP Server 配置启用 EOF 自动分包和设置 EOF 字符串

config.php 与 autoload 文件夹内的配置文件的关系

config.php 与 autoload 文件夹内的配置文件在服务启动时都会被扫描并注入到 Hyperf\Contract\ConfigInterface 对应的对象中,配置的结构为一个键值对的大数组,两种配置形式不同的在于 autoload 内配置文件的文件名会作为第一层 键(Key) 存在,而 config.php 内的则以您定义的为第一层。

总的来说:autoload文件夹所包含的配置和config.php是在启动时会自动配置进Hyperf\Contract\ConfigInterface,本质上没有什么区别

自定义配置

只需在 config/config.php 与 config/autoload/server.php 与 autoload 文件夹内的配置,都能在服务启动时被扫描并注入到 Hyperf\Contract\ConfigInterface 对应的对象中,这个流程是由 Hyperf\Config\ConfigFactory 在 Config 对象实例化时完成的。

获取配置

通过 Config 对象获取配置

Config 组件提供了三种方式获取配置,通过 Hyperf\Config\Config 对象获取、通过 @Value 注解获取和通过 config(string $key, $default) 函数获取。

通过 @Value 注解获取配置

注意,使用这个注解的前提下,应用对象必须是通过DI实例化的

class IndexController
{
    
    /**
     * @Value("config.key")
     */
    private $configValue;
    
    public function index()
    {
        return $this->configValue;
    }
    
}

依赖注入

事件机制

AOP 面向切面编程

AOP官方解释:

AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,通过动态代理等技术实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,也是 Hyperf 中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

理解:

若将每个程序所运行的功能画成一个长方形,而每个函数的上下文占用长方形一定的面积,那么AOP则是在这个长方形的特定的函数的开始或结束从总的长方形剪下来在他们之间加入运行逻辑。以达到在修改原函数的前提下添加新的逻辑。

注意事项:

所有被 AOP 影响的类,都会在 ./runtime/container/proxy/ 文件夹内生成对应的 代理类缓存,是否在启动时自动生成取决于 config/config.php 配置文件中 scan_cacheable 配置项的值,默认值为 false,如果该配置项为 true 则 Hyperf 不会扫描和生成代理类缓存,而是直接以现有的缓存文件作为最终的代理类。如果该配置项为 false,则 Hyperf 会在每次启动应用时扫描注解扫描域并自动的生成对应的代理类缓存,当代码发生变化时,代理类缓存也会自动的重新生成。

通常在开发环境下,该值为 false,这样更便于开发调试,而在部署生产环境时,我们可能会希望 Hyperf 提前将所有代理类提前生成,而不是使用时动态的生成,可以通过 php bin/hyperf.php 命令来生成所有代理类,然后再通过环境变量 SCAN_CACHEABLE 为 true 修改该配置项的值,以达到启动时间更短、应用内存占用更低的目的。

基于以上,如果您使用 Docker 或 Kubernetes 等虚拟化技术来部署您的应用的话,您可以在镜像构建阶段就生成对应的代理类缓存并写入到镜像中去,在运行镜像实例时,可大大减少启动时间和应用内存。

总结:如果修改了或者添加和删除了切面,需要清理./runtime/container/proxy/所有的文件


基础功能

路由

路由使用(基本):

1. 闭包:
Router::get('/hello-hyperf', function () {
    return 'Hello Hyperf.';
});
2.标准使用:
// 下面三种方式的任意一种都可以达到同样的效果
Router::get('/hello-hyperf', 'App\Controller\IndexController::hello');
Router::get('/hello-hyperf', 'App\Controller\IndexController@hello');
Router::get('/hello-hyperf', [App\Controller\IndexController::class, 'hello'])3. 路由方法
// 注册与方法名一致的 HTTP METHOD 的路由
Router::get($uri, $callback);
Router::post($uri, $callback);
Router::put($uri, $callback);
Router::patch($uri, $callback);
Router::delete($uri, $callback);
Router::head($uri, $callback);

// 注册任意 HTTP METHOD 的路由
Router::addRoute($httpMethod, $uri, $callback);
Router::addRoute(['GET', 'POST','PUT','DELETE'], $uri, $callback);

路由使用(注解):

控制器注解访问路由
MyDataController@AutoController()/my_data/index
MydataController@AutoController()/mydata/index
MyDataController@AutoController(prefix="/data")/data/index
1. @AutoController()
/**
 * @AutoController()
 */
class UserController
{
    // Hyperf 会自动为此方法生成一个 /user/index 的路由,允许通过 GET 或 POST 方式请求
    public function index(RequestInterface $request)
    {
        // 从请求中获得 id 参数
        $id = $request->input('id', 1);
        return (string)$id;
    }
}

2.@Controller()
/**
 * @Controller()
 */
class UserController
{
    // Hyperf 会自动为此方法生成一个 /user/index 的路由,允许通过 GET 或 POST 方式请求
    /**
     * @RequestMapping(path="index", methods="get,post")
     */
    public function index(RequestInterface $request)
    {
        // 从请求中获得 id 参数
        $id = $request->input('id', 1);
        return (string)$id;
    }
}

路由参数:

注意:路由参数必须和控制器参数键名、类型保持一致,否则控制器无法接受到相关参数

Router::get('/user/{id}', 'App\Controller\UserController::info');php
public function info(int $id)
{
    $user = User::find($id);
    return $user->toArray();
}

通过 route 方法获取

public function index(RequestInterface $request)
{
        // 存在则返回,不存在则返回默认值 null
        $id = $request->route('id');
        // 存在则返回,不存在则返回默认值 0
        $id = $request->route('id', 0);
}

注意事项:

路由有3种标准化的设置

当uri设置参数可选时一定要带上后面的杠,例如:

设置:/user/[{id}]

访问:/user/ (成功)/user (失败,未找到路由)

中间件

原理(官方解释):

图中的顺序为按照 Middleware 1 -> Middleware 2 -> Middleware 3 的顺序组织着,我们可以注意到当中间的横线穿过 内核 即 Middleware 3 后,又回到了 Middleware 2,为一个嵌套模型,那么实际的顺序其实就是:
Request -> Middleware 1 -> Middleware 2 -> Middleware 3 -> Middleware 2 -> Middleware 1 -> Response
重点放在 核心 即 Middleware 3,它是洋葱的分界点,分界点前面的部分其实都是基于 请求(Request) 进行处理,而经过了分界点时,内核 就产出了 响应(Response) 对象,也是 内核 的主要代码目标,在之后便是对 响应(Response) 进行处理了,内核 通常是由框架负责实现的,而其它的就由您来编排了。

理解:

中间件贯穿整个请求过程,可以在其中实现请求和响应过滤

执行顺序:

我们从上面可以了解到总共有 3 种级别的中间件,分别为 全局中间件类级别中间件方法级别中间件,如果都定义了这些中间件,执行顺序为:全局中间件 -> 类级别中间件 -> 方法级别中间件

控制器

理解:

只是一个实现形式

请求

获取请求:

/**
 * @AutoController()
 */
class IndexController
{
    public function info(RequestInterface $request)
    {
        // ...
    }
}

$request是通过容器RequestInterface注入到方法当中去

注意事项:

$request->url

$ request->fullurl

区别在于url没有查询参数而fullurl有查询参数

响应

获取响应:

<?php
namespace App\Controller;

use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;

class IndexController
{
    public function json(ResponseInterface $response): Psr7ResponseInterface
    {
        $data = [
            'key' => 'value'
        ];
        return $response->json($data);
    }
}

$response是通过容器ResponseInterface注入到方法当中去

重定向:

通过$response->redirect('url')方法实现重定向功能

<?php
namespace App\Controller;

use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;

class IndexController
{
    public function redirect(ResponseInterface $response): Psr7ResponseInterface
    {
        // redirect() 方法返回的是一个 Psr\Http\Message\ResponseInterface 对象,需再 return 回去
        return $response->redirect('/anotherUrl');
    }
}

异常处理

创建:

  1. 首先创建一个异常类
namespace App\Controller;

use App\Exception\FooException;

class IndexController extends AbstractController
{
    public function index()
    {
        throw new FooException('Foo Exception...', 800);
    }
}
  1. 其次创建一个异常处理器

    namespace App\Exception\Handler;
    
    use Hyperf\ExceptionHandler\ExceptionHandler;
    use Hyperf\HttpMessage\Stream\SwooleStream;
    use Psr\Http\Message\ResponseInterface;
    use App\Exception\FooException;
    use Throwable;
    
    class FooExceptionHandler extends  ExceptionHandler
    {
        public function handle(Throwable $throwable, ResponseInterface $response)
        {
            // 判断被捕获到的异常是希望被捕获的异常
            if ($throwable instanceof FooException) {
                // 格式化输出
                $data = json_encode([
                    'code' => $throwable->getCode(),
                    'message' => $throwable->getMessage(),
                ], JSON_UNESCAPED_UNICODE);
    
                // 阻止异常冒泡
                $this->stopPropagation();
                return $response->withStatus(500)->withBody(new SwooleStream($data));
            }
    
            // 交给下一个异常处理器
            return $response;
    
            // 或者不做处理直接屏蔽异常
        }
    
        /**
         * 判断该异常处理器是否要对该异常进行处理
         */
        public function isValid(Throwable $throwable): bool
        {
            return true;
        }
    }
    
  2. 注册异常处理器

    <?php
    // config/autoload/exceptions.php
    return [
       'handler' => [
           // 这里的 http 对应 config/autoload/server.php 内的 server 所对应的 name 值
           'http' => [
               // 这里配置完整的类命名空间地址已完成对该异常处理器的注册
               \App\Exception\Handler\FooExceptionHandler::class,
           ],    
       ],
    ];
    

完成

日志

注意事项:

配置参数如下

<?php

return [
    'default' => [
        'handler' => [
            'class' => \Monolog\Handler\StreamHandler::class,//日志输入/输出类管理
            'constructor' => [
                'stream' => BASE_PATH . '/runtime/logs/hyperf.log',//日志文件
                'level' => \Monolog\Logger::DEBUG,//日志等级
            ],
        ],
        'formatter' => [
            'class' => \Monolog\Formatter\LineFormatter::class,//日志格式化管理
            'constructor' => [
                'format' => null,
                'dateFormat' => null,
                'allowInlineLineBreaks' => true,
            ]
        ],
    ],
];

剩余项目还待完成:

命令行国际化 显示乱码 验证器 场景 数据库模型 查询构造器 其它组件 连接池 自定义进程 辅助类 定时任务 Task 机制 枚举类

上次编辑于:
贡献者: Lexon