坦克洋葱科/reactphp-socket-with-properties

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

1.x-dev 2023-01-09 12:05 UTC

This package is auto-updated.

Last update: 2024-09-09 16:01:44 UTC


README

CI status

安装

  1. 添加到composer.json
    "replace": {
        "react/socket": "*"
    },
  1. 安装
$ composer require tankonyako/reactphp-socket-with-properties:1.x-dev

使用属性

连接现在具有属性字段以存储自定义信息。

$connection->once('data', function ($chunk) use ($connection, $that) {
    var_dump($connection->properties);
});

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

该套接字库提供了基于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()

pause(): void 方法可用于暂停接受新的传入连接。

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

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

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

$socket->pause();

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

尽管通常不建议,但此方法仅为建议性,服务器可能会继续发出 connection 事件。

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

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

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

resume()

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

在之前的 pause() 之后,重新将套接字资源附加到 EventLoop 上。

$socket->pause();

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

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

close()

close(): void 方法可用于关闭此监听套接字。

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

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 上监听意味着它不能从系统外部访问。要更改套接字正在监听的主机,您可以提供一个接口的 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 域套接字(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');

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

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

$socket = new React\Socket\SocketServer('[::1]:8080', array(
    'tcp' => array(
        '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', array(
    'tls' => array(
        'local_cert' => 'server.pem'
    )
));

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

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

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

默认情况下,该服务器支持TLSv1.0+,并排除对旧版SSLv2/SSLv3的支持。从PHP 5.6+开始,您还可以显式选择与远程端协商的TLS版本。

$socket = new React\Socket\SocketServer('tls://127.0.0.1:8000', array(
    'tls' => array(
        '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

更改日志v1.9.0:已添加此类,并具有改进的构造函数签名,作为之前Server类的替代,以避免任何歧义。旧名称已弃用,不应再使用。

高级服务器使用

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, array(
    '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, array(
    'local_cert' => 'server.pem'
));

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

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

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

默认情况下,该服务器支持TLSv1.0+,并排除对旧版SSLv2/SSLv3的支持。从PHP 5.6+开始,您还可以显式选择与远程端协商的TLS版本。

$server = new React\Socket\TcpServer(8000);
$server = new React\Socket\SecureServer($server, null, array(
    '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,Exception>方法创建到给定远程地址的流式连接。

它返回一个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(array(
    'happy_eyeballs' => false
));

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

$connector = new React\Socket\Connector(array(
    '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(array(
    '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(array(
    '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(array(
    'timeout' => 10.0
));

类似地,如果您不想设置任何超时,并让操作系统处理,您可以传递一个布尔标志,如下所示

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

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

// only allow secure TLS connections
$connector = new React\Socket\Connector(array(
    '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(array(
    'tcp' => array(
        'bindto' => '192.168.0.1:0'
    ),
    'tls' => array(
        'verify_peer' => false,
        'verify_peer_name' => false
    ),
));

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

默认情况下,此连接器支持 TLSv1.0+,不包括对旧版 SSLv2/SSLv3 的支持。从 PHP 5.6+ 开始,您也可以明确选择与远程端协商的 TLS 版本

$connector = new React\Socket\Connector(array(
    'tls' => array(
        '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(array(
    '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();
});

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

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

变更日志 v1.9.0:构造函数的签名已更新,将可选的 $context 作为第一个参数,将可选的 $loop 作为第二个参数。之前的签名已被弃用,不应再使用。

// constructor signature as of v1.9.0
$connector = new React\Socket\Connector(array $context = [], ?LoopInterface $loop = null);

// legacy constructor signature before v1.9.0
$connector = new React\Socket\Connector(?LoopInterface $loop = null, array $context = []);

高级客户端使用

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, array(
    '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地址。然后它会用这个IP地址替换目标URI中的主机名,并附加一个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地址。然后它会用这个IP地址替换目标URI中的主机名,并附加一个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, array(
    'verify_peer' => false,
    'verify_peer_name' => false
));

默认情况下,此连接器支持 TLSv1.0+,不包括对旧版 SSLv2/SSLv3 的支持。从 PHP 5.6+ 开始,您也可以明确选择与远程端协商的 TLS 版本

$secureConnector = new React\Socket\SecureConnector($dnsConnector, null, array(
    '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:^1.12

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

此项目旨在在任何平台上运行,因此不需要任何PHP扩展,并支持在PHP 5.3到当前PHP 8+和HHVM上运行。强烈建议使用此项目的最新支持版本,部分原因是其显著的性能改进,部分原因是旧版本PHP需要以下所述的几个解决方案。

从PHP 5.6开始,安全TLS连接收到了一些重大升级,默认值现在更加安全,而旧版本需要显式上下文选项。此库不负责这些上下文选项,因此需要消费者根据上述描述设置适当的上下文选项。

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

PHP < 7.1.4(和PHP < 7.0.18)在通过TLS流一次性写入大量数据时存在一个bug。我们通过将旧PHP版本的写入块大小限制为8192字节来尝试解决这个问题。这只是一种解决方案,并且会在受影响的版本上产生明显的性能惩罚。

此项目还支持在HHVM上运行。请注意,非常旧的HHVM < 3.8不支持安全TLS连接,因为它缺少所需的stream_socket_enable_crypto()函数。因此,在受影响的版本上尝试创建安全TLS连接将返回一个被拒绝的承诺。此问题也由我们的测试套件处理,它将在受影响的版本上跳过相关测试。

测试

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

composer install

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

vendor/bin/phpunit

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

vendor/bin/phpunit --exclude-group internet

许可证

麻省理工学院(MIT),参见 许可文件