react/socket

为ReactPHP提供异步、流式文本TCP/IP和安全的TLS套接字服务器和客户端连接

资助包维护!
Open Collective

安装数: 45,357,562

依赖项: 404

建议者: 5

安全性: 0

星星: 1,187

关注者: 47

分支: 155

开放问题: 6

v1.16.0 2024-07-26 10:38 UTC

README

CI status installs on Packagist

ReactPHP提供异步、流式文本TCP/IP和安全的TLS套接字服务器和客户端连接。

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

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

套接字库提供了基于EventLoopStream组件的套接字层服务器和客户端的可重用接口。其服务器组件允许您构建接受来自网络客户端(如HTTP服务器)的传入连接的网络服务器。其客户端组件允许您构建建立到网络服务器(如HTTP或数据库客户端)的传出连接的网络客户端。此库提供了处理所有这些的异步、流式方法,因此您可以处理多个并发连接而不会阻塞。

目录

快速入门示例

这是一个当你发送任何内容时就会关闭连接的服务器

$socket = new React\Socket\SocketServer('127.0.0.1:8080');

$socket->on('connection', function (React\Socket\ConnectionInterface $connection) {
    $connection->write("Hello " . $connection->getRemoteAddress() . "!\n");
    $connection->write("Welcome to this amazing server!\n");
    $connection->write("Here's a tip: don't say anything.\n");

    $connection->on('data', function ($data) use ($connection) {
        $connection->close();
    });
});

请参阅示例

这是一个客户端,它会输出上述服务器的输出,然后尝试发送一个字符串给它

$connector = new React\Socket\Connector();

$connector->connect('127.0.0.1:8080')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->pipe(new React\Stream\WritableResourceStream(STDOUT));
    $connection->write("Hello World!\n");
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

连接使用

ConnectionInterface

ConnectionInterface用于表示任何传入和传出连接,如常规TCP/IP连接。

传入或传出连接是一个双工流(可读和可写),它实现了React的DuplexStreamInterface。它包含一些附加属性,用于表示此连接已建立或从中建立的本地和远程地址(客户端IP)。

最常见的情况下,实现此ConnectionInterface的实例由所有实现ServerInterface的类生成,并由所有实现ConnectorInterface的类使用。

因为 ConnectionInterface 实现了底层的 DuplexStreamInterface,所以您可以使用其任何事件和方法,就像平时一样。

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

$connection->on('end', function () {
    echo 'ended';
});

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

$connection->on('close', function () {
    echo 'closed';
});

$connection->write($data);
$connection->end($data = null);
$connection->close();
// …

有关更多详细信息,请参阅 DuplexStreamInterface

getRemoteAddress()

getRemoteAddress(): ?string 方法返回已建立此连接的完整远程地址(URI)。

$address = $connection->getRemoteAddress();
echo 'Connection with ' . $address . PHP_EOL;

如果无法确定远程地址或此时未知(例如,在连接关闭后),它可能会返回一个 NULL 值。

否则,它将以字符串值返回完整地址(URI),例如 tcp://127.0.0.1:8080tcp://[::1]:80tls://127.0.0.1:443unix://example.sockunix:///path/to/example.sock。请注意,各个 URI 组件是应用程序特定的,并取决于底层传输协议。

如果这是一个基于 TCP/IP 的连接,并且您只想获取远程 IP,您可以使用类似以下内容

$address = $connection->getRemoteAddress();
$ip = trim(parse_url($address, PHP_URL_HOST), '[]');
echo 'Connection with ' . $ip . PHP_EOL;

getLocalAddress()

getLocalAddress(): ?string 方法返回已建立此连接的完整本地地址(URI)。

$address = $connection->getLocalAddress();
echo 'Connection with ' . $address . PHP_EOL;

如果无法确定本地地址或此时未知(例如,在连接关闭后),它可能会返回一个 NULL 值。

否则,它将以字符串值返回完整地址(URI),例如 tcp://127.0.0.1:8080tcp://[::1]:80tls://127.0.0.1:443unix://example.sockunix:///path/to/example.sock。请注意,各个 URI 组件是应用程序特定的,并取决于底层传输协议。

此方法与 getRemoteAddress() 方法互补,因此不应混淆。

如果您的 TcpServer 实例正在监听多个接口(例如,使用地址 0.0.0.0),您可以使用此方法来找出哪个接口实际上接受了此连接(例如,公共或本地接口)。

如果您的系统有多个接口(例如,WAN 和 LAN 接口),您可以使用此方法来找出实际用于此连接的接口。

服务器使用

ServerInterface

ServerInterface 负责提供接受传入流式连接的接口,例如正常的 TCP/IP 连接。

