• 深入 Hyperf:HTTP 服务启动时发生了什么?


    当我们创建 Hyperf 项目之后,只需要在终端执行 php bin/hyperf.php start 启动命令,等上几秒钟,就可以看到终端输出的 Worker 进程已启动,HTTP 服务监听在 9501 端口的日志信息。

    [INFO] Worker#3 started.  
    [INFO] Worker#1 started.  
    [INFO] Worker#2 started.  
    [INFO] Worker#0 started.  
    [INFO] HTTP Server listening at 0.0.0.0:9501  
    

    打开浏览器访问 http://127.0.0.1:9501,不出意外的话,页面会显示 Hello Hyperf,说明 HTTP 服务已经在工作了。那么这是怎么做到的呢?当我们执行启动命令后,Hyperf 是如何让 HTTP 服务启动的?

    所以今天这篇文章我会从启动命令开始,给你介绍下 HTTP 服务是如何完成初始化并启动的。通过阅读这篇文章,你可以了解到以下内容:

    • Hyperf 启动时会做哪些初始化操作?
    • HTTP 服务启动时会做哪些初始化操作?
    • HTTP 服务初始化时有哪些关键配置项?

    接下来,我们就从 Hyperf 的入口文件开始,了解启动 HTTP 服务的实现思路。

    文章会持续修订,转载请注明来源地址:https://her-cat.com/posts/2023/05/15/what-happens-when-hyperf-http-server-starts/

    bin/hyperf.php 文件:Hyperf 的入口

    在启动命令中,除了 PHP 可执行文件以外,有两个是我们要关注的重点:

    • bin/hyperf.php:Hyperf 的入口文件
    • start:启动命令的参数

    先来看一下 bin/hyperf.php 文件,我将该文件的执行逻辑分成了四个阶段。

    初始化项目配置信息

    在这个阶段中,主要是通过调用一些 PHP 内置函数,完成 PHP 相关的配置初始化,比如运行内存大小限制、错误级别、时区等等。

    我们需要注意下在这一阶段定义的两个常量: BASE_PATH 和 SWOOLE_HOOK_FLAGS。

    • BASE_PATH:保存的是 Hyperf 项目所在目录的完整路径,Hyperf 中很多操作都是基于该常量来定位目录和文件的路径。
    • SWOOLE_HOOK_FLAGS:Swoole 采用 Hook 原生 PHP 函数的方式实现协程客户端,该常量保存的是 Hook 的函数的范围,比如套接字、文件、curl 等等。SWOOLE_HOOK_ALL 表示 Hook 所有函数。

    我们经常会在 Swoole 相关的资料文档中看到「一键协程化」技术,实际上指的就是在启用协程时传入 SWOOLE_HOOK_ALL 配置项,通过 Hook 所有函数,让项目中会发生 IO 阻塞的代码变成可以协程调度的异步 IO,即一键协程化。

    初始化类加载器

    在 Hyperf 中,我们可以使用注解减少一些繁琐的配置,还可以基于注解实现很多强大的功能。比如注解注入、AOP 面向切面编程、路由定义、权限控制等等。这些功能能够正常运行,其实都离不开类加载器在初始化过程中的准备工作。

    在初始化类加载器过程中,主要会进行以下操作:

    • 收集注解使用信息并完成注解收集器的初始化。
    • 生成代理类,为实现 AOP 及 Inject 注解注入功能做准备工作。
    • 生成运行时缓存,提高框架启动速度。

    初始化依赖注入容器

    在这个阶段, Hyperf 会先读取预先定义好的依赖关系的配置信息,包括 config/autoload/dependencies.php 配置文件中用户自定义的依赖关系,以及各组件中通过 ConfigProvider 机制定义的依赖关系。将这些初始的依赖关系保存到依赖注入容器中,完成对容器的初始化。

    初始化命令行应用

    我们回过头来看一下启动命令,你会发现,实际上 Hyperf 本身就是一个命令行应用,而启动命令中的 start 不过是命令行应用的参数,也就是要执行的命令的名字。

    在 Hyperf 中有很多内置的命令,比如 start、migrate、gen 等等,当然我们也可以根据自己的需求自定义命令。初始化命令行应用的过程,就是将这些 Hyperf 内置的命令、自定义的命令,注册到命令行应用中的过程。

    初始化并启动 HTTP 服务

    到了这里,Hyperf 的初始化工作就已经结束了,命令行应用就会开始对启动命令中的参数进行解析,通过参数找到在命令行应用中注册的命令并执行。参数 start 对应的命令类是 StartServer,你可以在 hyperf/server 组件中找到它。

    在 StartServer 中,完成了对 HTTP 服务的初始化以及启动操作,包含检查运行环境、读取服务配置文件、初始化 HTTP 服务、启动 HTTP 服务四个步骤,下面我们来了解一下这些步骤中分别做了哪些事情。

    检查运行环境

    我们知道,Hyperf 目前使用 Swoole 作为底层框架,所以在启动的时候,会先检查是否安装了 Swoole 的扩展,然后再检查是否禁用了 Swoole 的函数短名(short function name),如果没有禁用,就会输出提示信息并终止程序的运行。

    读取服务配置

    在 Hyperf 中,我们使用 config/autoload/server.php 文件来配置服务信息,详细的字段说明可以查看 Hyperf 官方文档

    其中有两个字段需要注意,分别是 server.typeserver.servers.type,很多人不太清楚这两个配置项的作用和区别,下面我们来了解一下。

    Swoole 提供了异步和协程两种风格的服务端,下面是两者的不同之处。

    • 协程风格的服务端可以在运行时动态创建、销毁,而异步风格的服务端在启动后就不能再对它进行操作了。
    • 协程风格的服务端对连接的处理是在单独的协程中完成,与客户端的交互是顺序性的,而异步风格的服务端无法保证顺序性。

    Hyperf 作为上层框架,当然要支持这两种风格的服务端,同时还要考虑到扩展性,方便后续接入其它风格的服务端。

    所以 Hyperf 在设计之初做了一层抽象,定义了一个 ServerInterface 接口,在接口中定义了三个常量,作为服务类型的枚举值。用于在配置文件中通过 server.servers.type 配置项设置服务的类型。同时,还定义了构造函数、初始化、启动三个方法。

    interface ServerInterface
    {    
    	// HTTP 服务  
        public const SERVER_HTTP = 1;    
        // Websocket 服务  
        public const SERVER_WEBSOCKET = 2;    
       // TCP 服务  
        public const SERVER_BASE = 3;    
       // 构造函数  
        public function __construct(ContainerInterface $container, LoggerInterface $logger, EventDispatcherInterface $dispatcher);    
       // 初始化  
        public function init(ServerConfig $config): ServerInterface;    
       // 启动  
        public function start(): void;
    }  
    

    Hyperf 不仅实现了基于 Swoole 的两种风格的服务端,还实现了基于 Swow 的服务端。

    • Hyperf\Server\Server:异步风格的服务端,由 Swoole 提供底层支持。
    • Hyperf\Server\CoroutineServer:协程风格的服务端,由 Swoole 提供底层支持。
    • Hyperf\Server\SwowServer:协程风格的服务端,由 Swow 提供底层支持。

    我们可以通过 server.type 配置项,来决定使用哪种风格的服务端用于运行各种类型的服务。当然,你也可以通过实现 ServerInterface 接口,自定义其它类型的服务端。

    初始化 HTTP 服务

    通过上面的内容你可以知道,在运行 Hyperf 的时候,只能使用一种服务端,但是可以运行多个不同类型的服务,比如 HTTP 服务、Websocket 服务等等。为了便于说明,我会使用异步风格服务端给你介绍初始化 HTTP 服务的过程。

    初始化 HTTP 服务的操作,是在 ServerFactory::configure 方法中完成的,主要可以分为两个步骤。

    • 第一步,将配置信息解析成 ServerConfig 对象。

    在这一步骤中,主要是将配置文件中数组形式的配置信息,解析成 ServerConfig 对象。

    class ServerConfig implements Arrayable
    {
        public function __construct(protected array $config = [])
        {
    	    // 将各种类型的服务解析成 Port 对象
            $servers = [];
            foreach ($config['servers'] as $item) {
                $servers[] = Port::build($item);
            }
    
    		// 将其它类型的配置都保存到对象中
            $this->setType($config['type'] ?? Server::class)
                ->setMode($config['mode'] ?? 0)
                ->setServers($servers)
                ->setProcesses($config['processes'] ?? [])
                ->setSettings($config['settings'] ?? [])
                ->setCallbacks($config['callbacks'] ?? []);
        }
    }
    

    当没有设置服务端的类型时,默认使用 Hyperf\Server\Server,即异步类型的服务端。

    • 第二步,调用 Hyperf\Server\Server::init 方法完成对 HTTP 服务的初始化。

    在这一步骤中,会调用 ServerFactory::getServer 方法,根据 ServerConfig 对象中的 type 属性实例化出对应的服务端对象,即 Hyperf\Server\Server 对象。在 Hyperf\Server\Server 对象中,定义了一个 server 属性,用于保存 Swoole 异步风格服务器对象。在 Swoole 异步风格的服务端中,有以下三种类型的服务器:

    • Swoole\Server:TCP 服务器,是所有异步风格服务器的基类。
    • Swoole\Http\Server:HTTP 服务器。
    • Swoole\WebSocket\Server:WebSocket 服务器。

    在 init 方法中,会根据 server.servers.type 配置项的值(即 ServerInterface 接口中的常量),实例化出相应的服务器对象,并保存到 server 属性中。

    这里会有一个问题,在 Hyperf\Server\Server 对象中只有一个 server 属性,但是,在 server.servers 配置项中,我们可以配置多个不同类型的服务,那么是如何支持运行多个服务的呢?

    这里就跟 Swoole 的服务器实现有关,Swoole 的异步风格服务器可以通过调用 addListener 方法监听多个端口,每个端口都可以设置不同的协议处理方式。这样就实现了一个服务器对象,同时运行多个不同类型的服务。

    下面我们来看一下 init 方法的主要逻辑。

    首先,在 init 方法中会先调用 ServerFactory::sortServers 方法,对需要启动的服务按照类型 Websocket、HTTP、TCP 的顺序进行排序。

    然后,依次遍历这些服务,完成对每个服务的初始化。循环中包括两个分支:

    • 第一个分支对应了 server 属性未初始化的情况。 此时,会调用 makeServer 方法实例化出相应的服务器对象,然后为服务器对象注册事件回调函数,最后初始化服务器对象的配置信息。
    • 第二个分支对应了 server 属性已初始化的情况。 此时,会调用服务器对象的 addListener 方法,增加一个端口并返回子服务器对象,然后为子服务器对象注册事件回调函数,最后初始化子服务器对象的配置信息。

    在 makeServer 方法中,会根据服务类型实例化出相应的服务器对象,下面代码展示了这部分的逻辑,你可以看下。

    switch ($type) {  
        case ServerInterface::SERVER_HTTP:  
            return new Swoole\Http\Server($host, $port, $mode, $sockType);  
        case ServerInterface::SERVER_WEBSOCKET:  
            return new Swoole\WebSocket\Server($host, $port, $mode, $sockType);  
        case ServerInterface::SERVER_BASE:  
            return new Swoole\Server($host, $port, $mode, $sockType);  
    }
    

    Swoole 提供了很多事件,比如 workerStart 工作进程启动后的事件、request 收到请求后的事件,这些事件在 Hyperf\Server\Event 中都有相应的常量。

    在 Hyperf 中,有三种事件回调函数的配置,分别是全局事件、服务事件、默认事件。

    • 全局事件:使用 server.callbacks 配置项设置全局的事件的回调函数。
    • 服务事件:使用 server.servers.callbacks 配置项为每一个服务单独设置事件的回调函数。
    • 默认事件:在 Hyperf\Server\Server 对象的 defaultCallbacks 方法中配置了一些默认的事件的回调函数。

    这些配置优先级是:服务事件 > 全局事件 > 默认事件。下面的代码展示了注册事件的回调函数的核心逻辑。

    // 按照优先级获取配置的所有事件及其回调函数
    $callbacks = array_replace($this->defaultCallbacks(), $config->getCallbacks(), $callbacks);
    foreach ($callbacks as $event => $callback) {
    	// 非 Swoole 事件,直接跳过
        if (! Event::isSwooleEvent($event)) {
            continue;
        }
        ...
        // 为服务器对象注册该事件的回调函数
        $server->on($event, $callback);
    }
    

    启动 HTTP 服务

    在启动 HTTP 服务之前,会执行以下代码设置一键协程化 Hook 的函数范围,swoole_hook_flags 函数的返回值就是 SWOOLE_HOOK_FLAGS 常量的值,即 SWOOLE_HOOK_ALL。

    Coroutine::set(['hook_flags' => swoole_hook_flags()]);
    

    接着会调用 ServerFactory::start 方法启动服务,在该方法中,直接调用 Hyperf\Server\Server 的 start 方法启动 Swoole 服务器。

    当 Swoole 服务器启动后,会执行注册在服务器对象的 Event::ON_WORKER_START 事件的回调函数 WorkerStartCallback::onWorkerStart。

    在 onWorkerStart 方法中,输出 Worker#{$workerId} started. 日志信息,并通过事件分发器分发 AfterWorkerStart 事件,在该事件的监听器 AfterWorkerStartListener 中,输出 HTTP Server listening at 0.0.0.0:9501 日志信息。

    到这里,HTTP 服务就已经启动了。

    总结

    在这篇文章中,我们通过 bin/hyperf.php 文件,了解了 Hyperf 在初始化框架时会执行哪些操作。接着,又通过 StartServer 了解了 HTTP 服务在启动过程中的四个步骤。其中,HTTP 服务的初始化是整个启动过程中的关键步骤,你可以配合源码进一步了解 Hyperf 的设计和实现思路。

    尽管本文的主题是 HTTP 服务,但实际上,无论是 WebSocket服务、TCP服务还是其他类型的服务,这些服务的启动过程与 HTTP 服务的启动过程大同小异。

    因此,掌握 HTTP 服务的启动过程,不仅有助于你了解 HTTP 服务的运行细节,还有助于你了解 Hyperf 以及其它类型服务的运行细节。当你遇到问题时,可以按照启动过程中的步骤逐步检查,从而帮助你更快地解决问题。

  • 相关阅读:
    在矩池云上使用R和RStudio
    服装商城网站 毕业设计-附源码241505
    30 数据分析(上)(业务略写可跳)|jupyter|matplotlib
    Connor学Android - Drawable
    排序,瀑布流和分页,子查询
    xxl-job源码解读:触发器线程池TriggerPool
    坚固可靠的多合一轨道交通天线让您的赏秋路途不再枯燥
    OAuth2:资源服务器
    第八章《Java高级语法》第10节:注解
    Spring-依赖注入findAutowireCandidates源码实现
  • 原文地址:https://www.cnblogs.com/her-cat/p/what-happens-when-hyperf-http-server-starts.html