tankonyako/reactphp-http-server

ReactPHP 的基于事件的、流式传输的纯文本 HTTP 和安全 HTTPS 服务器

dev-main 2023-01-09 12:40 UTC

This package is not auto-updated.

Last update: 2024-10-01 18:49:38 UTC


README

Build Status

ReactPHP 的基于事件的、流式传输的纯文本 HTTP 和安全 HTTPS 服务器。

目录

快速入门示例

这是一个响应所有请求时返回 "Hello World!" 的 HTTP 服务器。

$loop = React\EventLoop\Factory::create();

$server = new Server(function (ServerRequestInterface $request) {
    return new Response(
        200,
        array(
            'Content-Type' => 'text/plain'
        ),
        "Hello World!\n"
    );
});

$socket = new React\Socket\Server(8080, $loop);
$server->listen($socket);

$loop->run();

请参阅示例

用法

服务器

Server 类负责处理传入的连接,然后处理每个传入的 HTTP 请求。

它在内存中缓冲并解析完整的传入 HTTP 请求。一旦收到完整的请求,它将调用请求处理函数。这个请求处理函数需要传递给构造函数,并且将使用相应的 请求 对象调用,并期望返回一个 响应 对象。

$server = new Server(function (ServerRequestInterface $request) {
    return new Response(
        200,
        array(
            'Content-Type' => 'text/plain'
        ),
        "Hello World!\n"
    );
});

每个传入的 HTTP 请求消息总是由 PSR-7 ServerRequestInterface 表示,有关详细信息,请参阅以下 请求 章节。每个传出的 HTTP 响应消息总是由 PSR-7 ResponseInterface 表示,有关详细信息,请参阅以下 响应 章节。

为了处理任何连接,服务器需要通过 listen() 方法连接到 React\Socket\ServerInterface 的一个实例,如下章所述。在其最简单的形式中,您可以将其连接到 React\Socket\Server 以启动一个纯文本 HTTP 服务器,如下所示

$server = new Server($handler);

$socket = new React\Socket\Server('0.0.0.0:8080', $loop);
$server->listen($socket);

请参阅 listen() 方法及其 第一个示例 以获取更多详细信息。

Server 类作为底层 StreamingServer 的外观构建,为 80% 的用例提供合理的默认值,并且是使用此库的推荐方法,除非您确定自己知道在做什么。

与底层StreamingServer不同,这个类在内存中缓冲并解析完整的传入HTTP请求。一旦收到完整的请求,它将调用请求处理函数。这意味着传递给您的请求处理函数的请求将与PSR-7完全兼容。

另一方面,在内存中缓冲完整的HTTP请求直到它们可以被请求处理函数处理,这意味着这个类必须采用一些限制来避免消耗过多的内存。为了更高级的配置,它尊重您的php.ini设置来应用其默认设置。以下是这个类尊重的PHP设置及其相应的默认值列表

memory_limit 128M
post_max_size 8M
enable_post_data_reading 1
max_input_nesting_level 64
max_input_vars 1000

file_uploads 1
upload_max_filesize 2M
max_file_uploads 20

特别是,post_max_size设置限制了单个HTTP请求在缓冲其请求体时允许消耗多少内存。在此之上,这个类将尝试避免消耗超过您memory_limit的1/4来缓冲多个并发HTTP请求。因此,在默认设置128M最大值的情况下,它将尝试消耗不超过32M来缓冲多个并发HTTP请求。因此,它将以默认设置限制并发为4个HTTP请求。

您必须为您的PHP ini设置分配合理的值。通常建议要么减少单个请求允许使用的内存(设置post_max_size 1M或更少),要么增加总内存限制以允许更多的并发请求(设置memory_limit 512M或更多)。否则,这个类可能不得不禁用并发,一次只处理一个请求。

内部,这个类自动将这些限制分配给如下描述的中间件请求处理器。对于更高级的使用场景,您还可以使用高级的StreamingServer,并按照以下章节描述的方式自己分配这些中间件请求处理器。

StreamingServer

高级的StreamingServer类负责处理传入的连接,然后处理每个传入的HTTP请求。

Server类不同,它默认不会缓冲和解析传入的HTTP请求体。这意味着请求处理函数将使用流式请求体被调用。一旦接收到请求头,它将调用请求处理函数。这个请求处理函数需要传递给构造函数,并将使用相应的请求对象调用,并期望返回一个响应对象。

$server = new StreamingServer(function (ServerRequestInterface $request) {
    return new Response(
        200,
        array(
            'Content-Type' => 'text/plain'
        ),
        "Hello World!\n"
    );
});

每个传入的 HTTP 请求消息总是由 PSR-7 ServerRequestInterface 表示,有关详细信息,请参阅以下 请求 章节。每个传出的 HTTP 响应消息总是由 PSR-7 ResponseInterface 表示,有关详细信息,请参阅以下 响应 章节。

为了处理任何连接,服务器需要通过 listen() 方法连接到 React\Socket\ServerInterface 的一个实例,如下章所述。在其最简单的形式中,您可以将其连接到 React\Socket\Server 以启动一个纯文本 HTTP 服务器,如下所示

$server = new StreamingServer($handler);

$socket = new React\Socket\Server('0.0.0.0:8080', $loop);
$server->listen($socket);

请参阅 listen() 方法及其 第一个示例 以获取更多详细信息。

StreamingServer类被视为高级用法,除非您知道自己在做什么,否则建议您使用Server类。StreamingServer类是专门设计来帮助处理更高级的使用场景,在这些场景中,您希望完全控制消费传入的HTTP请求体和并发设置。

