stream_socket_client的异步使用方式

司寇望
2023-12-01

函数的基本介绍

PHP 5、7、8,stream_socket_client用于建立网络或IPC socket连接。函数的参数和返回为:

stream_socket_client(
    string $address,
    int &$error_code = null,
    string &$error_message = null,
    ?float $timeout = null,
    int $flags = STREAM_CLIENT_CONNECT,
    ?resource $context = null
): resource|false

其中第一个参数必填,其它为可选:

  1. address:传输协议、域名/IP、端口
  2. error_code:一个变量,调用结束后里面存储调用时发生的错误编号
  3. error_message:一个变量,调用结束后里面存储调用时发生的错误的文字提示
  4. timeout:超时时间,单位是秒,只在发起连接的时候有用,且只有在非异步连接的情况下才有用
  5. flags:标志位,四种可能的常量、功能可以通过位运算来启用:STREAM_CLIENT_CONNECT、STREAM_CLIENT_ASYNC_CONNECT、STREAM_CLIENT_PERSISTENT,分别是:默认的同步、异步、持久连接
  6. context:作用于流的上下文选项

函数返回值:调用成功会返回一个流的资源,如果失败返回false。

同步调用方式

本地测试的样例:

if (false === ($r = stream_socket_client('tcp://www.baidu.com:443', $error_code, $error_message, 3))) {
    echo '连接失败' . PHP_EOL;
    var_dump($error_code, $error_message);
    exit(1);
}

echo '连接成功' . PHP_EOL;
fclose($r);

样例使用同步的方式打开到www.baidu.com站点的443端口的TCP连接,在连接成功之后输出"连接成功"并关闭连接。如果失败那么输出失败的error_code和error_message。本地网络正常的情况下样例执行情况是:

$ php test.php
连接成功
$

如果遇到连接失败,比如将端口号从443改成一个没用的端口,比如8000,那么执行情况类似于:

$ time php test.php
PHP Warning:  stream_socket_client(): unable to connect to tcp://www.baidu.com:8000 (由于连接方在一段时间后没有正确答复
或连接的主机没有反应,连接尝试失败。
) in D:\test.php on line 3

Warning: stream_socket_client(): unable to connect to tcp://www.baidu.com:8000 (由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。
) in D:\test.php on line 3
连接失败
int(10060)
string(107) "由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。
"


real    0m3.069s
user    0m0.000s
sys     0m0.000s

$

结合执行时间可以看出,这个错误是因为超时时间3秒后远程服务器8000端口并不能响应连接,所以超时连接失败。此外因为默认是通过同步的方式连接远程服务器的,所以网络连接建立的时间内脚本阻塞在stream_socket_client。

异步调用方式

if (false === ($r = stream_socket_client('tcp://www.baidu.com:8000', $error_code, $error_message, 3, STREAM_CLIENT_ASYNC_CONNECT))) {
    echo '连接失败' . PHP_EOL;
    var_dump($error_code, $error_message);
    exit(1);
}

echo '连接成功' . PHP_EOL;
fclose($r);

和第一个样例类似,端口号8000,超时时间设置3秒和设置flags为STREAM_CLIENT_ASYNC_CONNECT。执行情况:

$ time php test.php
连接成功

real    0m0.832s
user    0m0.000s
sys     0m0.000s

所以,在情况不变,还是需要等待一段时间才能确定网络连接是不是成功的前提下,异步方式调用很快地stream_socket_client函数就返回了一个流资源,而不是false。并且你可以对这个流资源调用fclose关闭它。

当然,如果这时候调用函数发送数据是会报错的,样例代码和执行结果:

if (false === ($r = stream_socket_client('tcp://www.baidu.com:8000', $error_code, $error_message, 3, STREAM_CLIENT_ASYNC_CONNECT))) {
    echo '连接失败' . PHP_EOL;
    var_dump($error_code, $error_message);
    exit(1);
}

echo '连接成功' . PHP_EOL;

if(!stream_set_blocking($r, false)) {
    echo '设置流为非阻塞状态失败' . PHP_EOL;
    exit(1);
}

if (false === stream_socket_sendto($r, 'Hello here.')) {
    echo '发送数据失败' . PHP_EOL;
    exit(1);
}

fclose($r);

执行结果:

$ php test.php
连接成功
PHP Warning:  stream_socket_sendto(): 由于套接字没有连接并且(当使用一个 sendto 调用发送数据报套接字时)没有提供地址,发送或接收数据的请求没有被接受。

 in D:\test.php on line 16

Warning: stream_socket_sendto(): 由于套接字没有连接并且(当使用一个 sendto 调用发送数据报套接字时)没有提供地址,发送或接 收数据的请求没有被接受。

 in D:\test.php on line 16

$

就算这里将端口从8000改回443也是一样的,连接远程服务器的函数是异步时执行下面的代码是无法保证连接是否就绪的。这时需要使用stream_select之类的机制等待连接就绪:

if (false === ($r = stream_socket_client('tcp://www.baidu.com:443', $error_code, $error_message, 3, STREAM_CLIENT_ASYNC_CONNECT))) {
    echo '连接失败' . PHP_EOL;
    var_dump($error_code, $error_message);
    exit(1);
}

echo '连接成功' . PHP_EOL;

if(!stream_set_blocking($r, false)) {
    echo '设置流为非阻塞状态失败' . PHP_EOL;
    exit(1);
}

$read = $except = null;
while (true) {
    $write = [$r];
    $total = stream_select($read, $write, $except, 0, 1000);
    if (false === $total || $total <= 0) {
        continue;
    }
    foreach ($write as $ready) {
        if (false === stream_socket_sendto($ready, 'Hello here.')) {
            echo '发送数据失败' . PHP_EOL;
            exit(1);
        } else {
            echo '发送数据成功' . PHP_EOL;
            break 2;
        }
    }
}

fclose($r);

执行情况:

$ php test.php
连接成功
发送数据成功

顺便记录一个情况。stream_socket_sendto在这里一次调用可以发送的数据量是不小的,并不会受到TCP连接一个包数据负载65495字节的限制。当一次发送的数据量很大的时候,底层会对数据拆分成多个部分发送。另外一端在使用stream_get_contents接收数据的时候,我用stream_select做测试发现,一次可以读取到65495字节的数据,也可能是更大或者更小的字节数。所以在接收数据时需要考虑如何确保数据接收结束。

 类似资料: