react/http

基于 ReactPHP 的事件驱动、流式 HTTP 客户端和服务器实现

维护者

详细信息

github.com/reactphp/http

源代码

问题

资助包维护!
Open Collective

安装次数: 17,571,495

依赖者: 443

建议者: 12

安全: 2

星标: 737

关注者: 37

分支: 142

开放问题: 20

v1.10.0 2024-03-27 17:20 UTC

README

CI status installs on Packagist

基于 ReactPHP 的事件驱动、流式 HTTP 客户端和服务器实现。

开发版本:此分支包含即将发布的 v3 版本的代码。要查看当前稳定版 v1 的代码,请查看 1.x 分支

即将发布的 v3 版本将是此包的未来发展方向。然而,我们仍将积极支持 v1 版本,以满足尚未升级到最新版本的用户。有关更多信息,请参阅 安装说明

此 HTTP 库提供了基于 ReactPHP 的 SocketEventLoop 组件的可重用 HTTP 客户端和服务器实现。其客户端组件允许您并发发送任意数量的异步 HTTP/HTTPS 请求。其服务器组件允许您构建接受来自 HTTP 客户端(如网页浏览器)的 HTTP 请求的纯文本 HTTP 和安全 HTTPS 服务器。此库为此提供了异步、流式的方法,因此您可以在不阻塞的情况下处理多个并发 HTTP 请求。

目录

快速入门示例

安装后,您可以使用以下代码访问 HTTP 服务器并发送一些简单的 HTTP GET 请求

<?php

require __DIR__ . '/vendor/autoload.php';

$client = new React\Http\Browser();

$client->get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
    var_dump($response->getHeaders(), (string)$response->getBody());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

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

<?php

require __DIR__ . '/vendor/autoload.php';

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    return React\Http\Message\Response::plaintext(
        "Hello World!\n"
    );
});

$socket = new React\Socket\SocketServer('127.0.0.1:8080');
$http->listen($socket);

有关示例,请参阅 示例

客户端使用

请求方法

最重要的是,此项目提供了一个 Browser 对象,该对象提供了一些类似于 HTTP 协议方法的方法

$browser->get($url, array $headers = []);
$browser->head($url, array $headers = []);
$browser->post($url, array $headers = [], string|ReadableStreamInterface $body = '');
$browser->delete($url, array $headers = [], string|ReadableStreamInterface $body = '');
$browser->put($url, array $headers = [], string|ReadableStreamInterface $body = '');
$browser->patch($url, array $headers = [], string|ReadableStreamInterface $body = '');

这些方法中的每一个都需要一个 $url 和一些可选参数来发送 HTTP 请求。每个方法名都与相应的 HTTP 请求方法匹配,例如 get() 方法发送一个 HTTP GET 请求。

你可以选择性地传递一个额外的 $headers 关联数组,这些头信息将与 HTTP 请求一起发送。此外,如果提供了出站请求体并且其大小已知且非空,每个方法将自动添加一个匹配的 Content-Length 请求头。对于空请求体,如果请求方法通常期望请求体(仅适用于 POSTPUTPATCH HTTP 请求方法),则将只包括一个 Content-Length: 0 请求头。

如果你使用的是 流式请求体,它将默认使用 Transfer-Encoding: chunked,除非你显式传递一个匹配的 Content-Length 请求头。有关更多详细信息,请参阅 流式请求

默认情况下,所有上述方法都默认使用 HTTP/1.1 协议版本发送请求。如果你想要显式使用旧版 HTTP/1.0 协议版本,可以使用 withProtocolVersion() 方法。如果你想要使用任何其他或自定义的 HTTP 请求方法,可以使用 request() 方法。

上述每个方法都支持异步操作,要么通过 fulfills 返回一个 PSR-7 ResponseInterface,要么通过 rejects 抛出一个 Exception。有关更多详细信息,请参阅有关 promises 的下一章。

承诺

发送请求是异步的(非阻塞),因此实际上可以并行发送多个请求。Browser 将对每个请求以 PSR-7 ResponseInterface 消息进行响应,顺序不能保证。发送请求使用基于 Promise 的接口,这使得当 HTTP 请求完成时(即成功满足或由于错误而拒绝)很容易做出反应。

$browser->get($url)->then(
    function (Psr\Http\Message\ResponseInterface $response) {
        var_dump('Response received', $response);
    },
    function (Exception $e) {
        echo 'Error: ' . $e->getMessage() . PHP_EOL;
    }
);

如果你觉得这很奇怪,也可以使用更传统的 阻塞 API

请注意,使用完整的响应消息解析 Promise 意味着整个响应体必须保存在内存中。这很容易开始,并且对于较小的响应(例如常见的 HTML 页面或 RESTful 或 JSON API 请求)来说表现良好。

你可能还想了解 流式 API

  • 如果你正在处理大量的并发请求(100+)或者
  • 如果你想要在发生时处理单个数据块(而无需等待完整的响应体)或者
  • 如果你预计响应体的大小很大(例如,下载二进制文件时,1 MiB 或更多)或者
  • 如果你不确定响应体的大小(在访问任意远程 HTTP 端点且事先不知道响应体大小时,最好是安全第一)。

取消

返回的 Promise 是这样实现的,它可以在它仍然挂起时被取消。取消挂起的 Promise 将用 Exception 拒绝其值,并清理任何底层资源。

$promise = $browser->get($url);

Loop::addTimer(2.0, function () use ($promise) {
    $promise->cancel();
});

超时

该库使用了一种非常高效的HTTP实现,因此大多数HTTP请求通常仅需几毫秒即可完成。然而,当通过不可靠的网络(互联网)发送HTTP请求时,可能会发生一些错误,导致请求在一定时间后失败。因此,该库尊重PHP的default_socket_timeout设置(默认60秒)作为发送外出HTTP请求并等待成功响应的超时时间,否则将取消挂起的请求并使用异常拒绝其值。

请注意,此超时值包括创建底层传输连接、发送HTTP请求、接收HTTP响应头及其完整响应体以及遵循任何最终的重定向。有关配置要遵循的重定向数量(或完全禁用重定向)的说明,请参阅下面的重定向,以及下面的流式响应,不将接收大型响应体计入此超时。

您可以使用withTimeout()方法传递自定义的超时值(以秒为单位),如下所示

$browser = $browser->withTimeout(10.0);

$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    // response received within 10 seconds maximum
    var_dump($response->getHeaders());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

类似地,您可以使用布尔值false来不应用超时,或使用布尔值true来恢复默认处理。有关更多详细信息,请参阅withTimeout()

如果您使用流式响应体,接收响应体流所需的时间不会计入超时。这允许您将此传入流保持开启更长时间,例如在下载非常大的流或在长时间连接上流式传输数据时。

如果您使用流式请求体,发送请求体流所需的时间不会计入超时。这允许您将此传出流保持开启更长时间,例如在上传非常大的流时。

请注意,此超时处理适用于更高级别的HTTP层。底层(如套接字和DNS)也可能应用(不同的)超时值。特别是,底层套接字连接使用相同的default_socket_timeout设置来建立底层传输连接。要控制此连接超时行为,您可以注入自定义Connector,如下所示

$browser = new React\Http\Browser(
    new React\Socket\Connector(
        [
            'timeout' => 5
        ]
    )
);

身份验证

此库支持使用Authorization: Basic …请求头或允许您设置显式的Authorization请求头来实现HTTP基本身份验证

默认情况下,此库不包含外出Authorization请求头。如果服务器需要身份验证,它可能会返回一个401(未授权)状态码,该状态码将默认拒绝请求(有关详细信息,请参阅下面的withRejectErrorResponse()方法)。

为了传递身份验证详细信息,您只需将用户名和密码作为请求URL的一部分传递即可,如下所示

$promise = $browser->get('https://user:[email protected]/api');

请注意,在认证详情中特殊字符需要进行百分号编码,请参阅rawurlencode()。本示例将自动使用出站请求头 Authorization: Basic … 传递base64编码的认证详情。如果您所访问的HTTP端点需要其他认证方案,也可以显式传递此请求头。这在使用(RESTful)HTTP API并使用OAuth访问令牌或JSON Web Tokens (JWT) 时很常见。

$token = 'abc123';

$promise = $browser->get(
    'https://example.com/api',
    [
        'Authorization' => 'Bearer ' . $token
    ]
);

在遵循重定向时,默认情况下,Authorization 请求头不会发送到任何远程主机。当遵循一个包含认证详情的 Location 响应头的重定向时,这些详情将被发送以进行后续请求。请参阅下面的重定向

重定向

默认情况下,此库遵循任何重定向,并遵循来自远程服务器的 Location 响应头中的 3xx(重定向)状态码。承诺将在重定向链的最后一个响应中得到满足。

$browser->get($url, $headers)->then(function (Psr\Http\Message\ResponseInterface $response) {
    // the final response will end up here
    var_dump($response->getHeaders());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

任何重定向请求都将遵循原始请求的语义,并将包括与原始请求相同的请求头,除了以下列出的那些。如果原始请求是一个临时(307)或永久(308)重定向,请求体和请求头将被传递给重定向请求。否则,请求体将永远不会传递给重定向请求。因此,每个重定向请求都将删除任何 Content-LengthContent-Type 请求头。

如果原始请求使用了带有 Authorization 请求头的HTTP认证,则此请求头只有在重定向URL使用相同的宿主机时才会作为重定向请求的一部分传递。换句话说,由于可能存在隐私/安全顾虑,Authorizaton 请求头不会转发到其他外部主机。当遵循一个包含认证详情的 Location 响应头的重定向时,这些详情将被发送以进行后续请求。

您可以使用withFollowRedirects()方法来控制要遵循的最大重定向数或以原样返回任何重定向响应并应用自定义重定向逻辑,如下所示:

$browser = $browser->withFollowRedirects(false);

$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    // any redirects will now end up here
    var_dump($response->getHeaders());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

有关更多详细信息,请参阅withFollowRedirects()

阻塞

如上所述,此库默认提供了一个强大、异步的API。

您也可以使用reactphp/async将其集成到您传统的阻塞环境中。这允许您像这样简单地等待异步HTTP请求

use function React\Async\await;

$browser = new React\Http\Browser();

$promise = $browser->get('http://example.com/');

try {
    $response = await($promise);
    // response successfully received
} catch (Exception $e) {
    // an error occurred while performing the request
}

同样,您也可以并发处理多个请求,并等待一个Response对象数组

use function React\Async\await;
use function React\Promise\all;

$promises = [
    $browser->get('http://example.com/'),
    $browser->get('http://www.example.org/'),
];

$responses = await(all($promises));

这是由于PHP 8.1+中可用的fibers和我们的兼容性API实现的,这些API也适用于所有支持的PHP版本。请参阅reactphp/async以获取更多详细信息。

请记住上述关于在内存中缓冲整个响应消息的说明。作为替代方案,您也可以查看以下章节中的流式API

并发

如上所述,此库提供了一个强大、异步的API。能够一次发送大量的请求是此项目的核心功能之一。例如,您可以在同时处理SQL查询的同时轻松并发发送100个请求。

请记住,权力越大,责任越大。发送过多的请求可能会占用您方面的所有资源,甚至可能会因为远程端检测到您的不合理请求数量而被禁用。

// watch out if array contains many elements
foreach ($urls as $url) {
    $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
        var_dump($response->getHeaders());
    }, function (Exception $e) {
        echo 'Error: ' . $e->getMessage() . PHP_EOL;
    });
}

因此,通常建议将发送端的并发限制在合理值。使用相当小的限制是常见的,因为同时做超过十几件事情可能会很容易压倒接收端。您可以使用clue/reactphp-mq作为轻量级内存队列,以同时并发执行许多(但不是太多)事情。

// wraps Browser in a Queue object that executes no more than 10 operations at once
$q = new Clue\React\Mq\Queue(10, null, function ($url) use ($browser) {
    return $browser->get($url);
});

foreach ($urls as $url) {
    $q($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
        var_dump($response->getHeaders());
    }, function (Exception $e) {
        echo 'Error: ' . $e->getMessage() . PHP_EOL;
    });
}

超出并发限制的附加请求将自动入队,直到挂起的请求之一完成。这很好地与现有的基于Promise的API集成。有关详细信息,请参阅clue/reactphp-mq

此内存方法对于数千个待处理请求来说效果相当好。如果您正在处理非常大的输入列表(例如CSV或NDJSON文件中的数百万行),则可能需要考虑使用流式处理方法。有关详细信息,请参阅clue/reactphp-flux

流式响应

上述所有示例都假设您想将整个响应体存储在内存中。这很容易开始,并且对于较小的响应来说效果相当好。

然而,在某些情况下,使用流式处理方法通常是一个更好的选择,其中只需要在内存中保留小块数据。

  • 如果你正在处理大量的并发请求(100+)或者
  • 如果你想要在发生时处理单个数据块(而无需等待完整的响应体)或者
  • 如果你预计响应体的大小很大(例如,下载二进制文件时,1 MiB 或更多)或者
  • 如果你不确定响应体的大小(在访问任意远程 HTTP 端点且事先不知道响应体大小时,最好是安全第一)。

您可以使用requestStreaming()方法发送任意HTTP请求并接收流式响应。它使用相同的HTTP消息API,但不缓冲内存中的响应体。它仅以接收到的数据的小块处理响应体,并通过ReactPHP的Stream API转发这些数据。这对于任意大小(任何数量的)响应都适用。

这意味着它以正常的PSR-7 ResponseInterface解析,可以像往常一样访问响应消息参数。您可以像往常一样访问消息正文,但现在它还实现了ReactPHP的ReadableStreamInterface以及PSR-7 StreamInterface的部分。

$browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    $body = $response->getBody();
    assert($body instanceof Psr\Http\Message\StreamInterface);
    assert($body instanceof React\Stream\ReadableStreamInterface);

    $body->on('data', function ($chunk) {
        echo $chunk;
    });

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

    $body->on('close', function () {
        echo '[DONE]' . PHP_EOL;
    });
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

另请参阅流下载基准示例流转发示例

您可以在消息正文中调用以下方法:

$body->on($event, $callback);
$body->eof();
$body->isReadable();
$body->pipe(React\Stream\WritableStreamInterface $dest, array $options = []);
$body->close();
$body->pause();
$body->resume();

由于消息体处于流状态,调用以下方法没有太大意义。

$body->__toString(); // ''
$body->detach(); // throws BadMethodCallException
$body->getSize(); // null
$body->tell(); // throws BadMethodCallException
$body->isSeekable(); // false
$body->seek(); // throws BadMethodCallException
$body->rewind(); // throws BadMethodCallException
$body->isWritable(); // false
$body->write(); // throws BadMethodCallException
$body->read(); // throws BadMethodCallException
$body->getContents(); // throws BadMethodCallException

请注意,在流式处理模式下,超时的应用略有不同。在流式处理模式下,超时值涵盖了创建底层传输连接、发送HTTP请求、接收HTTP响应头以及任何可能的重定向。特别是,超时值不考虑接收(可能很大)的响应体。

如果您想将流式响应集成到更高层次的API中,那么与解析为流对象的Promise对象一起工作通常不方便。考虑也使用react/promise-stream。生成的流式代码可能如下所示

use function React\Promise\Stream\unwrapReadable;

function download(Browser $browser, string $url): React\Stream\ReadableStreamInterface {
    return unwrapReadable(
        $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) {
            return $response->getBody();
        })
    );
}

$stream = download($browser, $url);
$stream->on('data', function ($data) {
    echo $data;
});
$stream->on('error', function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

有关requestStreaming()方法的详细信息,请参阅。

流式请求

除了流式传输响应体之外,您还可以流式传输请求体。如果您想发送大的POST请求(上传文件等)或同时处理多个出站流,这会非常有用。您不需要将正文作为字符串传递,只需将实现ReactPHP的ReadableStreamInterface的实例传递给请求方法即可,如下所示

$browser->post($url, [], $stream)->then(function (Psr\Http\Message\ResponseInterface $response) {
    echo 'Successfully sent.';
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

如果您使用的是流式请求体(React\Stream\ReadableStreamInterface),它将默认使用Transfer-Encoding: chunked,或者您必须显式传递匹配的Content-Length请求头,如下所示

$body = new React\Stream\ThroughStream();
Loop::addTimer(1.0, function () use ($body) {
    $body->end("hello world");
});

$browser->post($url, ['Content-Length' => '11'], $body);

如果流式请求体触发一个error事件或在未首先触发成功的end事件的情况下显式关闭,请求将自动关闭并拒绝。

HTTP 代理

您还可以通过添加对clue/reactphp-http-proxy的依赖来通过HTTP CONNECT代理服务器建立您的出站连接。

HTTP CONNECT代理服务器(也通常称为“HTTPS代理”或“SSL代理”)通常用于通过中介(“代理”)隧道HTTPS流量,以隐藏原始地址(匿名性)或绕过地址封锁(地理位置封锁)。虽然许多(公共)HTTP CONNECT代理服务器通常仅限于HTTPS端口443,但从技术上讲,这可以用于隧道任何基于TCP/IP的协议,例如纯HTTP和TLS加密的HTTPS。

$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');

$connector = new React\Socket\Connector([
    'tcp' => $proxy,
    'dns' => false
]);

$browser = new React\Http\Browser($connector);

请参阅HTTP代理示例

SOCKS 代理

您还可以通过添加对clue/reactphp-socks的依赖来通过SOCKS代理服务器建立您的出站连接。

SOCKS代理协议族(SOCKS5、SOCKS4和SOCKS4a)通常用于通过中介(“代理”)隧道HTTP(S)流量,以隐藏原始地址(匿名性)或绕过地址封锁(地理位置封锁)。虽然许多(公共)SOCKS代理服务器通常仅限于HTTP(S)端口80443,但从技术上讲,这可以用于隧道任何基于TCP/IP的协议。

$proxy = new Clue\React\Socks\Client('127.0.0.1:1080');

$connector = new React\Socket\Connector([
    'tcp' => $proxy,
    'dns' => false
]);

$browser = new React\Http\Browser($connector);

请参阅SOCKS代理示例

SSH 代理

您还可以通过添加对clue/reactphp-ssh-proxy的依赖来通过SSH服务器建立您的出站连接。

安全外壳(SSH)是一种安全网络协议,最常用于访问远程服务器上的登录外壳。其架构允许它在单个连接上使用多个安全通道。除此之外,这也可以用来创建“SSH隧道”,通常用于通过中介(“代理”)隧道HTTP(S)流量,以隐藏原始地址(匿名性)或绕过地址封锁(地理位置封锁)。这可以用来隧道任何基于TCP/IP的协议(HTTP、SMTP、IMAP等),允许您访问否则无法从外部访问的本地服务(例如防火墙后面的数据库),因此也可以用于纯HTTP和TLS加密的HTTPS。

$proxy = new Clue\React\SshProxy\SshSocksConnector('[email protected]');

$connector = new React\Socket\Connector([
    'tcp' => $proxy,
    'dns' => false
]);

$browser = new React\Http\Browser($connector);

请参阅SSH代理示例

Unix 域套接字

默认情况下,此库支持通过明文TCP/IP和安全的TLS连接分别对http://https:// URL方案进行传输。此外,此库还支持显式配置的Unix域套接字(UDS)。

为了使用UDS路径,您必须显式配置连接器以覆盖目标URL,这样在请求URL中给出的主机名将不再用于建立连接

$connector = new React\Socket\FixedUriConnector(
    'unix:///var/run/docker.sock',
    new React\Socket\UnixConnector()
);

$browser = new React\Http\Browser($connector);

$client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) {
    var_dump($response->getHeaders(), (string)$response->getBody());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

另请参阅Unix域套接字(UDS)示例

服务器使用

HttpServer

React\Http\HttpServer类负责处理传入的连接,然后处理每个传入的HTTP请求。

收到完整的HTTP请求后,它将调用给定的请求处理函数。这个请求处理函数需要传递给构造函数,并将与相应的请求对象一起调用,并期望返回一个响应对象。

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    return React\Http\Message\Response::plaintext(
        "Hello World!\n"
    );
});

每个传入的HTTP请求消息始终由PSR-7 ServerRequestInterface表示,有关更多详细信息,请参阅以下请求章节。

每个输出的HTTP响应消息始终由PSR-7 ResponseInterface表示,有关更多详细信息,请参阅以下响应章节。

此类接受一个可选的LoopInterface|null $loop参数,可以用来传递用于此对象的事件循环实例。您可以使用null值来使用默认循环。除非您确定要显式使用给定的事件循环实例,否则不应提供此值。

为了开始监听任何传入的连接,HttpServer需要通过listen()方法附加到React\Socket\ServerInterface的实例,如以下章节所述。在其最简单的形式中,您可以将其附加到React\Socket\SocketServer以启动一个文本HTTP服务器,如下所示

$http = new React\Http\HttpServer($handler);

$socket = new React\Socket\SocketServer('0.0.0.0:8080');
$http->listen($socket);

有关详细信息,请参阅listen()方法和hello world服务器示例

默认情况下,HttpServer会在内存中缓冲并解析完整的传入HTTP请求。它将在收到完整的请求头和请求体后调用给定的请求处理函数。这意味着传递给您的请求处理函数的请求对象将与PSR-7(http-message)完全兼容。这为80%的使用场景提供了合理的默认设置,除非您确定您知道自己在做什么,否则这是推荐使用此库的方式。

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

memory_limit 128M
post_max_size 8M // capped at 64K

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 请求在缓冲其请求体时可以消耗多少内存。这需要限制,因为服务器可以并发处理大量请求,否则服务器可能会消耗大量内存。为了默认支持更高的并发性,此值被限制在 64K。如果您分配一个更高的值,它将默认只允许 64K。如果请求超出此限制,其请求体将被忽略,并且它将被处理为没有任何请求体的请求。有关如何覆盖此设置的显式配置,请参见下文。

默认情况下,此类将尝试避免为缓冲多个并发 HTTP 请求而消耗超过您 memory_limit 的一半。因此,在上述默认的 128M 最大值下,它将尝试为缓冲多个并发 HTTP 请求消耗不超过 64M。因此,它将并发性限制在上述默认值下的 1024 个 HTTP 请求。

您必须为您的 PHP ini 设置分配合理的值。通常建议不支持使用大 HTTP 请求体(例如,大文件上传)来缓冲传入的 HTTP 请求。如果您想增加此缓冲区大小,您还必须增加总内存限制以允许更多并发请求(设置 memory_limit 512M 或更多)或显式限制并发性。

为了覆盖上述缓冲默认值,您可以显式配置 HttpServer。您可以使用 LimitConcurrentRequestsMiddlewareRequestBodyBufferMiddleware(见下文)来显式配置一次可以处理的请求数量,如下所示

$http = new React\Http\HttpServer(
    new React\Http\Middleware\StreamingRequestMiddleware(),
    new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
    new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
    new React\Http\Middleware\RequestBodyParserMiddleware(),
    $handler
);

在此示例中,我们允许同时处理高达 100 个并发请求,并且每个请求可以缓冲高达 2M。这意味着您可能需要保留最多 200M 的内存用于传入请求体缓冲区。相应地,您需要调整 memory_limit ini 设置以允许这些缓冲区以及您的实际应用程序逻辑内存需求(例如 512M 或更多)。

内部,当没有提供 StreamingRequestMiddleware 时,此类会自动分配这些中间件处理程序。因此,您可以使用此示例覆盖所有默认设置以实现自定义限制。

作为在内存中缓冲整个请求体的替代方案,您还可以使用流式方法,其中只需要在内存中保留少量数据块。

$http = new React\Http\HttpServer(
    new React\Http\Middleware\StreamingRequestMiddleware(),
    $handler
);

在这种情况下,它将在收到 HTTP 请求头后调用请求处理函数,即在收到可能更大的 HTTP 请求体之前。这意味着传递给您的请求处理函数的 请求 可能与 PSR-7 不完全兼容。这是专门设计来帮助处理更复杂的用例,其中您希望完全控制消费传入 HTTP 请求体和并发设置。有关更多详细信息,请参阅下文 流式传入请求

listen()

可以使用 listen(React\Socket\ServerInterface $socket): void 方法在给定的套接字服务器实例上开始监听 HTTP 请求。

给定的 React\Socket\ServerInterface 负责发射底层的流连接。这个HTTP服务器需要附加到它上面,以便处理任何连接并将传入的流数据作为传入的HTTP请求消息处理。在其最常见的形式中,您可以将其附加到 React\Socket\SocketServer 以启动一个类似于这样的纯文本HTTP服务器

$http = new React\Http\HttpServer($handler);

$socket = new React\Socket\SocketServer('0.0.0.0:8080');
$http->listen($socket);

有关更多详细信息,请参阅 hello world服务器示例

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

同样,通常建议使用反向代理设置在默认HTTPS端口443(TLS终止)上接受安全的HTTPS请求,并且只将纯文本请求路由到这个HTTP服务器。作为替代方案,您还可以使用安全的TLS监听地址、证书文件以及可选的passphrase将安全的HTTPS请求与这个HTTP服务器一起接受,如下所示

$http = new React\Http\HttpServer($handler);

$socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', [
    'tls' => [
        'local_cert' => __DIR__ . '/localhost.pem'
    ]
]);
$http->listen($socket);

有关更多详细信息,请参阅 hello world HTTPS示例

服务器请求

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

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

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
   $body = "The method of the request is: " . $request->getMethod() . "\n";
   $body .= "The requested path is: " . $request->getUri()->getPath() . "\n";

   return React\Http\Message\Response::plaintext(
       $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',否则不会设置
$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR'] . "\n";

    return React\Http\Message\Response::plaintext(
        $body
    );
});

有关更多示例,请参阅 whatsmyip服务器示例

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

查询参数

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

$http = new React\Http\HttpServer(function (Psr\Http\Message\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 React\Http\Message\Response::html(
        $body
    );
});

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

另请参阅服务器查询参数示例

请求体

默认情况下,Server将在内存中缓冲和解析完整的请求体。这意味着给定的请求对象包括解析后的请求体和任何文件上传。

作为默认缓冲逻辑的替代方案,您还可以使用StreamingRequestMiddleware。跳到下一章以了解更多关于如何处理流式请求的信息。

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

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

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    $name = $request->getParsedBody()['name'] ?? 'anonymous';

    return React\Http\Message\Response::plaintext(
        "Hello $name!\n"
    );
});

有关更多详细信息,请参阅表单上传示例

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

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    $data = json_decode((string)$request->getBody());
    $name = $data->name ?? 'anonymous';

    return React\Http\Message\Response::json(
        ['message' => "Hello $name!"]
    );
});

有关更多详细信息,请参阅JSON API服务器示例

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

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    $files = $request->getUploadedFiles();
    $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing';

    return React\Http\Message\Response::plaintext(
        "Uploaded $name\n"
    );
});

有关更多详细信息,请参阅表单上传服务器示例

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

注意:HttpServer 会自动处理带有附加 Expect: 100-continue 请求头的请求。当 HTTP/1.1 客户端想要发送更大的请求体时,它们可以仅发送带有附加 Expect: 100-continue 请求头的请求头,并在发送实际(大)消息体之前等待。在这种情况下,服务器将自动向客户端发送一个中间的 HTTP/1.1 100 Continue 响应。这确保您将如预期的那样立即收到请求体。

流式接收请求

如果您正在使用高级 StreamingRequestMiddleware,则会在接收到请求头后处理请求对象。这意味着这发生在接收(可能很大的)请求体之前。

注意,这是非标准行为,被认为是高级用法。跳转到上一章了解更多关于如何处理缓冲的 请求体 的信息。

虽然在 PHP 生态系统中这种情况可能不常见,但这实际上是一种非常强大的方法,给您带来了一些其他方法无法实现的优点

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

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

$http = new React\Http\HttpServer(
    new React\Http\Middleware\StreamingRequestMiddleware(),
    function (Psr\Http\Message\ServerRequestInterface $request) {
        $body = $request->getBody();
        assert($body instanceof Psr\Http\Message\StreamInterface);
        assert($body instanceof React\Stream\ReadableStreamInterface);

        return new React\Promise\Promise(function ($resolve, $reject) use ($body) {
            $bytes = 0;
            $body->on('data', function ($data) use (&$bytes) {
                $bytes += strlen($data);
            });

            $body->on('end', function () use ($resolve, &$bytes){
                $resolve(React\Http\Message\Response::plaintext(
                    "Received $bytes bytes\n"
                ));
            });

            // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event
            $body->on('error', function (Exception $e) use ($resolve, &$bytes) {
                $resolve(React\Http\Message\Response::plaintext(
                    "Encountered error after $bytes bytes: {$e->getMessage()}\n"
                )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST));
            });
        });
    }
);

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

有关详细信息,请参阅 流式请求服务器示例

每当请求体流中出现新数据时,将发出 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)。

$http = new React\Http\HttpServer(
    new React\Http\Middleware\StreamingRequestMiddleware(),
    function (Psr\Http\Message\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.\n";

            return React\Http\Message\Response::plaintext(
                $body
            )->withStatus(React\Http\Message\Response::STATUS_LENGTH_REQUIRED);
        }

        return React\Http\Message\Response::plaintext(
            "Request body size: " . $size . " bytes\n"
        );
    }
);

注意:HttpServer 自动处理带有额外 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[]》方法获取当前请求发送的所有cookie。

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    $key = 'greeting';

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

        return React\Http\Message\Response::plaintext(
            $body
        );
    }

    return React\Http\Message\Response::plaintext(
        "Your cookie has been set.\n"
    )->withHeader('Set-Cookie', $key . '=' . urlencode('Hello world!'));
});

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

有关更多详细信息,请参阅cookie服务器示例

无效请求

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

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

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

服务器响应

传递给《HttpServer》构造函数的回调函数负责处理请求并返回响应,该响应将发送给客户端。

此函数必须返回一个实现《PSR-7 ResponseInterface》的实例或一个解析为《PSR-7 ResponseInterface》对象的《ReactPHP Promise》。

该项目提供了一个实现《PSR-7 ResponseInterface》的《Response》类。在其最简单形式中,您可以使用它如下

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    return React\Http\Message\Response::plaintext(
        "Hello World!\n"
    );
});

我们在我们的项目示例中使用了这个《Response》类,但您也可以使用《PSR-7 ResponseInterface》的任何其他实现。有关更多详细信息,请参阅《Response》类。

延迟响应

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

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    $promise = new Promise(function ($resolve, $reject) {
        Loop::addTimer(1.5, function() use ($resolve) {
            $resolve();
        });
    });

    return $promise->then(function () { 
        return React\Http\Message\Response::plaintext(
            "Hello World!"
        );
    });
});

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

流式发送响应

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

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    $stream = new ThroughStream();

    // send some data every once in a while with periodic timer
    $timer = Loop::addPeriodicTimer(0.5, function () use ($stream) {
        $stream->write(microtime(true) . PHP_EOL);
    });

    // end stream after a few seconds
    $timeout = Loop::addTimer(5.0, function() use ($stream, $timer) {
        Loop::cancelTimer($timer);
        $stream->end();
    });

    // stop timer if stream is closed (such as when connection is closed)
    $stream->on('close', function () use ($timer, $timeout) {
        Loop::cancelTimer($timer);
        Loop::cancelTimer($timeout);
    });

    return new React\Http\Message\Response(
        React\Http\Message\Response::STATUS_OK,
        [
            'Content-Type' => 'text/plain'
        ],
        $stream
    );
});

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

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

请注意,如果您使用的是实现ReactPHP的DuplexStreamInterface(例如上述示例中的ThroughStream)的实例,则需要特别注意。

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

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

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

响应长度

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

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    return React\Http\Message\Response::plaintext(
        "Hello World!\n"
    );
});

如果未知响应体大小,则无法自动添加Content-Length响应头。当使用没有显式Content-Length响应头的流式输出响应时,HTTP/1.1响应消息将自动使用Transfer-Encoding: chunked,而传统HTTP/1.0响应消息将包含纯响应体。如果您知道流式响应体的长度,您可以像这样显式指定它

$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
    $stream = new ThroughStream();

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

    return new React\Http\Message\Response(
        React\Http\Message\Response::STATUS_OK,
        [
            '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事件,可用于记录目的,如下所示

$http->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响应消息。

默认响应头

当从请求处理函数返回响应时,它将被HttpServer处理,然后发送回客户端。

将自动添加Server: ReactPHP/1响应头。您可以像这样添加自定义的Server响应头:

$http = new React\Http\HttpServer(function (ServerRequestInterface $request) {
    return new React\Http\Message\Response(
        React\Http\Message\Response::STATUS_OK,
        [
            'Server' => 'PHP/3'
        ]
    );
});

如果您根本不希望发送这个Server响应头(例如,当您不想暴露底层的服务器软件时),可以使用这样的空字符串值:

$http = new React\Http\HttpServer(function (ServerRequestInterface $request) {
    return new React\Http\Message\Response(
        React\Http\Message\Response::STATUS_OK,
        [
            'Server' => ''
        ]
    );
});

如果没有提供,将自动添加包含当前系统日期和时间的Date响应头。您可以像这样添加自定义的Date响应头:

$http = new React\Http\HttpServer(function (ServerRequestInterface $request) {
    return new React\Http\Message\Response(
        React\Http\Message\Response::STATUS_OK,
        [
            'Date' => gmdate('D, d M Y H:i:s \G\M\T')
        ]
    );
});

如果您根本不希望发送这个Date响应头(例如,当您没有合适的时钟可依赖时),可以使用这样的空字符串值:

$http = new React\Http\HttpServer(function (ServerRequestInterface $request) {
    return new React\Http\Message\Response(
        React\Http\Message\Response::STATUS_OK,
        [
            'Date' => ''
        ]
    );
});

HttpServer类将自动添加请求的协议版本,因此您无需手动添加。例如,如果客户端使用HTTP/1.1协议版本发送请求,则响应消息也将使用相同的协议版本,无论请求处理函数返回什么版本。

服务器支持持久连接。将自动添加适当的Connection: keep-aliveConnection: close响应头,尊重匹配的请求头值和HTTP默认头值。服务器负责处理Connection响应头,因此您不应该自己传递此响应头,除非您明确希望使用Connection: close响应头覆盖用户的选择。

中间件

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

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

自定义中间件

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

  • 它是一个有效的callable
  • 它接受一个实现PSR-7 ServerRequestInterface的实例作为第一个参数,以及一个可选的callable作为第二个参数。
  • 它返回以下之一:
    • 一个实现PSR-7 ResponseInterface的实例,以便直接消费。
    • 任何可以被Promise\resolve()消费的承诺,解析为PSR-7 ResponseInterface的实例,以便延迟消费。
    • 它可以抛出Exception(或返回一个拒绝的承诺)以表示错误条件并终止链。
  • 它调用$next($request)以继续处理下一个中间件请求处理函数,或者明确返回而不调用$next以终止链。
    • 下一个请求处理程序(递归地)使用与上述相同的逻辑从链中调用下一个请求处理程序,并返回(或抛出)与上述相同的结果。
    • 在调用 $next($request) 之前,可以修改 $request,以便更改下一个中间件操作的要处理的外来请求。
    • 可以消费 $next 的返回值来修改传出响应。
    • 如果您想实现自定义的“重试”逻辑等,则可能需要多次调用 $next 请求处理程序。

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

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

要使用中间件请求处理程序,只需将上面定义的所有可调用对象列表传递给 HttpServer。以下示例添加了一个中间件请求处理程序,该处理程序将当前时间作为头部(Request-Time)添加到请求中,并添加了一个始终返回不带主体的 200 OK 状态码的最终请求处理程序。

$http = new React\Http\HttpServer(
    function (Psr\Http\Message\ServerRequestInterface $request, callable $next) {
        $request = $request->withHeader('Request-Time', time());
        return $next($request);
    },
    function (Psr\Http\Message\ServerRequestInterface $request) {
        return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK);
    }
);

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

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

$http = new React\Http\HttpServer(
    function (Psr\Http\Message\ServerRequestInterface $request, callable $next) {
        $promise = React\Promise\resolve($next($request));
        return $promise->then(function (ResponseInterface $response) {
            return $response->withHeader('Content-Type', 'text/html');
        });
    },
    function (Psr\Http\Message\ServerRequestInterface $request) {
        return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK);
    }
);

请注意,$next 中间件请求处理程序还可以抛出一个 Exception(或返回一个被拒绝的承诺),如上所述。上一个示例没有捕获任何异常,因此会向 HttpServer 信号错误条件。或者,您也可以捕获任何 Exception 来实现自定义错误处理逻辑(或记录等),方法是将此包裹在一个 Promise 中,如下所示

$http = new React\Http\HttpServer(
    function (Psr\Http\Message\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 React\Http\Message\Response::plaintext(
                'Internal error: ' . $e->getMessage() . "\n"
            )->withStatus(React\Http\Message\Response::STATUS_INTERNAL_SERVER_ERROR);
        });
    },
    function (Psr\Http\Message\ServerRequestInterface $request) {
        if (mt_rand(0, 1) === 1) {
            throw new RuntimeException('Database error');
        }
        return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK);
    }
);

第三方中间件

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

虽然我们希望直接在 react/http 中支持 PSR-15,但我们理解这个接口并没有特别针对异步 API,因此没有充分利用承诺来处理延迟响应。简单来说,PSR-15 强制返回 PSR-7 ResponseInterface,我们同时也接受 PromiseInterface。因此,我们建议使用外部的 PSR-15 中间件适配器,它会对这些返回值进行即时猴子补丁,这样就可以在不做任何修改的情况下使用此包中的大多数 PSR-15 中间件。

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

API

浏览器

React\Http\Browser 负责向您的 HTTP 服务器发送 HTTP 请求,并跟踪挂起的传入 HTTP 响应。

$browser = new React\Http\Browser();

此类接受两个可选参数以实现更高级的使用。

$browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null);

如果您需要自定义连接器设置(DNS 解析、TLS 参数、超时、代理服务器等),您可以显式传递一个自定义的 ConnectorInterface 实例。

$connector = new React\Socket\Connector([
    'dns' => '127.0.0.1',
    'tcp' => [
        'bindto' => '192.168.10.1:0'
    ],
    'tls' => [
        'verify_peer' => false,
        'verify_peer_name' => false
    ]
]);

$browser = new React\Http\Browser($connector);

此类接受一个可选的LoopInterface|null $loop参数,可以用来传递用于此对象的事件循环实例。您可以使用null值来使用默认循环。除非您确定要显式使用给定的事件循环实例,否则不应提供此值。

请注意,浏览器类是最终的,不应该被扩展,它可能在未来的版本中被标记为最终。

get()

可以使用 get(string $url, array $headers = []): PromiseInterface 方法发送 HTTP GET 请求。

$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    var_dump((string)$response->getBody());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

有关 GET 请求客户端示例,请参阅GET 请求客户端示例

post()

可以使用 post(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface 方法发送 HTTP POST 请求。

$browser->post(
    $url,
    [
        'Content-Type' => 'application/json'
    ],
    json_encode($data)
)->then(function (Psr\Http\Message\ResponseInterface $response) {
    var_dump(json_decode((string)$response->getBody()));
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

有关 POST JSON 客户端示例,请参阅POST JSON 客户端示例

此方法也常用于提交 HTML 表单数据。

$data = [
    'user' => 'Alice',
    'password' => 'secret'
];

$browser->post(
    $url,
    [
        'Content-Type' => 'application/x-www-form-urlencoded'
    ],
    http_build_query($data)
);

如果出站请求体是一个 string,此方法将自动添加匹配的 Content-Length 请求头。如果您正在使用流式请求体(ReadableStreamInterface),它将默认使用 Transfer-Encoding: chunked,或者您必须显式传递一个匹配的 Content-Length 请求头,如下所示

$body = new React\Stream\ThroughStream();
Loop::addTimer(1.0, function () use ($body) {
    $body->end("hello world");
});

$browser->post($url, ['Content-Length' => '11'), $body);

head()

可以使用 head(string $url, array $headers = []): PromiseInterface 方法发送 HTTP HEAD 请求。

$browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    var_dump($response->getHeaders());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

patch()

可以使用 patch(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface 方法发送 HTTP PATCH 请求。

$browser->patch(
    $url,
    [
        'Content-Type' => 'application/json'
    ],
    json_encode($data)
)->then(function (Psr\Http\Message\ResponseInterface $response) {
    var_dump(json_decode((string)$response->getBody()));
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

如果出站请求体是一个 string,此方法将自动添加匹配的 Content-Length 请求头。如果您正在使用流式请求体(ReadableStreamInterface),它将默认使用 Transfer-Encoding: chunked,或者您必须显式传递一个匹配的 Content-Length 请求头,如下所示

$body = new React\Stream\ThroughStream();
Loop::addTimer(1.0, function () use ($body) {
    $body->end("hello world");
});

$browser->patch($url, ['Content-Length' => '11'], $body);

put()

可以使用 put(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface 方法发送 HTTP PUT 请求。

$browser->put(
    $url,
    [
        'Content-Type' => 'text/xml'
    ],
    $xml->asXML()
)->then(function (Psr\Http\Message\ResponseInterface $response) {
    var_dump((string)$response->getBody());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

有关 PUT XML 客户端示例,请参阅PUT XML 客户端示例

如果出站请求体是一个 string,此方法将自动添加匹配的 Content-Length 请求头。如果您正在使用流式请求体(ReadableStreamInterface),它将默认使用 Transfer-Encoding: chunked,或者您必须显式传递一个匹配的 Content-Length 请求头,如下所示

$body = new React\Stream\ThroughStream();
Loop::addTimer(1.0, function () use ($body) {
    $body->end("hello world");
});

$browser->put($url, ['Content-Length' => '11'], $body);

delete()

可以使用 delete(string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface 方法发送 HTTP DELETE 请求。

$browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    var_dump((string)$response->getBody());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

request()

可以使用 request(string $method, string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface 方法发送任意 HTTP 请求。

发送 HTTP 请求的首选方式是使用上面的请求方法,例如使用get()方法发送 HTTP GET 请求。

作为替代方案,如果您想使用自定义的HTTP请求方法,可以使用此方法

$browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    var_dump((string)$response->getBody());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

此方法会自动添加匹配的 Content-Length 请求头,如果已知的 outgoing 请求体大小非空。对于空的请求体,如果请求方法通常期望请求体(仅适用于 POSTPUTPATCH),则只会包含 Content-Length: 0 请求头。

如果您使用的是流式请求体(《ReadableStreamInterface》),它将默认使用 Transfer-Encoding: chunked,或者您必须显式传入一个匹配的 Content-Length 请求头,如下所示

$body = new React\Stream\ThroughStream();
Loop::addTimer(1.0, function () use ($body) {
    $body->end("hello world");
});

$browser->request('POST', $url, ['Content-Length' => '11'], $body);

requestStreaming()

requestStreaming(string $method, string $url, array $headers = [], string|ReadableStreamInterface $body = ''): PromiseInterface<ResponseInterface> 方法可用于发送任意HTTP请求并接收流式响应,而无需缓冲响应体。

发送HTTP请求的首选方式是使用上述 请求方法,例如使用 get() 方法发送HTTP GET 请求。默认情况下,这些方法会将整个响应体缓冲到内存中。这很容易上手,并且对于较小的响应来说效果还不错。

在某些情况下,使用流式方法可能更好,其中只需保留一小部分内存。您可以使用此方法发送任意HTTP请求并接收流式响应。它使用相同的HTTP消息API,但不缓冲内存中的响应体。它仅以接收到的数据小块的方式处理响应体,并通过 ReactPHP的Stream API 将这些数据转发。这适用于任意大小的(任意数量的)响应。

$browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    $body = $response->getBody();
    assert($body instanceof Psr\Http\Message\StreamInterface);
    assert($body instanceof React\Stream\ReadableStreamInterface);

    $body->on('data', function ($chunk) {
        echo $chunk;
    });

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

    $body->on('close', function () {
        echo '[DONE]' . PHP_EOL;
    });
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

有关更多详细信息、示例和可能的用例,请参阅 ReactPHP的 ReadableStreamInterface流式响应

此方法会自动添加匹配的 Content-Length 请求头,如果已知的 outgoing 请求体大小非空。对于空的请求体,如果请求方法通常期望请求体(仅适用于 POSTPUTPATCH),则只会包含 Content-Length: 0 请求头。

如果您使用的是流式请求体(《ReadableStreamInterface》),它将默认使用 Transfer-Encoding: chunked,或者您必须显式传入一个匹配的 Content-Length 请求头,如下所示

$body = new React\Stream\ThroughStream();
Loop::addTimer(1.0, function () use ($body) {
    $body->end("hello world");
});

$browser->requestStreaming('POST', $url, ['Content-Length' => '11'], $body);

withTimeout()

withTimeout(bool|number $timeout): Browser 方法可用于更改用于等待挂起请求的最大超时时间。

您可以传入要使用的秒数作为新的超时值

$browser = $browser->withTimeout(10.0);

您可以传入一个布尔值 false 来禁用任何超时。在这种情况下,请求可以永远挂起

$browser = $browser->withTimeout(false);

您可以传入一个布尔值 true 来重新启用默认超时处理。这将遵守PHP的 default_socket_timeout 设置(默认60秒)

$browser = $browser->withTimeout(true);

有关超时处理的更多详细信息,请参阅 超时

请注意,《Browser》 是一个不可变对象,即此方法实际上返回一个 Browser 实例,并应用了给定的超时值。

withFollowRedirects()

withFollowRedirects(bool|int $followRedirects): Browser 方法可用于更改如何处理HTTP重定向。

您可以传入要跟随的最大重定向数

$browser = $browser->withFollowRedirects(5);

当超过重定向数时,请求将自动被拒绝。您可以传入一个 0 来拒绝遇到任何重定向

$browser = $browser->withFollowRedirects(0);

$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    // only non-redirected responses will now end up here
    var_dump($response->getHeaders());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

您可以传入一个布尔值 false 来禁用跟随任何重定向。在这种情况下,请求将以重定向响应解决,而不是跟随 Location 响应头

$browser = $browser->withFollowRedirects(false);

$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    // any redirects will now end up here
    var_dump($response->getHeaderLine('Location'));
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

您可以传入一个布尔值 true 来重新启用默认重定向处理。默认情况下,最多跟随10个重定向

$browser = $browser->withFollowRedirects(true);

有关重定向处理的更多详细信息,请参阅 重定向

请注意,Browser 是一个不可变对象,即此方法实际上返回一个新的 带有给定重定向设置的 Browser 实例。

withRejectErrorResponse()

可以使用 withRejectErrorResponse(bool $obeySuccessCode): Browser 方法来更改是否拒绝非成功 HTTP 响应状态码(4xx 和 5xx)。

您可以将 bool false 传入以禁用拒绝使用 4xx 或 5xx 响应状态码的传入响应。在这种情况下,请求将以表示错误条件的响应消息解决。

$browser = $browser->withRejectErrorResponse(false);

$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    // any HTTP response will now end up here
    var_dump($response->getStatusCode(), $response->getReasonPhrase());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

您可以将 bool true 传入以重新启用默认状态码处理。默认情况下,会拒绝任何 4xx 或 5xx 范围内的响应状态码,并抛出 ResponseException

$browser = $browser->withRejectErrorResponse(true);

$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    // any successful HTTP response will now end up here
    var_dump($response->getStatusCode(), $response->getReasonPhrase());
}, function (Exception $e) {
    if ($e instanceof React\Http\Message\ResponseException) {
        // any HTTP response error message will now end up here
        $response = $e->getResponse();
        var_dump($response->getStatusCode(), $response->getReasonPhrase());
    } else {
        echo 'Error: ' . $e->getMessage() . PHP_EOL;
    }
});

请注意,Browser 是一个不可变对象,即此方法实际上返回一个 带有给定设置的 新的 Browser 实例。

withBase()

可以使用 withBase(string|null $baseUrl): Browser 方法来更改用于解析相对 URL 的基础 URL。

如果您配置了基础 URL,则对相对 URL 的任何请求都将首先相对于给定的绝对基础 URL 进行解析。这支持解析相对路径引用(如 ../ 等)。这对于(RESTful)API 调用尤其有用,其中所有端点(URL)都位于一个公共基础 URL 下。

$browser = $browser->withBase('http://api.example.com/v3/');

// will request http://api.example.com/v3/users
$browser->get('users')->then(…);

您可以将 null 作为基础 URL 传入以返回一个不使用基础 URL 的新实例。

$browser = $browser->withBase(null);

因此,使用相对 URL 请求不使用基础 URL 的浏览器将无法完成,并且会在不发送请求的情况下被拒绝。

如果给定的 $baseUrl 参数不是一个有效的 URL,则此方法将抛出 InvalidArgumentException

请注意,Browser 是一个不可变对象,即 withBase() 方法实际上返回一个 带有给定基础 URL 的 新的 Browser 实例。

withProtocolVersion()

可以使用 withProtocolVersion(string $protocolVersion): Browser 方法来更改所有后续请求将使用的 HTTP 协议版本。

所有上述 请求方法 默认为以 HTTP/1.1 发送请求。这是首选的 HTTP 协议版本,它还提供了与旧式 HTTP/1.0 服务器的良好向后兼容性。因此,很少需要显式更改此协议版本。

如果您想显式使用旧式 HTTP/1.0 协议版本,可以使用此方法。

$browser = $browser->withProtocolVersion('1.0');

$browser->get($url)->then(…);

请注意,Browser 是一个不可变对象,即此方法实际上返回一个 带有新协议版本 的新的 Browser 实例。

withResponseBuffer()

可以使用 withResponseBuffer(int $maximumSize): Browser 方法来更改响应体的最大缓冲区大小。

发送HTTP请求的首选方式是使用上述 请求方法,例如使用 get() 方法发送HTTP GET 请求。默认情况下,这些方法会将整个响应体缓冲到内存中。这很容易上手,并且对于较小的响应来说效果还不错。

默认情况下,响应体缓冲区将被限制在 16 MiB。如果响应体超过此最大大小,则请求将被拒绝。

您可以传入最大字节数以进行缓冲。

$browser = $browser->withResponseBuffer(1024 * 1024);

$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
    // response body will not exceed 1 MiB
    var_dump($response->getHeaders(), (string) $response->getBody());
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

请注意,对于每个待处理的请求,响应体缓冲区必须保持在内存中,直到其传输完成,并且只有在待处理请求得到满足后才会被释放。因此,通常不建议增加此最大缓冲区大小以允许更大的响应体。相反,您可以使用 requestStreaming() 方法 来接收任意大小的响应,而无需缓冲。相应地,此最大缓冲区设置对流式响应没有影响。

请注意,Browser 是一个不可变对象,即此方法实际上返回一个 带有给定设置的 新的 Browser 实例。

withHeader()

可以使用 withHeader(string $header, string $value): Browser 方法为所有后续请求添加请求头。

$browser = $browser->withHeader('User-Agent', 'ACME');

$browser->get($url)->then(…);

请注意,新头将覆盖先前使用相同名称(不区分大小写)设置的所有头。除非为任何请求显式设置,否则后续请求将默认使用这些头。

withoutHeader()

可以使用 withoutHeader(string $header): Browser 方法删除通过 withHeader() 方法 设置的任何默认请求头。

$browser = $browser->withoutHeader('User-Agent');

$browser->get($url)->then(…);

请注意,此方法仅影响使用 withHeader(string $header, string $value): Browser 方法设置的头。

React\Http\Message

响应

可以使用 React\Http\Message\Response 类来表示出站服务器响应消息。

$response = new React\Http\Message\Response(
    React\Http\Message\Response::STATUS_OK,
    [
        'Content-Type' => 'text/html'
    ],
    "<html>Hello world!</html>\n"
);

此类实现了 PSR-7 ResponseInterface,它又扩展了 PSR-7 MessageInterface

此外,此类还实现了 PSR-7 Message Util StatusCodeInterface,这意味着大多数常见的HTTP状态码都可用作具有 STATUS_* 前缀的类常量。例如,200 OK404 Not Found 状态码可以分别用作 Response::STATUS_OKResponse::STATUS_NOT_FOUND

内部实现基于一个基类,这被视为可能在未来发生变化的技术细节。

html()

可以使用静态的 html(string $html): Response 方法来创建HTML响应。

$html = <<<HTML
<!doctype html>
<html>
<body>Hello wörld!</body>
</html>

HTML;

$response = React\Http\Message\Response::html($html);

这是一个方便的快捷方法,返回与以下等效的值

$response = new React\Http\Message\Response(
    React\Http\Message\Response::STATUS_OK,
    [
        'Content-Type' => 'text/html; charset=utf-8'
    ],
    $html
);

此方法始终返回具有 200 OK 状态码以及为给定HTTP源字符串(以UTF-8(Unicode)编码)提供的适当 Content-Type 响应头部的响应。通常建议在给定的纯文本字符串末尾添加换行符。

如果您想使用不同的状态码或自定义HTTP响应头,您可以使用提供的PSR-7方法操作返回的响应对象,或直接使用 Response 构造函数实例化自定义HTTP响应对象。

$response = React\Http\Message\Response::html(
    "<h1>Error</h1>\n<p>Invalid user name given.</p>\n"
)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST);
json()

可以使用静态的 json(mixed $data): Response 方法来创建JSON响应。

$response = React\Http\Message\Response::json(['name' => 'Alice']);

这是一个方便的快捷方法,返回与以下等效的值

$response = new React\Http\Message\Response(
    React\Http\Message\Response::STATUS_OK,
    [
        'Content-Type' => 'application/json'
    ],
    json_encode(
        ['name' => 'Alice'],
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION
    ) . "\n"
);

此方法始终返回具有 200 OK 状态码和为给定结构化数据(作为JSON文本编码)提供的适当 Content-Type 响应头部的响应。

给定的结构化数据将被编码为JSON文本。数据中的任何 string 值都必须以UTF-8(Unicode)编码。如果编码失败,此方法将抛出 InvalidArgumentException

默认情况下,给定的结构化数据将使用上述标志进行编码。这包括美化打印和保留 float 值的零小数部分以简化调试。假设任何额外的数据开销通常由使用HTTP响应压缩来补偿。

如果您想使用不同的状态码或自定义HTTP响应头,您可以使用提供的PSR-7方法操作返回的响应对象,或直接使用 Response 构造函数实例化自定义HTTP响应对象。

$response = React\Http\Message\Response::json(
    ['error' => 'Invalid user name given']
)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST);
plaintext()

可以使用静态的 plaintext(string $text): Response 方法来创建纯文本响应。

$response = React\Http\Message\Response::plaintext("Hello wörld!\n");

这是一个方便的快捷方法,返回与以下等效的值

$response = new React\Http\Message\Response(
    React\Http\Message\Response::STATUS_OK,
    [
        'Content-Type' => 'text/plain; charset=utf-8'
    ],
    "Hello wörld!\n"
);

此方法始终返回具有 200 OK 状态码和为给定纯文本字符串(以UTF-8(Unicode)编码)提供的适当 Content-Type 响应头部的响应。通常建议在给定的纯文本字符串末尾添加换行符。

如果您想使用不同的状态码或自定义HTTP响应头,您可以使用提供的PSR-7方法操作返回的响应对象,或直接使用 Response 构造函数实例化自定义HTTP响应对象。

$response = React\Http\Message\Response::plaintext(
    "Error: Invalid user name given.\n"
)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST);
xml()

静态的 xml(string $xml): Response 方法可以用来创建一个 XML 响应。

$xml = <<<XML
<?xml version="1.0" encoding="utf-8"?>
<body>
    <greeting>Hello wörld!</greeting>
</body>

XML;

$response = React\Http\Message\Response::xml($xml);

这是一个方便的快捷方法,返回与以下等效的值

$response = new React\Http\Message\Response(
    React\Http\Message\Response::STATUS_OK,
    [
        'Content-Type' => 'application/xml'
    ],
    $xml
);

此方法始终返回一个带有 200 OK 状态码和适当 Content-Type 响应头部的响应,对应于给定的 XML 源字符串。通常建议使用 UTF-8(Unicode)编码,并在 XML 声明中指定这一点,并在给定的 XML 源字符串末尾添加一个换行符。

如果您想使用不同的状态码或自定义HTTP响应头,您可以使用提供的PSR-7方法操作返回的响应对象,或直接使用 Response 构造函数实例化自定义HTTP响应对象。

$response = React\Http\Message\Response::xml(
    "<error><message>Invalid user name given.</message></error>\n"
)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST);

请求

可以使用 React\Http\Message\Request 类来表示一个外出的 HTTP 请求消息。

此类实现了 PSR-7 RequestInterface,它扩展了 PSR-7 MessageInterface

这主要用于内部表示 HTTP 客户端实现中的每个外出的 HTTP 请求消息。同样,您也可以与其他 HTTP 客户端实现一起使用此类,以及用于测试。

内部实现基于一个基类,这被视为可能在未来发生变化的技术细节。

ServerRequest

可以使用 React\Http\Message\ServerRequest 类来表示一个进入的服务器请求消息。

此类实现了 PSR-7 ServerRequestInterface,它扩展了 PSR-7 RequestInterface,而后者又扩展了 PSR-7 MessageInterface

这主要用于内部表示每个进入的请求消息。同样,您也可以在测试案例中使用此类来测试您的 Web 应用程序如何响应特定的 HTTP 请求。

内部实现基于一个基类,这被视为可能在未来发生变化的技术细节。

Uri

可以使用 React\Http\Message\Uri 类来表示 URI(或 URL)。

此类实现了 PSR-7 UriInterface

这主要用于内部表示我们的 HTTP 客户端和服务器实现中每个 HTTP 请求消息的 URI。同样,您也可以与其他 HTTP 实现一起使用此类,以及用于测试。

ResponseException

React\Http\Message\ResponseException 是一个 Exception 子类,如果远程服务器返回非成功状态码(除 2xx 或 3xx 之外),将用于拒绝请求承诺。您可以通过 withRejectErrorResponse() 方法 来控制此行为。

可以使用 getCode(): int 方法来返回 HTTP 响应状态码。

可以使用 getResponse(): ResponseInterface 方法来访问其底层响应对象。

React\Http\Middleware

StreamingRequestMiddleware

可以使用 React\Http\Middleware\StreamingRequestMiddleware 来处理带有流式请求体的进入请求(不进行缓冲)。

这允许您处理任何大小的请求,而无需在内存中缓冲请求体。相反,它将请求体表示为一个 ReadableStreamInterface,该接口在接收到数据时发出数据块。

$http = new React\Http\HttpServer(
    new React\Http\Middleware\StreamingRequestMiddleware(),
    function (Psr\Http\Message\ServerRequestInterface $request) {
        $body = $request->getBody();
        assert($body instanceof Psr\Http\Message\StreamInterface);
        assert($body instanceof React\Stream\ReadableStreamInterface);

        return new React\Promise\Promise(function ($resolve) use ($body) {
            $bytes = 0;
            $body->on('data', function ($chunk) use (&$bytes) {
                $bytes += \count($chunk);
            });
            $body->on('close', function () use (&$bytes, $resolve) {
                $resolve(new React\Http\Message\Response(
                    React\Http\Message\Response::STATUS_OK,
                    [],
                    "Received $bytes bytes\n"
                ));
            });
        });
    }
);

有关更多详细信息,请参阅 流式进入请求

此外,此中间件可以与 LimitConcurrentRequestsMiddlewareRequestBodyBufferMiddleware(见下文)一起使用,以显式配置一次可以处理的请求数量总和。

$http = new React\Http\HttpServer(
    new React\Http\Middleware\StreamingRequestMiddleware(),
    new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
    new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
    new React\Http\Middleware\RequestBodyParserMiddleware(),
    $handler
);

内部,此类用作“标记”,以不在 HttpServer 中触发默认请求缓冲行为。它不实现任何自己的逻辑。

LimitConcurrentRequestsMiddleware

可以使用 React\Http\Middleware\LimitConcurrentRequestsMiddleware 来限制可以并发执行的后续处理器的数量。

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

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

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

$http = new React\Http\HttpServer(
    new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10),
    $handler
);

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

$http = new React\Http\HttpServer(
    new React\Http\Middleware\StreamingRequestMiddleware(),
    new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
    new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
    new React\Http\Middleware\RequestBodyParserMiddleware(),
    $handler
);

更复杂的示例包括限制一次可以缓存的请求数量,然后确保实际请求处理程序依次处理一个请求,而不存在并发

$http = new React\Http\HttpServer(
    new React\Http\Middleware\StreamingRequestMiddleware(),
    new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
    new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request
    new React\Http\Middleware\RequestBodyParserMiddleware(),
    new React\Http\Middleware\LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency)
    $handler
);

RequestBodyBufferMiddleware

内置的中间件之一是React\Http\Middleware\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(请见上文)一起使用,以限制总并发请求数量。

用法

$http = new React\Http\HttpServer(
    new React\Http\Middleware\StreamingRequestMiddleware(),
    new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
    new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
    function (Psr\Http\Message\ServerRequestInterface $request) {
        // The body from $request->getBody() is now fully available without the need to stream it 
        return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK);
    },
);

RequestBodyParserMiddleware

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

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

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

$handler = function (Psr\Http\Message\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 Psr\Http\Message\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 React\Http\Message\Response(
        React\Http\Message\Response::STATUS_OK,
        [
            'Content-Type' => 'text/plain'
        ],
        $name . ' uploaded ' . $uploaded
    );
};

$http = new React\Http\HttpServer(
    new React\Http\Middleware\StreamingRequestMiddleware(),
    new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers
    new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
    new React\Http\Middleware\RequestBodyParserMiddleware(),
    $handler
);

有关更多详细信息,请参阅表单上传服务器示例

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

new React\Http\Middleware\RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file

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

new React\Http\Middleware\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 设置,因为它在这里没有意义,并且留给高级实现。如果您想遵循此设置,您必须检查其值并有效地避免完全使用此中间件。

安装

安装此库的推荐方法是 通过 Composer对 Composer 不熟悉?

一旦发布,此项目将遵循 SemVer。目前,这将安装最新的开发版本

composer require react/http:^3@dev

有关版本升级的详细信息,请参阅 CHANGELOG

此项目旨在在任何平台上运行,因此不需要任何 PHP 扩展,并支持在 PHP 7.1 到当前 PHP 8+ 上运行。强烈建议使用此项目的最新支持的 PHP 版本。

测试

要运行测试套件,您首先需要克隆此仓库,然后通过Composer安装所有依赖项(请访问Composer网站)

composer install

要运行测试套件,请前往项目根目录并执行

vendor/bin/phpunit

测试套件还包含一些依赖稳定网络连接的功能集成测试。如果您不想运行这些测试,可以像这样简单地跳过

vendor/bin/phpunit --exclude-group internet

许可

MIT许可,请参阅许可文件