大多数高级组件(如 HTTP 服务器)接受实现此接口的实例以接受传入流式连接。这通常通过依赖注入来完成,因此实际上交换此实现与其他接口实现相对简单。这意味着您应该对接口类型进行提示,而不是对接口的具体实现进行提示。

除了定义一些方法之外,此接口还实现了 EventEmitterInterface,允许您对某些事件做出反应。

连接事件

当建立新的连接时,即新客户端连接到此服务器套接字时,将发出 connection 事件。

$socket->on('connection', function (React\Socket\ConnectionInterface $connection) {
    echo 'new connection' . PHP_EOL;
});

有关处理传入连接的详细信息,请参阅 ConnectionInterface

错误事件

当从客户端接受新连接时出现错误时,将发出 error 事件。

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

请注意,这不是一个致命的错误事件,即服务器即使在发生此事件后还会继续监听新的连接。

getAddress()

您可以使用 getAddress(): ?string 方法来返回服务器当前正在监听的完整地址(URI)。

$address = $socket->getAddress();
echo 'Server listening on ' . $address . PHP_EOL;

如果无法确定地址或此时未知(例如,在套接字关闭后),它可能会返回一个 NULL 值。

否则,它将以字符串值返回完整地址(URI),例如 tcp://127.0.0.1:8080tcp://[::1]:80tls://127.0.0.1:443unix://example.sockunix:///path/to/example.sock。请注意,各个 URI 组件是应用程序特定的,并取决于底层传输协议。

如果这是一个基于TCP/IP的服务器,并且您只想使用本地端口,您可以使用类似以下的方式:

$address = $socket->getAddress();
$port = parse_url($address, PHP_URL_PORT);
echo 'Server listening on port ' . $port . PHP_EOL;

暂停()

可以使用pause(): void方法来暂停接受新的连接。

从EventLoop中移除socket资源,从而停止接受新的连接。请注意,监听socket保持活跃状态,不会被关闭。

这意味着新的传入连接将保持在操作系统的backlog中,直到其可配置的backlog被填满。一旦backlog被填满,操作系统可能会拒绝进一步的传入连接,直到backlog通过恢复接受新的连接再次被清空。

一旦服务器被暂停,不应再发出任何connection事件。

$socket->pause();

$socket->on('connection', assertShouldNeverCalled());

尽管一般不推荐,此方法仅为建议性,服务器可能继续发出connection事件。

除非另有说明,成功打开的服务器不应以暂停状态启动。

您可以通过再次调用resume()来继续处理事件。

请注意,这两种方法可以多次调用,特别是多次调用pause()不应有任何效果。同样,在调用close()之后调用也是无操作的。

恢复()

可以使用resume(): void方法来恢复接受新的传入连接。

在之前的pause()之后将socket资源重新附加到EventLoop。

$socket->pause();

Loop::addTimer(1.0, function () use ($socket) {
    $socket->resume();
});

请注意,这两种方法可以多次调用,特别是如果没有先前的pause()而调用resume()不应有任何效果。同样,在调用close()之后调用也是无操作的。

关闭()

可以使用close(): void方法来关闭此监听socket。

这将停止在此socket上监听新的传入连接。

echo 'Shutting down server socket' . PHP_EOL;
$socket->close();

在同一实例上多次调用此方法是无操作的。

SocketServer

SocketServer类是本包中实现ServerInterface的主要类,允许您接受传入的流连接,如明文TCP/IP或安全的TLS连接流。

为了接受明文TCP/IP连接,您可以简单地传递一个主机和端口号组合,如下所示:

$socket = new React\Socket\SocketServer('127.0.0.1:8080');

监听本地主机地址127.0.0.1意味着它将无法从系统外部访问。为了更改socket监听的主机,您可以提供接口的IP地址或使用特殊的0.0.0.0地址来监听所有接口。

$socket = new React\Socket\SocketServer('0.0.0.0:8080');

如果您想监听IPv6地址,您必须将主机用方括号括起来。

$socket = new React\Socket\SocketServer('[::1]:8080');

为了使用随机端口分配,您可以使用端口号0

$socket = new React\Socket\SocketServer('127.0.0.1:0');
$address = $socket->getAddress();

为了监听Unix域socket(UDS)路径,您必须使用unix://方案来前缀URI。

$socket = new React\Socket\SocketServer('unix:///tmp/server.sock');

为了监听现有的文件描述符(FD)编号,您必须使用php://fd/方案来前缀URI,如下所示:

$socket = new React\Socket\SocketServer('php://fd/3');

如果给定的URI无效,不包含端口号,包含其他方案,或者它包含主机名,它将抛出InvalidArgumentException

// throws InvalidArgumentException due to missing port
$socket = new React\Socket\SocketServer('127.0.0.1');

如果给定的URI看起来是有效的,但监听它失败(例如,如果端口已被占用,或者端口号低于1024可能需要root访问等),它将抛出RuntimeException