特别是,这个类不会在内存中缓冲和解析传入的HTTP请求。它将在接收到HTTP请求头后调用请求处理函数,即在接收可能更大的HTTP请求体之前。这意味着传递给您的请求处理函数的请求可能不完全与PSR-7兼容。有关更多详细信息,请参阅以下流式请求

listen()

listen(React\Socket\ServerInterface $socket): void 方法可以用来启动处理来自指定套接字服务器的连接。给定的 React\Socket\ServerInterface 负责发射底层的流连接。此 HTTP 服务器需要连接到它,以便处理任何连接并将传入的流数据作为传入的 HTTP 请求消息。在其最常见的形式中,您可以将其连接到一个 React\Socket\Server 以启动一个类似这样的纯文本 HTTP 服务器

$server = new Server($handler);
// or
$server = new StreamingServer($handler);

$socket = new React\Socket\Server('0.0.0.0:8080', $loop);
$server->listen($socket);

此示例将在所有接口(公开)上监听 HTTP 请求的备用 HTTP 端口 8080。作为替代方案,使用监听地址 127.0.0.1:8080 仅让此 HTTP 服务器在 localhost(回环)接口上监听,这是非常常见的。这样,您可以在默认的 HTTP 端口 80 上托管应用程序,并且仅将特定请求路由到此 HTTP 服务器。

同样,通常建议使用反向代理设置来接受默认 HTTPS 端口 443 上的安全 HTTPS 请求(TLS 终止)并将纯文本请求路由到此 HTTP 服务器。作为替代方案,您还可以通过将此连接到 React\Socket\Server 并使用安全的 TLS 监听地址、证书文件以及可选的 passphrase 来接受安全 HTTPS 请求

$server = new Server($handler);
// or
$server = new StreamingServer($handler);

$socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array(
    'local_cert' => __DIR__ . '/localhost.pem'
));
$server->listen($socket);

有关更多详细信息,请参阅 示例 #11

请求

如上所示,ServerStreamingServer 类负责处理传入的连接,然后处理每个传入的 HTTP 请求。

请求对象将在客户端接收到请求后进行处理。此请求对象实现了 PSR-7 ServerRequestInterface,该接口又扩展了 PSR-7 RequestInterface,并将按如下方式传递给回调函数。

$server = new Server(function (ServerRequestInterface $request) {
   $body = "The method of the request is: " . $request->getMethod();
   $body .= "The requested path is: " . $request->getUri()->getPath();

   return new Response(
       200,
       array(
           'Content-Type' => 'text/plain'
       ),
       $body
   );
});

有关请求对象的更多详细信息,请参阅 PSR-7 ServerRequestInterfacePSR-7 RequestInterface 的文档。

请求参数

可以使用 getServerParams(): mixed[] 方法获取与 $_SERVER 变量类似的服务器端参数。目前有以下参数可用

  • REMOTE_ADDR 请求发送者的 IP 地址
  • REMOTE_PORT 请求发送者的端口
  • SERVER_ADDR 服务器的 IP 地址
  • SERVER_PORT 服务器端口
  • REQUEST_TIME 完整请求头接收到的 Unix 时间戳,整数,类似于 time()
  • REQUEST_TIME_FLOAT 完整请求头接收到的 Unix 时间戳,浮点数,类似于 microtime(true)
  • HTTPS 如果请求使用了 HTTPS,则设置为 'on',否则不会设置
$server = new Server(function (ServerRequestInterface $request) {
    $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR'];

    return new Response(
        200,
        array(
            'Content-Type' => 'text/plain'
        ),
        $body
    );
});

有关更多详细信息,请参阅 示例 #3

高级:请注意,如果您在 Unix 域套接字(UDS)路径上监听,则地址参数将不会被设置,因为此协议没有主机/端口的概念。

查询参数

可以使用 getQueryParams(): array 方法获取与 $_GET 变量类似的查询参数。

$server = new Server(function (ServerRequestInterface $request) {
    $queryParams = $request->getQueryParams();

    $body = 'The query parameter "foo" is not set. Click the following link ';
    $body .= '<a href="/?foo=bar">to use query parameter in your request</a>';

    if (isset($queryParams['foo'])) {
        $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']);
    }

    return new Response(
        200,
        array(
            'Content-Type' => 'text/html'
        ),
        $body
    );
});

以上示例中的响应将返回一个包含链接的响应体。URL 包含查询参数 foo,其值为 bar。像这个示例一样使用 htmlentities 可以防止 跨站脚本(简称 XSS)

另请参阅 示例 #4

请求体

如果您使用的是 Server,则请求对象将在内存中缓冲和解析,并包含完整的请求体。这包括解析后的请求体和任何文件上传。

如果您使用的是高级的 StreamingServer 类,请跳到下一章以了解更多关于如何处理 流式请求 的信息。

如上所述,每个传入的 HTTP 请求总是由 PSR-7 ServerRequestInterface 表示。此接口提供了一些方法,如以下所述,这些方法在处理传入请求体时很有用。

可以使用 getParsedBody(): null|array|object 方法来获取解析后的请求体,类似于 PHP 的 $_POST 变量。此方法可能返回一个(可能嵌套的)数组结构,包含所有请求体参数,或者如果请求体无法解析,则返回 null 值。默认情况下,此方法将仅返回使用 Content-Type: application/x-www-form-urlencodedContent-Type: multipart/form-data 请求头的请求的解析数据(通常用于 HTML 表单提交数据的 POST 请求)。

$server = new Server(function (ServerRequestInterface $request) {
    $name = $request->getParsedBody()['name'] ?? 'anonymous';

    return new Response(
        200,
        array(),
        "Hello $name!\n"
    );
});

有关更多详细信息,请参阅 示例 #12

可以使用 getBody(): StreamInterface 方法来获取此请求体的原始数据,类似于 PHP 的 php://input 流。此方法返回一个表示请求体的 PSR-7 StreamInterface 实例。这在使用默认情况下不会解析的自定义请求体时特别有用,例如 JSON(Content-Type: application/json)或 XML(Content-Type: application/xml)请求体(这些常用于基于 JSON 的或 RESTful/RESTish API 中的 POSTPUTPATCH 请求)。

$server = new Server(function (ServerRequestInterface $request) {
    $data = json_decode((string)$request->getBody());
    $name = $data->name ?? 'anonymous';

    return new Response(
        200,
        array('Content-Type' => 'application/json'),
        json_encode(['message' => "Hello $name!"])
    );
});

有关更多详细信息,请参阅 示例 #9

可以使用 getUploadedFiles(): array 方法来获取此请求中的上传文件,类似于 PHP 的 $_FILES 变量。此方法返回一个(可能嵌套的)数组结构,包含所有文件上传,每个上传都由 PSR-7 UploadedFileInterface 表示。此数组仅在使用 Content-Type: multipart/form-data 请求头时才会填充(通常用于 HTML 文件上传的 POST 请求)。

$server = new Server(function (ServerRequestInterface $request) {
    $files = $request->getUploadedFiles();
    $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing';

    return new Response(
        200,
        array(),
        "Uploaded $name\n"
    );
});

有关更多详细信息,请参阅 示例 #12

可以使用 getSize(): ?int 方法来获取请求体的大小,类似于 PHP 的 $_SERVER['CONTENT_LENGTH'] 变量。此方法返回以字节为单位的请求体完整大小,由消息边界定义。如果请求消息不包含请求体(例如简单的 GET 请求),则此值可能为 0。此方法在缓冲的请求体上操作,即请求体大小始终已知,即使请求未指定 Content-Length 请求头或使用 Transfer-Encoding: chunked 进行 HTTP/1.1 请求。

注意:服务器自动处理带有附加的 Expect: 100-continue 请求头的请求。当 HTTP/1.1 客户端想要发送较大的请求体时,它们可以只发送请求头,并附加一个 Expect: 100-continue 请求头,然后等待再发送实际的(大的)消息体。在这种情况下,服务器将自动向客户端发送一个中间的 HTTP/1.1 100 Continue 响应。这确保您将按预期收到请求体,而不会有延迟。

流式请求

如果您使用的是高级的 StreamingServer,请求对象将在接收到请求头后立即处理。这意味着这发生在接收到(可能更大的)请求体之前。

如果您使用的是 Server 类,请跳转到上一章以了解如何处理缓冲的 请求体 的更多信息。

虽然在 PHP 生态系统中这可能是罕见的,但实际上这是一种非常强大的方法,它提供了其他方法无法实现的几个优势。

  • 在接收到大请求体之前对请求做出反应,例如拒绝未认证的请求或超出允许的消息长度(文件上传)的请求。
  • 在请求体的其余部分到达之前或发送者缓慢流式传输数据之前开始处理请求体的部分。
  • 在不需要在内存中缓冲任何内容的情况下处理大请求体,例如接受巨大的文件上传或可能无限请求体流。

getBody(): StreamInterface 方法可以用来访问请求体流。在流式模式下,此方法返回一个实现 PSR-7 StreamInterfaceReactPHP ReadableStreamInterface 的流实例。然而,大多数 PSR-7 StreamInterface 方法都是基于同步请求体的假设设计的。鉴于这不适用于该服务器,以下 PSR-7 StreamInterface 方法不使用,也不应该调用:tell()eof()seek()rewind()write()read()。如果这对您的用例是一个问题,并且/或者您想访问上传的文件,强烈建议使用缓冲的 请求体 或使用 RequestBodyBufferMiddleware

$server = new StreamingServer(function (ServerRequestInterface $request) {
    return new Promise(function ($resolve, $reject) use ($request) {
        $contentLength = 0;
        $request->getBody()->on('data', function ($data) use (&$contentLength) {
            $contentLength += strlen($data);
        });

        $request->getBody()->on('end', function () use ($resolve, &$contentLength){
            $response = new Response(
                200,
                array(
                    'Content-Type' => 'text/plain'
                ),
                "The length of the submitted request body is: " . $contentLength
            );
            $resolve($response);
        });

        // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event
        $request->getBody()->on('error', function (\Exception $exception) use ($resolve, &$contentLength) {
            $response = new Response(
                400,
                array(
                    'Content-Type' => 'text/plain'
                ),
                "An error occured while reading at length: " . $contentLength
            );
            $resolve($response);
        });
    });
});

上面的示例只是简单地计算请求体中接收的字节数。这可以用作缓冲或处理请求体的框架。

有关更多信息,请参阅示例 #13

每当请求体流上有新数据可用时,都会发出 data 事件。服务器还会自动处理使用 Transfer-Encoding: chunked 的任何传入请求,并且只发出实际的有效负载作为数据。

当请求体流成功终止时,即它被读取到预期的结尾时,将发出 end 事件。

如果请求流包含对 Transfer-Encoding: chunked 无效的数据或连接在收到完整的请求流之前关闭,将发出 error 事件。服务器将自动停止从连接读取,并丢弃所有传入的数据而不是关闭连接。仍然可以发送响应消息(除非连接已经关闭)。

在发生 errorend 事件后,将触发一个 close 事件。

有关请求体流的更多详细信息,请参阅 ReactPHP ReadableStreamInterface 文档。

可以使用 getSize(): ?int 方法来获取请求体的大小,类似于 PHP 的 $_SERVER['CONTENT_LENGTH'] 变量。此方法返回请求体的大小(以字节为单位),由消息边界定义。如果请求消息不包含请求体(例如简单的 GET 请求),则此值可能为 0。此方法在流式请求体上操作,即在使用 Transfer-Encoding: chunked 对 HTTP/1.1 请求进行编码时,请求体大小可能未知(null)。

$server = new StreamingServer(function (ServerRequestInterface $request) {
    $size = $request->getBody()->getSize();
    if ($size === null) {
        $body = 'The request does not contain an explicit length.';
        $body .= 'This example does not accept chunked transfer encoding.';

        return new Response(
            411,
            array(
                'Content-Type' => 'text/plain'
            ),
            $body
        );
    }

    return new Response(
        200,
        array(
            'Content-Type' => 'text/plain'
        ),
        "Request body size: " . $size . " bytes\n"
    );
});

注意:StreamingServer 会自动处理带有额外 Expect: 100-continue 请求头的请求。当 HTTP/1.1 客户端想要发送较大的请求体时,它们可以只发送请求头,并附加一个额外的 Expect: 100-continue 请求头,然后在发送实际(大)消息体之前等待。在这种情况下,服务器将自动向客户端发送一个中间的 HTTP/1.1 100 Continue 响应。这确保您将如预期的那样无延迟地接收流式请求体。

请求方法

请注意,服务器支持 任何 请求方法(包括自定义和非标准方法),以及每个方法在 HTTP 规范中定义的所有请求目标格式,包括 正常origin-form 请求以及 absolute-formauthority-form 中的代理请求。可以使用 getUri(): UriInterface 方法获取有效的请求 URI,这为您提供了访问各个 URI 组件的权限。请注意,根据给定的 request-target,某些 URI 组件可能存在或不存,例如,对于 asterisk-formauthority-form 中的请求,getPath(): string 方法将返回空字符串。其 getHost(): string 方法将返回由有效请求 URI 确定的主机,如果没有 HTTP/1.0 客户端指定(即没有 Host 标头),则默认为本地套接字地址。其 getScheme(): string 方法将根据请求是否通过安全的 TLS 连接到目标主机返回 httphttps

如果对于此 URI 方案来说是非标准的,Host 标头的值将被清理,以匹配此主机组件以及端口组件。

您可以使用 getMethod(): stringgetRequestTarget(): string 来检查这是一个可接受的请求,并可能想用适当的错误代码拒绝其他请求,例如 400(错误请求)或 405(方法不允许)。

CONNECT 方法在隧道设置(HTTPS 代理)中很有用,这不是大多数 HTTP 服务器关心的东西。请注意,如果您想处理此方法,客户端可以发送与 Host 标头值不同的请求目标(例如,删除默认端口),并且请求目标在转发时必须优先。

Cookie 参数

可以使用 getCookieParams(): string[] 方法来获取当前请求发送的所有 cookies。

$server = new Server(function (ServerRequestInterface $request) {
    $key = 'react\php';

    if (isset($request->getCookieParams()[$key])) {
        $body = "Your cookie value is: " . $request->getCookieParams()[$key];

        return new Response(
            200,
            array(
                'Content-Type' => 'text/plain'
            ),
            $body
        );
    }

    return new Response(
        200,
        array(
            'Content-Type' => 'text/plain',
            'Set-Cookie' => urlencode($key) . '=' . urlencode('test;more')
        ),
        "Your cookie has been set."
    );
});

上面的示例将尝试在首次访问时设置一个 cookie,并在所有后续尝试中尝试打印 cookie 的值。注意示例如何使用 urlencode() 函数来对非字母数字字符进行编码。这种编码在解码 cookie 的名称和值时也内部使用(这与 PHP 的 cookie 函数等其他实现一致)。

有关更多详细信息,请参阅 示例 #5

无效请求

服务器(Server)和流式服务器(StreamingServer)类支持 HTTP/1.1 和 HTTP/1.0 请求消息。如果客户端发送无效的请求消息,使用无效的 HTTP 协议版本或发送无效的 Transfer-Encoding 请求头值,服务器将自动向客户端发送 400(错误请求)HTTP 错误响应并关闭连接。除此之外,它还会触发一个 error 事件,可用于记录等目的。

$server->on('error', function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

请注意,如果您在请求处理函数中没有返回有效的响应对象,服务器也会触发一个 error 事件。有关更多详细信息,请参阅无效响应

响应

传递给 Server 或高级 StreamingServer 构造函数的回调函数负责处理请求并返回响应,该响应将被发送到客户端。此函数必须返回一个实现 PSR-7 ResponseInterface 对象或一个将解析为 PSR-7 ResponseInterface 对象的 ReactPHP Promise 的实例。

在这个项目中,您将找到一个实现 PSR-7 ResponseInterfaceResponse 类。我们在我们的项目中使用这个类的实例化,但您也可以使用任何您喜欢的 PSR-7 ResponseInterface 实现。

$server = new Server(function (ServerRequestInterface $request) {
    return new Response(
        200,
        array(
            'Content-Type' => 'text/plain'
        ),
        "Hello World!\n"
    );
});

延迟响应

上面的例子直接返回响应,因为它不需要处理时间。使用数据库、文件系统或长时间计算(实际上任何需要 >=1ms 的操作)来创建您的响应,将会减慢服务器的速度。为了防止这种情况,您应该使用 ReactPHP Promise。以下示例显示了这样的长期操作可能的样子

$server = new Server(function (ServerRequestInterface $request) use ($loop) {
    return new Promise(function ($resolve, $reject) use ($loop) {
        $loop->addTimer(1.5, function() use ($resolve) {
            $response = new Response(
                200,
                array(
                    'Content-Type' => 'text/plain'
                ),
                "Hello world"
            );
            $resolve($response);
        });
    });
});

上述示例将在 1.5 秒后创建一个响应。这个例子表明,如果您的响应需要时间来创建,您需要使用一个承诺。当请求体结束时,ReactPHP Promise 将解析为一个 Response 对象。如果客户端在承诺尚未解决时关闭连接,该承诺将被自动取消。可以使用承诺取消处理程序来清理在这种情况下分配的任何待处理资源(如果适用)。如果承诺在客户端关闭后解决,它将被简单地忽略。

流式响应

在这个项目中,Response 类支持为响应体添加实现 ReactPHP ReadableStreamInterface 的实例。因此,您可以直接将数据流式传输到响应体。请注意,其他 PSR-7 ResponseInterface 的实现可能只支持字符串。

$server = new Server(function (ServerRequestInterface $request) use ($loop) {
    $stream = new ThroughStream();

    $timer = $loop->addPeriodicTimer(0.5, function () use ($stream) {
        $stream->write(microtime(true) . PHP_EOL);
    });

    $loop->addTimer(5, function() use ($loop, $timer, $stream) {
        $loop->cancelTimer($timer);
        $stream->end();
    });

    return new Response(
        200,
        array(
            'Content-Type' => 'text/plain'
        ),
        $stream
    );
});

上述示例将以每 0.5 秒的间隔将当前 Unix 时间戳(以浮点数表示微秒)发送到客户端,并在 5 秒后结束。这只是您可以使用流的一个示例,您也可以通过小块发送大量数据或将其用于需要计算的主体数据。

如果请求处理程序解析的响应流已经关闭,它将简单地发送一个空响应体。如果客户端在流仍然打开时关闭连接,响应流将自动关闭。如果承诺在客户端关闭后解决,响应流将自动关闭。可以使用 close 事件来清理在这种情况下分配的任何待处理资源(如果适用)。

请注意,如果你使用实现了 ReactPHP 的 DuplexStreamInterface(例如上面示例中的 ThroughStream)的流实例时,需要特别注意。

对于大多数情况,这只会简单地消费其可读部分,并将流发出的任何数据转发(发送),从而完全忽略流的可写部分。然而,如果这是一个 101(切换协议)响应或对 CONNECT 方法的 2xx(成功)响应,它还会在流的可写部分 写入 数据。可以通过拒绝所有使用 CONNECT 方法的请求(大多数正常的原始 HTTP 服务器可能会这样做)或者确保只使用 ReadableStreamInterface 的实例来避免这种情况。

101(切换协议)响应代码对于更高级的 Upgrade 请求很有用,例如升级到 WebSocket 协议或实现超出 HTTP 规范和这个 HTTP 库范围的定制协议逻辑。如果你想处理 Upgrade: WebSocket 标头,你可能需要考虑使用 Ratchet。如果你想处理定制协议,你可能需要查看 HTTP 规范,并查看 示例 #31 和 #32 以获取更多详细信息。特别是,除非你发送一个也在相应的 HTTP/1.1 Upgrade 请求头值中出现的 Upgrade 响应头值,否则不得使用 101(切换协议)响应代码。在这种情况下,服务器会自动处理发送 Connection: upgrade 头值,因此你不需要这样做。

CONNECT 方法在隧道设置(HTTPS 代理)中很有用,并且不是大多数原始 HTTP 服务器想要关心的。HTTP 规范为该方法定义了一个不透明的“隧道模式”,并且没有使用消息体。出于一致性的原因,此库在响应体中使用了 DuplexStreamInterface 以处理隧道应用程序数据。这意味着对 CONNECT 请求的 2xx(成功)响应实际上可以使用流式响应体来处理隧道应用程序数据,因此客户端通过连接发送的任何原始数据都将通过可写流进行管道传输以供消费。请注意,虽然 HTTP 规范没有使用请求体来处理 CONNECT 请求,但可能仍然存在。正常的请求体处理适用于此处,并且连接只有在请求体处理完毕(在大多数情况下应该是空的)后才会转换为“隧道模式”。有关更多详细信息,请参阅 示例 #22

响应长度

如果已知响应体大小,将自动添加 Content-Length 响应头。这是最常见的情况,例如,当使用如下所示的 string 响应体时

$server = new Server(function (ServerRequestInterface $request) {
    return new Response(
        200,
        array(
            'Content-Type' => 'text/plain'
        ),
        "Hello World!\n"
    );
});

如果不知道响应体大小,将无法自动添加 Content-Length 响应头。当使用没有明确 Content-Length 响应头的 流式响应 时,出站的 HTTP/1.1 响应消息将自动使用 Transfer-Encoding: chunked,而传统的 HTTP/1.0 响应消息将包含纯响应体。如果你知道你的流式响应体的大小,你可以明确指定它,如下所示

$server = new Server(function (ServerRequestInterface $request) use ($loop) {
    $stream = new ThroughStream();

    $loop->addTimer(2.0, function () use ($stream) {
        $stream->end("Hello World!\n");
    });

    return new Response(
        200,
        array(
            'Content-Length' => '13',
            'Content-Type' => 'text/plain',
        ),
        $stream
    );
});

根据 HTTP 规范,对 HEAD 请求的任何响应以及任何带有 1xx(信息性)、204(无内容)或 304(未修改)状态码的响应将不包括消息体。这意味着你的回调不需要特别注意这一点,任何响应体都将简单地被忽略。

同样,任何对CONNECT请求的2xx(成功)响应,任何带有1xx(信息)或204(无内容)状态码的响应,都不会包含Content-LengthTransfer-Encoding头信息,因为这些不适用于这些消息。请注意,对HEAD请求的响应和任何带有304(未修改)状态码的响应可能会包含这些头信息,即使消息不包含响应体,因为这些头信息会适用于该消息,如果相同的请求使用了(无条件的)GET

无效响应

如上所述,每个出站的HTTP响应始终由PSR-7 ResponseInterface表示。如果您的请求处理函数返回无效值或抛出未处理的ExceptionThrowable,服务器将自动向客户端发送500(内部服务器错误)HTTP错误响应。在此基础上,它还将触发一个error事件,可用于记录此类事件。

$server->on('error', function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
    if ($e->getPrevious() !== null) {
        echo 'Previous: ' . $e->getPrevious()->getMessage() . PHP_EOL;
    }
});

请注意,如果客户端发送一个无效的HTTP请求,该请求从未到达您的请求处理函数,服务器也会触发一个error事件。有关更多详细信息,请参阅无效请求。此外,流式请求正文也可以在请求正文中触发一个error事件。

如果发生未处理的错误,服务器将仅向客户端发送一个非常通用的500(内部服务器错误)HTTP错误响应,而不包含任何其他详细信息。虽然我们理解这可能会使初始调试更困难,但它也意味着服务器默认不会泄露任何应用程序细节或堆栈跟踪。通常建议在您的请求处理函数中捕获任何ExceptionThrowable,或者使用middleware以避免此类通用错误处理,并创建自己的HTTP响应消息。

默认响应头

在回调函数返回后,响应将由ServerStreamingServer分别处理。它们将添加请求的协议版本,因此您无需这样做。

如果没有提供,将自动添加一个带有系统日期和时间的Date头。您可以像这样添加自定义的Date头。

$server = new Server(function (ServerRequestInterface $request) {
    return new Response(
        200,
        array(
            'Date' => date('D, d M Y H:i:s T')
        )
    );
});

如果您没有合适的时钟可以依赖,应使用空字符串取消设置此头。

$server = new Server(function (ServerRequestInterface $request) {
    return new Response(
        200,
        array(
            'Date' => ''
        )
    );
});

请注意,除非您指定自定义的X-Powered-By头,否则将自动假定一个X-Powered-By: react/alpha头。

$server = new Server(function (ServerRequestInterface $request) {
    return new Response(
        200,
        array(
            'X-Powered-By' => 'PHP 3'
        )
    );
});

如果您根本不想发送此头,可以使用空字符串作为值。

$server = new Server(function (ServerRequestInterface $request) {
    return new Response(
        200,
        array(
            'X-Powered-By' => ''
        )
    );
});

请注意,目前不支持持久连接(Connection: keep-alive)。因此,HTTP/1.1响应消息将自动包含一个Connection: close头,而不考虑显式传递的任何头值。

中间件

如上所述,Server和高级StreamingServer接受一个请求处理函数参数,该参数负责处理传入的HTTP请求,然后创建并返回出站的HTTP响应。

许多常见的用例涉及在将请求传递给最终的业务逻辑请求处理函数之前验证、处理和操作传入的HTTP请求。因此,该项目支持中间件请求处理函数的概念。

中间件请求处理函数应遵守以下规则

  • 它是一个有效的callable
  • 它接受ServerRequestInterface作为第一个参数,以及一个可选的callable作为第二个参数。
  • 它返回以下之一
    • 实现ResponseInterface接口以供直接消费的实例。
    • 任何可以被Promise\resolve()消费的承诺,解析结果为ResponseInterface以供延迟消费。
    • 它可能会抛出Exception(或返回一个被拒绝的承诺),以表示错误条件并终止链。
  • 它调用$next($request)以继续处理下一个中间件请求处理器,或显式返回而不调用$next以终止链。
    • $next请求处理器(递归地)使用与上述相同的逻辑调用链中的下一个请求处理器,并按照上述方式返回(或抛出)。
    • 在调用$next($request)之前,可能会修改$request以改变下一个中间件操作的传入请求。
    • 可以消费$next的返回值以修改输出响应。
    • 如果需要实现自定义“重试”逻辑等,$next请求处理器可能会被多次调用。

请注意,这个非常简单的定义允许您使用匿名函数或任何使用魔术__invoke()方法的类。这允许您轻松地动态创建自定义中间件请求处理器或使用基于类的策略来简化使用现有的中间件实现。

虽然该项目提供了使用中间件实现的方法,但它并不旨在定义中间件实现的外观。我们意识到存在一个充满活力的中间件实现生态系统,以及使用PSR-15(HTTP服务器请求处理器)标准化这些接口的持续努力,并支持这一目标。因此,该项目仅捆绑了一些必需的中间件实现,以匹配PHP的请求行为(见下文),并积极鼓励第三方中间件实现。

