无尘阁日记

无尘阁日记

Yii2的client的timeout不能删除啊,起作用呢。 似乎CURLOPT_CONNECTTIMEOUT这个反倒不起作用啊。
2026-03-04

你这个现象,99% 是因为你实际跑的不是 CurlTransport,而是 StreamTransport

  • timeout 之所以“删不掉、真起作用”,是因为在 StreamTransport(fopen/stream) 里,timeout 会被当成 stream context 的超时,确实生效。

  • CURLOPT_CONNECTTIMEOUT 之所以“反倒不起作用”,往往是因为 根本没走 cURL(所以所有 CURLOPT_* 都会被忽略)。

下面一步步给你钉死。

1)先确认:你的 Client 到底用的什么 Transport

在发请求前打一下:

Yii::info('httpclient transport=' . get_class($client->transport));

你会看到类似:

  • yii\httpclient\CurlTransport ✅ 走 cURL,CURLOPT_* 才可能生效

  • yii\httpclient\StreamTransport ❗走 stream,CURLOPT_* 全都不生效,但 'timeout' 会生效

如果你看到的是 StreamTransport,那就完全解释得通你现在的观察。

2)强制用 CurlTransport(让 CURLOPT_CONNECTTIMEOUT 真正生效)

$client = new \yii\httpclient\Client([
    'transport' => 'yii\httpclient\CurlTransport',
]);

然后再配:

->setOptions([
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT => 10,
])

这样 CURLOPT_CONNECTTIMEOUT 才有意义。

3)为什么你会退化到 StreamTransport?

常见原因:

A. 服务器没装/没启用 curl 扩展

看一下:

var_dump(extension_loaded('curl'));

返回 false 就会自动用 StreamTransport。

B. 代码里/配置里指定了 StreamTransport

比如全局组件配置写死了 transport。

4)即使走 cURL,CONNECTTIMEOUT 也可能“看起来不生效”的几种情况

如果你确认已经是 CurlTransport,但还是觉得不生效,通常是下面这些“误会点”:

4.1 DNS 解析不算在 CONNECTTIMEOUT 里

CURLOPT_CONNECTTIMEOUT 主要管 建立连接(TCP/握手),但 域名解析耗时可能不算进去。
(你用的是 IP 10.255.255.1,这条一般不涉及 DNS。)

4.2 连接被复用(Keep-Alive)

如果连接复用,根本没“connect”阶段了,CONNECTTIMEOUT自然不会触发。
(你这种不可达 IP 一般不会复用,但在压测/代理场景容易遇到。)

4.3 你写了多套 timeout,最终谁覆盖谁

在 Yii2 httpclient 里:

  • 'timeout' => 180 往往会被映射成 CURLOPT_TIMEOUT

  • 你又显式写 CURLOPT_TIMEOUT => 180

等于重复,但不致命;只是让你不好判断到底哪条在起作用。

建议统一只留一套:

->setOptions([
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT => 10,
])

5)给你一段“自证”测试代码(最直观)

$client = new \yii\httpclient\Client([
    'transport' => 'yii\httpclient\CurlTransport',
]);

Yii::info('transport=' . get_class($client->transport));

$start = microtime(true);

try {
    $res = $client->createRequest()
        ->setMethod('GET')
        ->setUrl('http://10.255.255.1/test')
        ->setOptions([
            CURLOPT_CONNECTTIMEOUT => 3,
            CURLOPT_TIMEOUT => 6,
        ])
        ->send();

} catch (\Throwable $e) {
    $cost = microtime(true) - $start;
    echo "cost={$cost}\n";
    echo $e->getMessage() . "\n";
}

你会看到 cost 接近 3 秒(连接超时)或 6 秒(总超时),非常好判断。

结论(对应你说的现象)

  • timeout 不能删、而且生效:你很可能在用 StreamTransport,或 timeout 被 Yii 映射成了总超时。

  • CURLOPT_CONNECTTIMEOUT 不起作用:大概率 没走 cURL,或者连接阶段被复用/被别的配置覆盖。

你把 get_class($client->transport) 的结果贴我一下(一行就行),我就能直接告诉你:到底是哪种情况,以及你现在这套 options 哪些会生效、哪些一定不会。


问:

我确实用的StreamTransport,那么我该怎么配置?

答:

你用的是 StreamTransport(底层走 fopen/stream_context),那规则就很清晰了:

  • ✅ 生效的核心只有 timeout(以及少量 stream 上下文参数)

  • ❌ 任何 CURLOPT_* 都不会生效(因为根本没用 cURL)

下面给你一套可直接用的配置方式

1)StreamTransport 下的超时怎么配

✅ 总超时(最关键)

StreamTransport 主要认这个:

->setOptions([
    'timeout' => 300, // 秒,总超时(连接+读写)
])

你之前写的 'timeout' => 180 在 Stream 下确实会起作用。

现实效果:连接阶段、读取阶段都会受它约束,但不像 cURL 那样严格区分 connect/read。

2)怎么配置 HTTP 上下文(header、content、proxy 等)

在 StreamTransport,setOptions() 里的选项会转换成 stream context。
最稳的写法是把 stream context 明确写进去:

$res = $client->createRequest()
    ->setUrl($url)
    ->setMethod('POST')
    ->setData($data)
    ->setFormat(\yii\httpclient\Client::FORMAT_JSON)
    ->setOptions([
        'timeout' => 300,
        'http' => [
            'header' => "Content-Type: application/json\r\nUser-Agent: ZPY2.0\r\n",
            // 'ignore_errors' => true, // 想拿到 4xx/5xx 的 body 时可以开
        ],
        // 'ssl' => [
        //     'verify_peer' => false,
        //     'verify_peer_name' => false,
        // ],
    ])
    ->send();

注意:你现在 .addHeaders() 仍然可以用,但在 StreamTransport 下,有时你把 header 写进 http.header 更可控(尤其是某些代理/网关环境)。

3)StreamTransport 能不能分开 “连接超时”?

基本做不到像 cURL 那样精准分离 CONNECTTIMEOUTTIMEOUT

Stream 的 timeout 更像一个统一的“流操作超时”

如果你特别需要“连接 3 秒、读 30 秒”这种精细控制:

  • ✅ 最简单:切到 CurlTransport

  • ❗坚持 Stream:只能用整体 timeout + 业务层重试/降级来模拟

4)SSL 验证怎么配(StreamTransport 专用)

你之前写的:

'sslVerifyPeer' => false,
'sslCafile' => '',
'sslCapath' => '',

那是 Guzzle 的写法,在 Yii2 httpclient 里不对。

StreamTransport 正确是:

->setOptions([
    'timeout' => 300,
    'ssl' => [
        'verify_peer' => false,
        'verify_peer_name' => false,
        // 'cafile' => '/path/to/cacert.pem',
        // 'capath' => '/path/to/ca-dir',
    ],
])

生产环境不建议关验证;如果是内网自签证书,建议配 cafile。

5)代理怎么配(StreamTransport)

->setOptions([
    'timeout' => 300,
    'http' => [
        'proxy' => 'tcp://127.0.0.1:7890',
        'request_fulluri' => true,
    ],
])

6)一个“生产可用模板”(StreamTransport)

$client = new \yii\httpclient\Client([
    'transport' => 'yii\httpclient\StreamTransport',
]);

$res = $client->createRequest()
    ->setUrl($url)
    ->setMethod('POST')
    ->setFormat(\yii\httpclient\Client::FORMAT_JSON)
    ->setData($data)
    ->setOptions([
        'timeout' => 300, // 5分钟
        'http' => [
            'header' => implode("\r\n", [
                'Content-Type: application/json',
                'User-Agent: ZPY2.0',
            ]) . "\r\n",
            'ignore_errors' => true, // 可选:拿到 4xx/5xx 内容
        ],
        'ssl' => [
            'verify_peer' => false,       // 可选
            'verify_peer_name' => false,  // 可选
        ],
    ])
    ->send();

if (!$res->isOk) {
    Yii::error([
        'status' => $res->statusCode,
        'body' => $res->content,
    ], 'report-http');
}

7)你现在这段 options 应该怎么改

你原来的:

  • 'timeout' => 180 ✅保留

  • CURLOPT_CONNECTTIMEOUT / CURLOPT_TIMEOUT ❌删掉(无效)

  • sslVerifyPeer / sslCafile / sslCapath ❌换成 ssl 上下文写法

8)最后一个关键建议(很实用)

如果你调用的是你们自己的“报告服务器”,并且你真的需要清晰区分:

  • 连接超时(比如 3 秒)

  • 读取超时(比如 300 秒)

那我建议你强制开启 curl 扩展 + CurlTransport,这是最干净、可控的方案。