$first = new React\Socket\SocketServer('127.0.0.1:8080');

// throws RuntimeException because port is already in use
$second = new React\Socket\SocketServer('127.0.0.1:8080');

请注意,这些错误条件可能因您的系统或/和配置而异。请参阅异常消息和代码以获取有关实际错误条件的更多详细信息。

您可以选择指定底层的流socket资源的TCP socket上下文选项,如下所示:

$socket = new React\Socket\SocketServer('[::1]:8080', [
    'tcp' => [
        'backlog' => 200,
        'so_reuseport' => true,
        'ipv6_v6only' => true
    ]
]);

请注意,可用的套接字上下文选项、它们的默认值以及更改这些选项的效果可能会根据您的系统和/或PHP版本而有所不同。传递未知上下文选项不会产生任何效果。除非显式指定,否则backlog上下文选项的默认值为511

您可以通过简单地添加tls:// URI方案来启动安全的TLS(以前称为SSL)服务器。内部,它将等待明文TCP/IP连接,然后对每个连接执行TLS握手。因此,它需要有效的TLS上下文选项,如果您使用PEM编码的证书文件,其基本形式可能如下所示

$socket = new React\Socket\SocketServer('tls://127.0.0.1:8080', [
    'tls' => [
        'local_cert' => 'server.pem'
    ]
]);

请注意,证书文件不会在实例化时加载,而是在传入连接初始化其TLS上下文时加载。这意味着任何无效的证书文件路径或内容都只会导致稍后发生一个error事件。

如果您的私钥被密码加密,您必须像这样指定它

$socket = new React\Socket\SocketServer('tls://127.0.0.1:8000', [
    'tls' => [
        'local_cert' => 'server.pem',
        'passphrase' => 'secret'
    ]
]);

默认情况下,此服务器支持TLSv1.0+,并排除了对旧版SSLv2/SSLv3的支持。您还可以明确选择您希望与远程端协商的TLS版本

$socket = new React\Socket\SocketServer('tls://127.0.0.1:8000', [
    'tls' => [
        'local_cert' => 'server.pem',
        'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER
    ]
]);

请注意,可用的TLS上下文选项、它们的默认值以及更改这些选项的效果可能会根据您的系统和/或PHP版本而有所不同。外部上下文数组允许您同时使用tcp(以及可能更多的)上下文选项。传递未知上下文选项不会产生任何效果。如果您不使用tls://方案,则传递tls上下文选项不会产生任何效果。

每当客户端连接时,它将发出一个带有实现ConnectionInterface的连接实例的connection事件

$socket->on('connection', function (React\Socket\ConnectionInterface $connection) {
    echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL;
    
    $connection->write('hello there!' . PHP_EOL);
    …
});

有关更多详细信息,请参阅ServerInterface

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

请注意,SocketServer类是TCP/IP套接字的具体实现。如果您想在高级协议实现中做类型提示,您应使用通用的ServerInterface

高级服务器使用

TcpServer

TcpServer类实现了ServerInterface,并负责接受明文TCP/IP连接。

$server = new React\Socket\TcpServer(8080);

如上所述,$uri参数可以仅包含端口号,在这种情况下,服务器将默认监听localhost地址127.0.0.1,这意味着它无法从系统外部访问。

为了使用随机端口分配,您可以使用端口号0

$server = new React\Socket\TcpServer(0);
$address = $server->getAddress();

为了更改套接字监听的宿主地址,您可以通过构造函数提供的第一个参数提供一个IP地址,该地址可以带有可选的tcp://方案

$server = new React\Socket\TcpServer('192.168.0.1:8080');

如果您想监听IPv6地址,您必须将主机用方括号括起来。

$server = new React\Socket\TcpServer('[::1]:8080');

如果给定的URI无效,不包含端口号,包含其他方案,或者它包含主机名,它将抛出InvalidArgumentException

// throws InvalidArgumentException due to missing port
$server = new React\Socket\TcpServer('127.0.0.1');

如果给定的URI看起来是有效的,但监听它失败(例如,如果端口已被占用,或者端口号低于1024可能需要root访问等),它将抛出RuntimeException

$first = new React\Socket\TcpServer(8080);

// throws RuntimeException because port is already in use
$second = new React\Socket\TcpServer(8080);

请注意,这些错误条件可能因您的系统或/和配置而异。请参阅异常消息和代码以获取有关实际错误条件的更多详细信息。

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

可选地,您可以指定底层数据流套接字资源的套接字上下文选项,如下所示

$server = new React\Socket\TcpServer('[::1]:8080', null, [
    'backlog' => 200,
    'so_reuseport' => true,
    'ipv6_v6only' => true
]);

