惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Security Latest
Security Latest
C
Cisco Blogs
P
Palo Alto Networks Blog
Know Your Adversary
Know Your Adversary
Project Zero
Project Zero
C
Cyber Attacks, Cyber Crime and Cyber Security
NISL@THU
NISL@THU
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Secure Thoughts
P
Privacy International News Feed
V
Vulnerabilities – Threatpost
D
Docker
Google Online Security Blog
Google Online Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Recent Announcements
Recent Announcements
T
The Exploit Database - CXSecurity.com
G
Google Developers Blog
Schneier on Security
Schneier on Security
小众软件
小众软件
爱范儿
爱范儿
GbyAI
GbyAI
J
Java Code Geeks
T
Tailwind CSS Blog
Cisco Talos Blog
Cisco Talos Blog
The Hacker News
The Hacker News
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
TaoSecurity Blog
TaoSecurity Blog
MyScale Blog
MyScale Blog
B
Blog RSS Feed
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Securelist
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Y
Y Combinator Blog
S
Schneier on Security
Latest news
Latest news
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 叶小钗
F
Fortinet All Blogs
M
MIT News - Artificial intelligence
PCI Perspectives
PCI Perspectives
V
V2EX
V2EX - 技术
V2EX - 技术
O
OpenAI News
W
WeLiveSecurity

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 深入 Hyperf:Inject 注解是如何工作的? 译|理解容器镜像层 使用 Rust 实现高性能预写日志(Write Ahead Log) 译|使用 Linux 命名空间隔离系统 Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 排查 ES 查询问题:深入了解 json_encode() 函数 浅析 Redlock 分布式锁实现原理 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:客户端的同步调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 Workerman 源码分析:文件上传 浅入浅出 HTTP 协议 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 优化 Workerman 检查主进程是否存活的逻辑 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 基于 Redis 实现延迟队列 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
深入 Hyperf:HTTP 服务是如何处理请求的?
她和她的猫 · 2023-12-30 · via 文章列表 - 她和她的猫

上一篇文章介绍了 HTTP 服务启动时发生了什么,让我们对于 Hyperf 框架以及 HTTP 服务的启动过程有了更进一步的理解。今天我们继续来了解一下,当我们访问 HTTP 服务时,它是如何处理请求并返回响应内容的。

在 HTTP 服务启动时,Hyperf 会向 Swoole 注册请求事件处理函数。当收到 HTTP 请求时,Swoole 会调用该函数。

在 HTTP 服务启动后,当我们向 HTTP 服务发送请求时,HTTP 请求会被发送到 Swoole 中。Swoole 会将 HTTP 报文解析成 HTTP 请求对象,并构造出一个 HTTP 响应对象。

接着,Swoole 会调用请求事件处理函数,将请求和响应对象作为参数传递给该函数。在该函数中完成对请求的处理,并调用响应对象发送响应内容。

在这篇文章中,我们将分三个部分介绍 Hyperf 中 HTTP 服务处理请求的过程,在第一部分将会介绍如何注册 HTTP 服务的请求事件处理函数,第二部分会介绍如何处理 HTTP 请求并发送响应内容。

注册请求事件处理函数

在 HTTP 服务启动时,Hyperf 需要向 Swoole 注册请求事件处理函数,那么在 Hyperf 中这个处理函数是什么呢?

在 HTTP 服务「读取服务配置」阶段,会从 config/autoload/server.php 配置文件中获取配置信息,其中 Event::ON_REQUEST 是请求事件的枚举值,Hyperf\HttpServer\Server::onRequest() 就是请求事件的处理函数。

return [
    ...
    'servers' => [
        [
            ...
            'callbacks' => [
                Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'],
            ],
        ],
    ],
    ...
]

在「初始化 HTTP 服务」阶段,将请求事件处理函数注册到 Swoole 中,这部分工作的主要内容如下所示:

foreach ($events as $event => $callback) {
    ...
    [$className, $method] = $callback;    
    $class = $this->container->get($className);
    if (method_exists($class, 'setServerName')) {
        // 设置服务名称
        $class->setServerName($serverName);
    }    // 初始化核心中间件
    if ($class instanceof MiddlewareInitializerInterface) {
        $class->initCoreMiddleware($serverName);
    }
    ...
    // 注册事件处理函数
    $server->on($event, $callback);
}

initCoreMiddleware() 方法中,初始化了 HTTP 服务的路由信息,并且通过服务名称获取该 HTTP 服务的中间件以及异常处理器。

public function initCoreMiddleware(string $serverName): void  {    
    $this->serverName = $serverName;  
    $this->coreMiddleware = $this->createCoreMiddleware();    
    $config = $this->container->get(ConfigInterface::class);    
    $this->middlewares = $config->get('middlewares.' . $serverName, []);    
    $this->exceptionHandlers = $config->get('exceptions.handler.' . $serverName, $this->getDefaultExceptionHandler());  
}  

通过上面的内容我们可以知道,处理 HTTP 请求的逻辑是在 Hyperf\HttpServer\Server::onRequest() 方法中完成的,该方法的原型如下:

public function onRequest($request, $response): void  

对于 Hyperf\HttpServer\Server::onRequest() 方法来说,我把它执行的工作分成了以下几个阶段。

初始化请求和响应对象

在这一阶段中,需要初始化 PSR-7 请求和响应对象。这是因为,Hyperf 的标准组件都是基于 PSR 标准实现的,而底层框架可能并没有基于 PSR 标准实现,因此需要先进行兼容性适配。

[$psr7Request, $psr7Response] = $this->initRequestAndResponse($request, $response);  

initRequestAndResponse() 方法中,先判断对象是否基于 PSR 标准,如果不是,则将其转换成 PSR-7 请求和响应对象。

匹配请求的路由信息

在这一阶段中,调用 Hyperf\HttpServer\CoreMiddleware::dispatch() 方法,使用上面初始化好的 PSR-7 请求对象匹配路由信息。

$psr7Request = $this->coreMiddleware->dispatch($psr7Request);  

dispatch() 方法中,使用请求对象的请求方式、请求地址匹配 HTTP 服务中的路由信息,将匹配结果转换成 Hyperf\HttpServer\Router\Dispatched 对象,并保存到新的请求对象的属性中并返回。

public function dispatch(ServerRequestInterface $request): ServerRequestInterface  
{  
    $routes = $this->dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());  
  
    $dispatched = new Dispatched($routes);  
    return Context::set(ServerRequestInterface::class, $request->withAttribute(Dispatched::class, $dispatched));  
}  

看到这里,你可能会有疑问,为什么不将 Dispatched 直接保存到原来的请求对象的属性中?

因为这是 PSR-7 标准规定的。在 PSR-7 标准中,请求被认为是不可变的;必须实现所有可能更改状态的方法,以便它们保留当前请求的内部状态,并返回包含更改状态的实例。

也就是说,如果你想修改请求对象中的信息,那么你必须从当前对象中克隆一个新的对象,然后在新的对象中进行修改并返回新的对象。下面是 withAttribute 方法的实现:

public function withAttribute($name, $value)  
{  
    $clone = clone $this;  
    $clone->attributes[$name] = $value;  
    return $clone;
}  

知道了这一点之后,相信你以后再也不会有「为什么我设置了属性但是却拿不到值」这种疑惑了。

准备全局和路由中间件

在 HTTP 服务中,中间件根据作用范围分为两种:全局中间件和路由中间件。

全局中间件会被应用到所有路由上,而路由中间件仅应用到部分路由上,是否应用需要根据路由匹配结果来决定。如果匹配到了路由,则从中间件管理器中获取该路由的中间件。否则,仅使用全局中间件。

$dispatched = $psr7Request->getAttribute(Dispatched::class);  
// 获取全局中间件  
$middlewares = $this->middlewares;  
// 判断是否匹配到了路由  
if ($dispatched->isFound()) {  
    // 通过服务名称、请求地址、请求方式获取路由中间件  
    $registeredMiddlewares = MiddlewareManager::get($this->serverName, $dispatched->handler->route, $psr7Request->getMethod());  
    $middlewares = array_merge($middlewares, $registeredMiddlewares);  
}  

在一切准备就绪后,Hyperf 会调用 Hyperf\Dispatcher\HttpDispatcher::dispatch() 方法,将请求对象依次交给每个中间件进行处理,然后调用核心中间件的 Hyperf\HttpServer\CoreMiddleware::process() 方法进行最终处理。

$psr7Response = $this->dispatcher->dispatch($psr7Request, $middlewares, $this->coreMiddleware);  

执行路由处理函数

Hyperf\HttpServer\CoreMiddleware::process() 方法中,主要逻辑是根据 Dispatcher 对象的状态执行相应的动作。

  • 如果路由不存在,则抛出 NotFoundHttpException 异常。
  • 如果请求方式不正确,则抛出 MethodNotAllowedHttpException 异常。
  • 如果找到路由,则解析并执行绑定在该路由上的处理函数,然后返回响应对象。