为了使用中间件请求处理器,只需将上面定义的所有可调用项的数组传递给相应的ServerStreamingServer即可。以下示例添加了一个中间件请求处理器,该处理器将当前时间作为标题(Request-Time)添加到请求中,并添加了一个始终返回200代码而没有主体的最终请求处理器

$server = new Server(array(
    function (ServerRequestInterface $request, callable $next) {
        $request = $request->withHeader('Request-Time', time());
        return $next($request);
    },
    function (ServerRequestInterface $request) {
        return new Response(200);
    }
));

注意中间件请求处理器和最终请求处理器的接口非常简单(且类似)。唯一的区别是最终请求处理器不接收$next处理器。

同样,您可以使用$next中间件请求处理器函数的结果来修改输出响应。请注意,根据上述文档,$next中间件请求处理器可以直接返回ResponseInterface或包装在延迟解析的承诺中的ResponseInterface。为了简化处理这两条路径,您只需像这样将其包装在Promise\resolve()调用中

$server = new Server(array(
    function (ServerRequestInterface $request, callable $next) {
        $promise = React\Promise\resolve($next($request));
        return $promise->then(function (ResponseInterface $response) {
            return $response->withHeader('Content-Type', 'text/html');
        });
    },
    function (ServerRequestInterface $request) {
        return new Response(200);
    }
));

请注意,$next中间件请求处理器也可以抛出Exception(或返回一个被拒绝的承诺),如上所述。前面的示例没有捕获任何异常,因此会向Server发出错误条件。或者,您也可以捕获任何Exception以实现自定义错误处理逻辑(或记录等),通过将其包装在Promise中,如下所示

$server = new Server(array(
    function (ServerRequestInterface $request, callable $next) {
        $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) {
            $resolve($next($request));
        });
        return $promise->then(null, function (Exception $e) {
            return new Response(
                500,
                array(),
                'Internal error: ' . $e->getMessage()
            );
        });
    },
    function (ServerRequestInterface $request) {
        if (mt_rand(0, 1) === 1) {
            throw new RuntimeException('Database error');
        }
        return new Response(200);
    }
));

LimitConcurrentRequestsMiddleware

LimitConcurrentRequestsMiddleware可以用来限制可以并发执行多少个下一个处理器。

如果调用此中间件,它将检查挂起的处理器数量是否低于允许的限制,然后简单地调用下一个处理器,并返回下一个处理器返回的任何内容(或抛出)。

如果挂起的处理程序数量超过允许的限制,请求将被排队(并且其流式请求体将被暂停),并返回一个挂起的承诺。一旦挂起的处理程序返回(或抛出异常),它将从这个队列中选择最旧的请求并调用下一个处理程序(并且其流式请求体将被恢复)。

以下示例显示了如何使用此中间件确保一次最多调用10个处理程序

$server = new Server(array(
    new LimitConcurrentRequestsMiddleware(10),
    $handler
));

类似地,此中间件通常与RequestBodyBufferMiddleware(见下文)一起使用,以限制一次可以缓存的请求数量

$server = new StreamingServer(array(
    new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
    new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
    new RequestBodyParserMiddleware(),
    $handler
));

更复杂的一些例子包括限制一次可以缓存的请求数量,然后确保实际的请求处理程序仅依次处理请求,没有任何并发

$server = new StreamingServer(array(
    new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
    new RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
    new RequestBodyParserMiddleware(),
    new LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency)
    $handler
));

RequestBodyBufferMiddleware

一个内置的中间件是RequestBodyBufferMiddleware,它可以用来在内存中缓存整个传入请求体。这如果在请求处理程序中需要完整的PSR-7兼容性而默认的流式请求体处理不需要时很有用。构造函数接受一个可选参数,即最大请求体大小。如果没有提供,它将使用PHP配置中的post_max_size(默认8 MiB)。(注意,将使用匹配的SAPI的值,在大多数情况下是CLI配置。)

任何请求体超过此限制的传入请求都将被接受,但其请求体将被丢弃(空请求体)。这样做是为了避免在内存中保留一个过大的传入请求(例如,考虑一个2 GB的文件上传)。这允许下一个中间件处理程序仍然处理此请求,但它将看到一个空的请求体。这与PHP的默认行为类似,如果超过此限制,则不会解析请求体。然而,与PHP的默认行为不同的是,原始请求体不可通过php://input访问。

RequestBodyBufferMiddleware将缓存已知大小(即指定了Content-Length头)的请求体以及未知大小(即指定了Transfer-Encoding: chunked头)的请求体。

所有请求将在内存中缓冲,直到到达请求体末尾,然后调用下一个中间件处理程序以传递完整的、缓存的请求。同样,这也会立即调用下一个中间件处理程序来处理请求体为空(如简单的GET请求)以及已经缓存的请求(如由于另一个中间件)。

请注意,给定的缓冲区大小限制应用于每个请求单独。这意味着如果您允许2 MiB的限制,并且收到1000个并发请求,这些缓冲区可能仅分配高达2000 MiB。因此,强烈建议您同时使用LimitConcurrentRequestsMiddleware(见上文)来限制并发请求数量。

用法

$server = new StreamingServer(array(
    new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
    new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
    function (ServerRequestInterface $request) {
        // The body from $request->getBody() is now fully available without the need to stream it 
        return new Response(200);
    },
));

RequestBodyParserMiddleware

RequestBodyParserMiddleware接受一个完全缓存的请求体(通常来自RequestBodyBufferMiddleware),并从传入的HTTP请求体中解析表单值和文件上传。

