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