5

深入 Hyperf:HTTP 服务启动时发生了什么? - 她和她的猫_her-cat

 1 year ago
source link: https://www.cnblogs.com/her-cat/p/what-happens-when-hyperf-http-server-starts.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK