深入 Hyperf:HTTP 服务是如何处理请求的?

2023-12-30
次阅读
5 分钟阅读时长

上一篇文章介绍了 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 请求

通过上面的内容我们可以知道,处理 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 服务处理请求的整个过程。通过阅读本文,你将更加深入地了解 HTTP 服务的内部运作机制,这为你进一步使用和研究 HTTP 服务提供了坚实的基础。

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