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

2023-05-15
次阅读
9 分钟阅读时长

当我们创建 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 服务的实现思路。

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 以及其它类型服务的运行细节。当你遇到问题时,可以按照启动过程中的步骤逐步检查,从而帮助你更快地解决问题。

本文作者:她和她的猫
本文地址https://her-cat.com/posts/2023/05/15/what-happens-when-hyperf-http-server-starts/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!