此中间件处理程序负责将使用Content-Type: application/x-www-form-urlencodedContent-Type: multipart/form-data的HTTP请求的值应用到PHP的默认超全局变量$_POST$_FILES上。您可以使用PSR-7中定义的$request->getParsedBody()$request->getUploadedFiles()方法,而不是依赖这些超全局变量。

因此,每个文件上传都将表示为实现UploadedFileInterface的实例。由于其阻塞特性,moveTo()方法不可用,并抛出RuntimeException异常。您可以使用$contents = (string)$file->getStream();来访问文件内容,并将其持久化到您喜欢的数据存储中。

$handler = function (ServerRequestInterface $request) {
    // If any, parsed form fields are now available from $request->getParsedBody()
    $body = $request->getParsedBody();
    $name = isset($body['name']) ? $body['name'] : 'unnamed';

    $files = $request->getUploadedFiles();
    $avatar = isset($files['avatar']) ? $files['avatar'] : null;
    if ($avatar instanceof UploadedFileInterface) {
        if ($avatar->getError() === UPLOAD_ERR_OK) {
            $uploaded = $avatar->getSize() . ' bytes';
        } elseif ($avatar->getError() === UPLOAD_ERR_INI_SIZE) {
            $uploaded = 'file too large';
        } else {
            $uploaded = 'with error';
        }
    } else {
        $uploaded = 'nothing';
    }

    return new Response(
        200,
        array(
            'Content-Type' => 'text/plain'
        ),
        $name . ' uploaded ' . $uploaded
    );
};

$server = new StreamingServer(array((
    new LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
    new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
    new RequestBodyParserMiddleware(),
    $handler
));

有关更多详细信息,请参阅 示例 #12

默认情况下,此中间件尊重upload_max_filesize(默认值为2M)ini设置。超过此限制的文件将被拒绝,并返回UPLOAD_ERR_INI_SIZE错误。您可以通过将最大文件大小(以字节为单位)作为构造函数的第一个参数显式传递来控制每个单独的文件上传的最大文件大小,如下所示

new RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file

默认情况下,此中间件尊重file_uploads(默认值为1)和max_file_uploads(默认值为20)ini设置。这些设置控制单个请求中可以上传的文件数量。如果您在单个请求中上传更多文件,额外的文件将被忽略,并且getUploadedFiles()方法返回一个截断的数组。请注意,在提交时留空的上传字段不计入此限制。您可以通过显式传递第二个参数到构造函数来控制每个请求的最大文件上传数量,如下所示

new RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each

请注意,此中间件处理程序仅解析请求体中已缓冲的所有内容。务必由上述示例中给出的先前中间件处理程序对请求体进行缓冲。此先前中间件处理程序还负责拒绝超出允许的消息大小(如大文件上传)的传入请求。上述RequestBodyBufferMiddleware简单地丢弃过多的请求体,从而产生一个空体。如果您在不进行缓冲的情况下使用此中间件,它将尝试解析一个空的(流式)体,因此可能会假设一个空的数据结构。有关更多详细信息,请参阅RequestBodyBufferMiddleware

此中间件尊重PHP的MAX_FILE_SIZE隐藏字段。超出此限制的文件将被拒绝,并返回UPLOAD_ERR_FORM_SIZE错误。

此中间件尊重max_input_vars(默认值为1000)和max_input_nesting_level(默认值为64)ini设置。

请注意,此中间件忽略了enable_post_data_reading(默认值为1)ini设置,因为它在这里尊重的意义不大,并且留给更高层次的实现。如果您想尊重此设置,您必须检查其值,并有效避免完全使用此中间件。

第三方中间件

虽然此项目提供了使用中间件实现的方法(见上述内容),但它并不旨在定义中间件实现的外观。我们意识到存在一个充满活力的中间件实现生态系统,以及与PSR-15(HTTP服务器请求处理器)标准化这些实现之间接口的持续努力,并支持这一目标。因此,此项目仅捆绑了一些必需的中间件实现,这些实现需要与PHP的请求行为相匹配(见上述内容),并积极鼓励第三方中间件实现。

虽然我们很乐意在 react/http 中直接支持 PSR-15,但我们理解这个接口并没有专门针对异步API,因此没有利用到对延迟响应的Promise。简单来说,当PSR-15强制返回 ResponseInterface 时,我们也接受返回 PromiseInterface<ResponseInterface>。因此,我们建议使用外部PSR-15中间件适配器,该适配器会动态地对这些返回值进行猴子补丁,使得使用大多数PSR-15中间件无需任何修改即可与该包配合使用。

除此之外,您还可以使用上述中间件定义来创建自定义中间件。第三方中间件的非详尽列表可以在中间件wiki中找到。如果您构建或知道一个自定义中间件,请确保让全世界都知道,并欢迎将其添加到这个列表中。

安装

推荐的安装此库的方式是通过ComposerComposer新手?

这将安装最新的支持版本

$ composer require react/http:^0.8.7

有关版本升级的详细信息,请参阅变更日志

该项目旨在在任何平台上运行,因此不需要任何PHP扩展,支持从旧版PHP 5.3到当前PHP 7+和HHVM的运行。强烈建议使用PHP 7+进行此项目。

测试

要运行测试套件,首先需要克隆此仓库,然后通过Composer安装所有依赖项

$ composer install

要运行测试套件,请进入项目根目录并运行

$ php vendor/bin/phpunit

许可证

MIT,见许可证文件