首页 > 其他分享 >深入 Hyperf:HTTP 服务启动时发生了什么?

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

时间:2023-05-19 14:55:28浏览次数:67  
标签:初始化 服务 Swoole Server 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 配置项设置服务的类型。同时,还定义了构造函数、初始化、启动三个方法。

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

标签:初始化,服务,Swoole,Server,Hyperf,深入,HTTP
From: https://www.cnblogs.com/her-cat/p/what-happens-when-hyperf-http-server-starts.html

相关文章

  • Chrome更新全新设计的 HTTPS 图标
    导读HTTPS是一种用于安全地传输数据的网络协议。HTTPS是在HTTP协议的基础上添加了SSL/TLS协议,通过对数据进行加密,从而保证数据传输的安全性。HTTPS常用于保护敏感信息,如个人身份信息、银行账户信息、信用卡信息等。目前市面上所有的浏览器都会在与网站通过HTTPS......
  • 数据库页面 — 深入探究
    数据库通常使用固定大小的页面来存储数据。表、集合、行、列、索引、序列、文档等最终都以字节形式存在页面中。这样,存储引擎就可以与负责数据格式和API的数据库前端分离开来。此外,当一切都是页面时,更容易读取、写入或缓存数据。下面是SQLServer页面布局的示例。在本文中,我......
  • Netty集成HTTP的GET和POST通讯
    核心就是ChannelInitializer的实现使用http消息解码器packagecom.coremain.handler;importio.netty.channel.ChannelInitializer;importio.netty.channel.socket.SocketChannel;importio.netty.handler.codec.http.HttpObjectAggregator;importio.netty.handler.codec......
  • 深入理解之JavaScript之call, apply, bind方法
    在JavaScript中,call、apply和bind是Function对象自带的三个方法,这三个方法的主要作用是改变函数执行时的上下文,再具体一点就是改变函数运行时的this指向。Function.prototype.call()call()方法调用一个函数,其具有一个指定的this值和多个参数(参数的列表)。fun.call(thisArg,a......
  • Apipost Error: Invalid URI "http:///%20"
    1.情景展示使用Apipost调接口时,出现如上图所示的错误。2.具体分析去他们官网,帮助文档,连个搜索功能都没有,也是服了。说明这个请求地址有问题,但实际我把请求地址放到浏览器当中是可以正常访问的。3.解决方案百思不得其解。最后,发现:原来,是自己在复制URL的时候,前面多了一个......
  • 抓包工具Charles:(三)https协议的抓取
    当我们在抓包是,经常遇到https协议,抓取的内容不是乱码就是unknown。例如: 解决的办法很简单:第一步:在charles中安装ssl协议      第二步:  ......
  • https证书如何导入
    提问https证书如何导入回答C#生成自签名证书varrsa=RSA.Create();varx500Name=newX500DistinguishedName("CN=[通用名],O=[公司],OU=[单位]");varreq=newCertificateRequest(x500Name,rsa,HashAlgorithmName.SHA256,RSASignaturePadding.Pkcs1);varsan......
  • HTTP状态码的学习&记录
    一、什么是HTTP状态码?HTTP状态码是指在客户端向服务器发送请求时,服务器返回的响应状态码。二、HTTP状态码都有哪一些?客户端:指使用网络或分布式系统服务的终端用户设备或软件,通常是指桌面电脑、笔记本电脑、智能手机、平板电脑等计算机设备。服务端:指提供网络或分......
  • http协议
    HTTP协议1.http协议定义协议是指计算机网络中,两台计算机之间进行通信所必须共同遵守的规定或规则。HTTP协议(超文本传输协议HyperTextTransferProtocol),你浏览的每一个网页都是基于HTTP协议呈现的,HTTP协议是互联网应用中,客户端(浏览器)与服务器之间进行数据通......
  • HTTP1.0、HTTP1.1、HTTP2.0 协议的特点
    HTTP(HypertextTransferProtocol)是一种用于在Web浏览器和Web服务器之间传输数据的协议。HTTP的版本有很多,其中比较常见的有HTTP1.0、HTTP1.1和HTTP2.0,它们有各自的特点。HTTP1.0的特点:1.每个请求/响应需要建立新的TCP连接,并在完成后关闭连接,导致较高的延迟。2.......