请注意,可用的套接字上下文选项、它们的默认值以及更改这些选项的效果可能会根据您的系统和/或PHP版本而有所不同。传递未知上下文选项不会产生任何效果。除非显式指定,否则backlog上下文选项的默认值为511

每当客户端连接时,它将发出一个带有实现ConnectionInterface的连接实例的connection事件

$server->on('connection', function (React\Socket\ConnectionInterface $connection) {
    echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL;
    
    $connection->write('hello there!' . PHP_EOL);
    …
});

有关更多详细信息,请参阅ServerInterface

SecureServer

SecureServer类实现了ServerInterface,并负责提供安全的TLS(以前称为SSL)服务器。

它是通过包装一个TcpServer实例来实现的,该实例等待明文TCP/IP连接,然后对每个连接执行TLS握手。因此,它需要有效的TLS上下文选项,如果您使用PEM编码的证书文件,其基本形式可能如下所示

$server = new React\Socket\TcpServer(8000);
$server = new React\Socket\SecureServer($server, null, [
    'local_cert' => 'server.pem'
]);

请注意,证书文件不会在实例化时加载,而是在传入连接初始化其TLS上下文时加载。这意味着任何无效的证书文件路径或内容都只会导致稍后发生一个error事件。

如果您的私钥被密码加密,您必须像这样指定它

$server = new React\Socket\TcpServer(8000);
$server = new React\Socket\SecureServer($server, null, [
    'local_cert' => 'server.pem',
    'passphrase' => 'secret'
]);

默认情况下,此服务器支持TLSv1.0+,并排除了对旧版SSLv2/SSLv3的支持。您还可以明确选择您希望与远程端协商的TLS版本

$server = new React\Socket\TcpServer(8000);
$server = new React\Socket\SecureServer($server, null, [
    'local_cert' => 'server.pem',
    'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER
]);

请注意,可用的TLS上下文选项、它们的默认值以及更改这些选项的效果可能因您的系统或PHP版本而异。传递未知上下文选项不会产生任何效果。

每当客户端完成TLS握手时,它将发出一个带有实现ConnectionInterface的连接实例的connection事件。

$server->on('connection', function (React\Socket\ConnectionInterface $connection) {
    echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL;
    
    $connection->write('hello there!' . PHP_EOL);
    …
});

每当客户端无法成功执行TLS握手时,它将发出一个error事件,然后关闭底层的TCP/IP连接。

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

有关更多详细信息,请参阅ServerInterface

请注意,SecureServer类是TLS套接字的实际实现。如果您想在高级协议实现中使用类型提示,您应该使用通用的ServerInterface

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

高级用法:尽管允许任何ServerInterface作为第一个参数,除非您知道自己在做什么,否则您应该将TcpServer实例作为第一个参数传递。内部,SecureServer必须设置底层数据流资源所需的TLS上下文选项。这些资源不会通过本包中定义的任何接口暴露,而仅通过内部Connection类暴露。TcpServer类保证发出实现ConnectionInterface的连接,并使用内部Connection类来暴露这些底层资源。如果您使用自定义的ServerInterface,并且其connection事件不符合此要求,则SecureServer将发出一个error事件,然后关闭底层连接。

UnixServer

UnixServer类实现了ServerInterface,并负责在Unix域套接字(UDS)上接受连接。

$server = new React\Socket\UnixServer('/tmp/server.sock');

与上述内容相同,$uri参数可以只包含套接字路径或以unix://方案前缀的套接字路径。

如果给定的URI似乎有效,但监听失败(例如,如果套接字已被使用或文件不可访问等),则将抛出RuntimeException

$first = new React\Socket\UnixServer('/tmp/same.sock');

// throws RuntimeException because socket is already in use
$second = new React\Socket\UnixServer('/tmp/same.sock');

请注意,这些错误条件可能因您的系统或/和配置而异。特别是,当UDS路径已存在且无法绑定时,Zend PHP只报告“未知错误”。在这种情况下,您可能想检查给定的UDS路径上的is_file(),以报告更友好的错误消息。有关实际错误条件的更多详细信息,请参阅异常消息和代码。

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

每当客户端连接时,它将发出一个带有实现ConnectionInterface的连接实例的connection事件

$server->on('connection', function (React\Socket\ConnectionInterface $connection) {
    echo 'New connection' . PHP_EOL;

    $connection->write('hello there!' . PHP_EOL);
    …
});

有关更多详细信息,请参阅ServerInterface

LimitingServer

LimitingServer装饰器包装给定的ServerInterface,并负责限制和跟踪此服务器实例的打开连接。

每当底层服务器发出connection事件时,它将检查其限制,然后要么

  • 通过将其添加到打开连接列表中来跟踪此连接,然后转发connection事件
  • 在达到其限制时拒绝(关闭)连接,并将转发一个error事件。

每当连接关闭时,它将从打开连接列表中删除此连接。

$server = new React\Socket\LimitingServer($server, 100);
$server->on('connection', function (React\Socket\ConnectionInterface $connection) {
    $connection->write('hello there!' . PHP_EOL);
    …
});

有关更多详细信息,请参阅第二个示例

您必须传递最大打开连接数,以确保服务器在超过此限制时自动拒绝(关闭)连接。在这种情况下,它将发出一个error事件来通知此情况,并且不会发出任何connection事件。

$server = new React\Socket\LimitingServer($server, 100);
$server->on('connection', function (React\Socket\ConnectionInterface $connection) {
    $connection->write('hello there!' . PHP_EOL);
    …
});

您可以选择传递一个null限制,以不对打开连接数进行限制,并继续接受新的连接,直到操作系统资源(例如打开文件句柄)耗尽。如果您不希望应用限制但仍然想使用getConnections()方法,这可能很有用。

您可以选择配置服务器,在达到连接限制后暂停接受新的连接。在这种情况下,它将暂停底层服务器,并且不再处理任何新的连接,因此也不再关闭任何过量的连接。底层操作系统负责在达到其限制之前保持待处理连接的队列,此时它将开始拒绝进一步的连接。一旦服务器低于连接限制,它将继续从队列中消耗连接,并处理每个连接上的任何未处理数据。这种模式可能对一些设计为等待响应消息的协议(如HTTP)很有用,但对需要即时响应的协议(如交互式聊天中的“欢迎”消息)可能不太有用。

$server = new React\Socket\LimitingServer($server, 100, true);
$server->on('connection', function (React\Socket\ConnectionInterface $connection) {
    $connection->write('hello there!' . PHP_EOL);
    …
});
getConnections()

可以使用 getConnections(): ConnectionInterface[] 方法来返回包含所有当前活动连接的数组。

foreach ($server->getConnection() as $connection) {
    $connection->write('Hi!');
}

客户端使用

ConnectorInterface

ConnectorInterface 负责提供建立流式连接的接口,例如正常的TCP/IP连接。

这是在此包中定义的主要接口,并且在React的庞大生态系统中被广泛使用。

大多数高级组件(如HTTP、数据库或其他网络服务客户端)接受实现此接口的实例以创建与底层网络服务的TCP/IP连接。这通常通过依赖注入来完成,因此实际上替换此实现与其他接口实现相对简单。

该接口只提供了一种方法

connect()

可以使用 connect(string $uri): PromiseInterface<ConnectionInterface> 方法创建到指定远程地址的流式连接。

它返回一个Promise,在成功时满足具有 ConnectionInterface 的流,如果连接不成功则拒绝并抛出 Exception

$connector->connect('google.com:443')->then(
    function (React\Socket\ConnectionInterface $connection) {
        // connection successfully established
    },
    function (Exception $error) {
        // failed to connect due to $error
    }
);

有关更多详细信息,请参阅ConnectionInterface

返回的Promise必须以这种方式实现,以便在它仍然是挂起状态时可以取消。取消挂起的Promise必须使用Exception拒绝其值。它应根据适用性清理任何底层资源和引用。

$promise = $connector->connect($uri);

$promise->cancel();

Connector

Connector 类是这个包中的主要类,它实现了 ConnectorInterface 并允许您创建流式连接。

您可以使用此连接器创建任何类型的流式连接,例如明文TCP/IP、安全TLS或本地Unix连接流。

它绑定到主事件循环,可以使用如下方式使用:

$connector = new React\Socket\Connector();

$connector->connect($uri)->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
}, function (Exception $e) {
    echo 'Error: ' . $e->getMessage() . PHP_EOL;
});

为了创建明文TCP/IP连接,您只需传递一个主机和端口号组合,如下所示:

$connector->connect('www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

如果您在目标URI中未指定URI方案,它将默认假设 tcp:// 并建立明文TCP/IP连接。请注意,TCP/IP连接需要在目标URI中包含主机和端口部分,如上所示,所有其他URI组件都是可选的。

为了创建安全的TLS连接,您可以使用 tls:// URI方案,如下所示:

$connector->connect('tls://www.google.com:443')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

为了创建本地Unix域套接字连接,您可以使用 unix:// URI方案,如下所示:

$connector->connect('unix:///tmp/demo.sock')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

getRemoteAddress()方法将返回目标Unix域套接字(UDS)路径,该路径是作为给定的connect()方法的参数提供的,包括unix://方案,例如unix:///tmp/demo.sock。大多数情况下,getLocalAddress()方法将返回一个null值,因为在此处此值不适用于UDS连接。

在内部,Connector被实现为对在这个包中实现的较低级别连接器的高级门面。这意味着它也共享它们的所有功能和实现细节。如果你想在高级协议实现中做类型提示,你应该使用通用的ConnectorInterface

v1.4.0版本开始,Connector类默认使用Happy Eyeballs算法来自动在给定主机名时通过IPv4或IPv6进行连接。这会同时尝试使用IPv4和IPv6进行连接(优先考虑IPv6),从而避免了用户在使用不完美的IPv6连接或配置时通常遇到的问题。如果你想要恢复到旧的行为,即只进行IPv4查找并只尝试单个IPv4连接,你可以这样设置Connector

$connector = new React\Socket\Connector([
    'happy_eyeballs' => false
]);

同样,你也可以按照以下方式影响默认的DNS行为。默认情况下,Connector类将尝试检测你的系统DNS设置(如果无法确定系统设置,则使用Google的公共DNS服务器8.8.8.8作为后备)将所有公共主机名解析为底层IP地址。如果你明确想要使用自定义DNS服务器(例如本地DNS中继或公司范围内的DNS服务器),你可以这样设置Connector

$connector = new React\Socket\Connector([
    'dns' => '127.0.1.1'
]);

$connector->connect('localhost:80')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

如果你根本不想使用DNS解析器,只想连接到IP地址,你也可以这样设置你的Connector

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

$connector->connect('127.0.0.1:80')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

高级:如果你需要一个自定义的DNS React\Dns\Resolver\ResolverInterface实例,你也可以这样设置你的Connector

$dnsResolverFactory = new React\Dns\Resolver\Factory();
$resolver = $dnsResolverFactory->createCached('127.0.1.1');

$connector = new React\Socket\Connector([
    'dns' => $resolver
]);

$connector->connect('localhost:80')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

默认情况下,tcp://tls:// URI方案将使用尊重你的default_socket_timeout ini设置的超时值(默认为60秒)。如果你想要一个自定义的超时值,你可以这样简单地传递

$connector = new React\Socket\Connector([
    'timeout' => 10.0
]);

同样,如果你不想应用任何超时,并让操作系统来处理,你可以传递一个布尔标志,如下所示

$connector = new React\Socket\Connector([
    'timeout' => false
]);

默认情况下,Connector支持tcp://tls://unix:// URI方案。如果你想明确禁止其中任何一个,你可以简单地传递布尔标志,如下所示

// only allow secure TLS connections
$connector = new React\Socket\Connector([
    'tcp' => false,
    'tls' => true,
    'unix' => false,
));

$connector->connect('tls://google.com:443')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

tcp://tls://也接受传递给底层连接器的额外上下文选项。如果你想要明确传递额外的上下文选项,你可以简单地传递上下文选项数组的这种方式

// allow insecure TLS connections
$connector = new React\Socket\Connector([
    'tcp' => [
        'bindto' => '192.168.0.1:0'
    ],
    'tls' => [
        'verify_peer' => false,
        'verify_peer_name' => false
    ],
]);

$connector->connect('tls://localhost:443')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

默认情况下,此连接器支持TLSv1.0+并排除对旧版SSLv2/SSLv3的支持。你也可以明确选择你想要与远程端协商的TLS版本

$connector = new React\Socket\Connector([
    'tls' => [
        'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
    ]
]);

有关上下文选项的更多详细信息,请参阅PHP关于socket上下文选项SSL上下文选项的文档。

高级:默认情况下,Connector支持tcp://tls://unix:// URI方案。为此,它会自动设置所需的连接器类。如果你想为这些中的任何一个明确传递自定义连接器,你可以简单地传递一个实现ConnectorInterface的实例,如下所示

$dnsResolverFactory = new React\Dns\Resolver\Factory();
$resolver = $dnsResolverFactory->createCached('127.0.1.1');
$tcp = new React\Socket\HappyEyeBallsConnector(null, new React\Socket\TcpConnector(), $resolver);

$tls = new React\Socket\SecureConnector($tcp);

$unix = new React\Socket\UnixConnector();

$connector = new React\Socket\Connector([
    'tcp' => $tcp,
    'tls' => $tls,
    'unix' => $unix,

    'dns' => false,
    'timeout' => false,
]);

$connector->connect('google.com:80')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

在内部,除非像上面的示例那样禁用DNS,否则tcp://连接器始终会被DNS解析器包装。在这种情况下,tcp://连接器接收实际的域名而不是仅解析的IP地址,因此负责执行查找。内部,自动创建的tls://连接器始终包装底层的tcp://连接器,在启用安全的TLS模式之前建立底层的明文TCP/IP连接。如果您只想为安全的TLS连接使用自定义的底层tcp://连接器,则可以像上面那样显式传递一个tls://连接器。内部,除非像上面的示例那样禁用超时,否则tcp://tls://连接器始终会被TimeoutConnector包装。

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

高级客户端使用

TcpConnector

TcpConnector类实现了ConnectorInterface接口,并允许您创建到任何IP-端口号组合的明文TCP/IP连接。

$tcpConnector = new React\Socket\TcpConnector();

$tcpConnector->connect('127.0.0.1:80')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

请参阅示例

可以通过取消其挂起的承诺来取消挂起的连接尝试,如下所示

$promise = $tcpConnector->connect('127.0.0.1:80');

$promise->cancel();

在挂起的承诺上调用cancel()将关闭底层的套接字资源,从而取消挂起的TCP/IP连接,并拒绝产生的承诺。

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

您可以选择像这样将额外的套接字上下文选项传递给构造函数

$tcpConnector = new React\Socket\TcpConnector(null, [
    'bindto' => '192.168.0.1:0'
]);

请注意,此类仅允许您连接到IP-端口号组合。如果给定的URI无效,不包含有效的IP地址和端口号或包含任何其他方案,它将使用InvalidArgumentException拒绝。

如果给定的URI看起来是有效的,但连接失败(例如,如果远程主机拒绝连接等),它将使用RuntimeException拒绝。

如果您想连接到主机名-端口号组合,请参阅以下章节。

高级用法:内部,TcpConnector为每个流资源分配一个空的上下文资源。如果目标URI包含一个hostname查询参数,其值将用于设置TLS对端名称。这被SecureConnectorDnsConnector用于验证对端名称,也可以用于您想自定义TLS对端名称的情况。

HappyEyeBallsConnector

HappyEyeBallsConnector类实现了ConnectorInterface接口,并允许您创建到任何主机名-端口号组合的明文TCP/IP连接。内部,它实现了来自RFC6555RFC8305的happy eyeballs算法来支持IPv6和IPv4主机名。

它是通过装饰一个给定的TcpConnector实例来实现的,这样它首先通过DNS(如果适用)查找给定的域名,然后建立到解析的目标IP地址的底层TCP/IP连接。

请确保像这样设置您的DNS解析器和底层TCP连接器

$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dns = $dnsResolverFactory->createCached('8.8.8.8');

$dnsConnector = new React\Socket\HappyEyeBallsConnector(null, $tcpConnector, $dns);

$dnsConnector->connect('www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

请参阅示例

可以通过取消其挂起的承诺来取消挂起的连接尝试,如下所示

$promise = $dnsConnector->connect('www.google.com:80');

$promise->cancel();

在挂起的承诺上调用cancel()将取消底层的DNS查找和/或底层TCP/IP连接并拒绝产生的承诺。

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

高级用法:内部,HappyEyeBallsConnector依赖于一个Resolver来查找给定主机名的IP地址。然后,它将目标URI中的主机名替换为该IP地址,并附加一个hostname查询参数,并将此更新后的URI传递给底层连接器。Happy Eye Balls算法描述了查找给定主机名的IPv6和IPv4地址,因此此连接器发送两个DNS查找以获取A和AAAA记录。然后,它使用所有IP地址(包括v6和v4)并尝试以50ms的间隔连接到所有这些地址。在IPv6和IPv4地址之间交替。当建立连接时,取消所有其他DNS查找和连接尝试。

DnsConnector

DnsConnector类实现了ConnectorInterface,并允许您创建任何主机名-端口号组合的明文TCP/IP连接。

它是通过装饰一个给定的TcpConnector实例来实现的,这样它首先通过DNS(如果适用)查找给定的域名,然后建立到解析的目标IP地址的底层TCP/IP连接。

请确保像这样设置您的DNS解析器和底层TCP连接器

$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dns = $dnsResolverFactory->createCached('8.8.8.8');

$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns);

$dnsConnector->connect('www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write('...');
    $connection->end();
});

请参阅示例

可以通过取消其挂起的承诺来取消挂起的连接尝试,如下所示

$promise = $dnsConnector->connect('www.google.com:80');

$promise->cancel();

在挂起的Promise上调用cancel()将取消底层DNS查找和/或底层TCP/IP连接,并拒绝生成的Promise。

高级用法:内部,DnsConnector依赖于React\Dns\Resolver\ResolverInterface来查找给定主机名的IP地址。然后,它将目标URI中的主机名替换为该IP地址,并附加一个hostname查询参数,并将此更新后的URI传递给底层连接器。底层连接器因此负责连接到目标IP地址,而此查询参数可以用于检查原始主机名,并由TcpConnector用于设置TLS对等名称。如果显式给出hostname,则不会修改此查询参数,这在您希望自定义TLS对等名称时可能很有用。

SecureConnector

SecureConnector类实现了ConnectorInterface,并允许您创建任何主机名-端口号组合的安全TLS(以前称为SSL)连接。

它是通过装饰给定的DnsConnector实例来实现的,这样它首先创建一个明文TCP/IP连接,然后在流上启用TLS加密。

$secureConnector = new React\Socket\SecureConnector($dnsConnector);

$secureConnector->connect('www.google.com:443')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n");
    ...
});

请参阅示例

可以通过取消其挂起的承诺来取消挂起的连接尝试,如下所示

$promise = $secureConnector->connect('www.google.com:443');

$promise->cancel();

在挂起的Promise上调用cancel()将取消底层TCP/IP连接和/或SSL/TLS协商,并拒绝生成的Promise。

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

您可以选择像这样将额外的SSL上下文选项传递给构造函数

$secureConnector = new React\Socket\SecureConnector($dnsConnector, null, [
    'verify_peer' => false,
    'verify_peer_name' => false
]);

默认情况下,此连接器支持TLSv1.0+并排除对旧版SSLv2/SSLv3的支持。你也可以明确选择你想要与远程端协商的TLS版本

$secureConnector = new React\Socket\SecureConnector($dnsConnector, null, [
    'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
]);

高级用法:内部,SecureConnector依赖于在底层流资源上设置所需的上下文选项。因此,它应在连接器堆栈中的某个位置与TcpConnector一起使用,以便为每个流资源分配一个空的上下文资源并验证对等名称。否则,不这样做可能会导致TLS对等名称不匹配错误或一些难以追踪的竞争条件,因为所有流资源都将使用单个、共享的默认上下文资源。

TimeoutConnector

TimeoutConnector类实现了ConnectorInterface,并允许您向任何现有连接器实例添加超时处理。

它是通过装饰任何给定的ConnectorInterface实例并启动一个计时器来实现的,该计时器将自动拒绝和取消任何底层连接尝试,如果它花费的时间过长。

$timeoutConnector = new React\Socket\TimeoutConnector($connector, 3.0);

$timeoutConnector->connect('google.com:80')->then(function (React\Socket\ConnectionInterface $connection) {
    // connection succeeded within 3.0 seconds
});

请参阅示例中的任何示例。

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

可以通过取消其挂起的承诺来取消挂起的连接尝试,如下所示

$promise = $timeoutConnector->connect('google.com:80');

$promise->cancel();

在挂起的Promise上调用cancel()将取消底层连接尝试,取消计时器并拒绝生成的Promise。

UnixConnector

UnixConnector类实现了ConnectorInterface,并允许您连接到Unix域套接字(UDS)路径,如下所示

$connector = new React\Socket\UnixConnector();

$connector->connect('/tmp/demo.sock')->then(function (React\Socket\ConnectionInterface $connection) {
    $connection->write("HELLO\n");
});

连接到Unix域套接字是一个原子操作,即其承诺将立即解决(要么解决要么拒绝)。因此,在结果承诺上调用cancel()没有任何效果。

getRemoteAddress()方法将返回传递给connect()方法的目标Unix域套接字(UDS)路径,并在前面加上unix://方案,例如unix:///tmp/demo.sock。大多数情况下,getLocalAddress()方法将返回一个null值,因为这个值不适用于这里的UDS连接。

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

FixedUriConnector

FixedUriConnector类实现了ConnectorInterface并装饰现有的连接器,使其始终使用固定的、预配置的URI。

这可以用于不支持某些URI的消费者,例如当你想明确连接到Unix域套接字(UDS)路径而不是连接到由高级API假设的默认地址时。

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

// destination will be ignored, actually connects to Unix domain socket
$promise = $connector->connect('localhost:80');

安装

安装此库的推荐方法是通过Composer你是Composer新手吗?

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

composer require react/socket:^3@dev

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

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

旧版PHP < 7.3.3(以及PHP < 7.2.15)存在一个bug,其中feof()可能会在碎片化的TLS记录上阻塞并使用100%的CPU使用率。我们通过一次消耗完整的接收缓冲区来尝试解决这个问题,以避免在TLS缓冲区中的旧数据。这已知的解决方法可以解决表现良好的对等方的CPU使用率过高,但这可能导致高吞吐量场景中的非常大的数据块。由于网络I/O缓冲区或受影响版本的恶意对等方,buggy行为仍然可能被触发,强烈建议升级。

旧版PHP < 7.1.4存在一个bug,在一次性通过TLS流写入大量数据时。我们通过仅对旧版PHP版本限制写入块大小为8192字节来尝试解决这个问题。这只是一种解决方案,并在受影响的版本上产生可感知的性能损失。

测试

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

composer install

要运行测试套件,请转到项目根目录并运行

vendor/bin/phpunit

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

vendor/bin/phpunit --exclude-group internet

许可证

MIT,请参阅LICENSE文件