以下代码展示了这部分的执行逻辑。

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    ...
    /** @var Dispatched $dispatched */
    $dispatched = $request->getAttribute(Dispatched::class);
    // 根据 `Dispatcher` 对象的状态执行相应动作
    $response = match ($dispatched->status) {
        Dispatcher::NOT_FOUND => $this->handleNotFound($request),
        Dispatcher::METHOD_NOT_ALLOWED => $this->handleMethodNotAllowed($dispatched->params, $request),
        Dispatcher::FOUND => $this->handleFound($dispatched, $request),
        default => null,
    };
    ...
}

Hyperf 支持闭包和请求处理器两种方式设置路由的处理函数,以下是这两种方式的示例。

// 闭包  
Router::get('/hello-hyperf', function () {  
    return 'Hello Hyperf.';
});  
  
// 请求处理器,下面三种方式的任意一种都可以达到同样的效果  
Router::get('/hello-hyperf', 'App\Controller\IndexController::hello');  
Router::get('/hello-hyperf', 'App\Controller\IndexController@hello');  
Router::get('/hello-hyperf', [App\Controller\IndexController::class, 'hello']);  

handleFound() 方法中,如果处理函数是闭包,则调用 parseClosureParameters() 方法解析闭包的参数,然后运行闭包。

if ($dispatched->handler->callback instanceof Closure) {    
    $parameters = $this->parseClosureParameters($dispatched->handler->callback, $dispatched->params);    
    $callback = $dispatched->handler->callback;    
    $response = $callback(...$parameters);
}  

如果处理函数是请求处理器,则调用 prepareHandler() 方法解析出请求处理器中的控制器(Controller)和操作(Action),通过容器实例化出控制器对象,然后调用 parseMethodParameters() 方法解析操作方法的参数,最后运行该方法。

[$controller, $action] = $this->prepareHandler($dispatched->handler->callback);  
$controllerInstance = $this->container->get($controller);  
...  
$parameters = $this->parseMethodParameters($controller, $action, $dispatched->params);  
$response = $controllerInstance->{$action}(...$parameters);  

处理函数执行完成后,如果返回的结果没有实现 ResponseInterface 接口,则调用 transferToResponse() 方法对其进行转换,最后返回响应对象。

if (! $response instanceof ResponseInterface) {  
    $response = $this->transferToResponse($response, $request);  
}  
return $response->withAddedHeader('Server', 'Hyperf');  

发送响应内容

有了响应对象之后,就需要将响应内容发送给客户端,这部分工作通过调用 Hyperf\HttpServer\ResponseEmitter::emit() 方法完成。

// Send the Response to client.  
if (! isset($psr7Response) || ! $psr7Response instanceof ResponseInterface) {  
    return;
}  
if (isset($psr7Request) && $psr7Request->getMethod() === 'HEAD') {  
    $this->responseEmitter->emit($psr7Response, $response, false);  
} else {  
    $this->responseEmitter->emit($psr7Response, $response);  
}  

emit() 方法中,将 PSR-7 响应对象中的响应头、Cookies 以及状态码等信息写入到 Swoole 的响应对象中。然后判断响应体是否为文件对象,如果是则调用 Swoole\Http\Response::sendfile() 方法将文件发送到客户端;否则调用 Swoole\Http\Response::end() 方法发送响应内容。

public function emit(ResponseInterface $response, mixed $connection, bool $withContent = true): void
{
    try {
        if (strtolower($connection->header['Upgrade'] ?? '') === 'websocket') {
            return;
        }
        // 将 PSR-7 响应对象的信息写入到 Swoole 的响应对象中
        $this->buildSwooleResponse($connection, $response);
        // 判断响应内容是否为文件
        $content = $response->getBody();
        if ($content instanceof FileInterface) {
            // 发送文件到客户端
            $connection->sendfile($content->getFilename());
            return;
        }
        // 发送响应内容
        if ($withContent) {
            $connection->end((string) $content);
        } else {
            $connection->end();
        }
    } catch (Throwable $exception) {
        $this->logger?->critical((string) $exception);
    }
}

最后,我们就能够在浏览器中看到响应内容了。

总结

在这篇文章中,我们了解了 Hyperf 处理 HTTP 请求的完整流程。希望这些内容能帮你更好地理解 Hyperf 的工作原理。