一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

PHP函数curl_multi_族实现非阻塞并发抓取:爬虫进阶

时间:2026-07-11 09:17:52 编辑:袖梨 来源:一聚教程网

必须调用curl_multi_add_handle才能发起请求,因curl_multi_init仅创建任务容器,未添加句柄则curl_multi_exec无网络行为;选项须在添加前设置,重复添加或顺序错误将导致配置丢失或未定义行为。

curl_multi_init 之后必须调用 curl_multi_add_handle 才能真正发起请求

很多人以为 curl_multi_init() 创建句柄后请求就自动发出去了,其实它只是初始化一个“任务容器”,所有具体请求都得手动塞进去。漏掉 curl_multi_add_handle()curl_multi_exec() 就永远返回 CURLM_CALL_MULTI_PERFORM 或直接卡住,没有任何实际网络行为。

常见错误场景:循环创建多个 curl_init() 句柄,但只对其中一部分调用 curl_multi_add_handle();或者误把 curl_setopt() 放在 curl_multi_add_handle() 之后——此时设置会丢失,因为选项必须在加入 multi 句柄前完成。

  • 每个独立的 cURL 句柄($ch)必须先配置好 CURLOPT_URLCURLOPT_RETURNTRANSFER 等参数,再传给 curl_multi_add_handle()
  • 添加后不能再调用 curl_setopt($ch, ...) 修改参数;如需动态控制,应在添加前用数组缓存配置,或改用 curl_multi_setopt()(仅限少数全局选项)
  • 重复添加同一个句柄会导致未定义行为,务必确保每个 $ch 只加一次

curl_multi_exec 的 while 循环不是“多线程”,而是轮询驱动状态机

curl_multi_exec() 本身不阻塞,但它返回的状态码(如 CURLM_CALL_MULTI_PERFORM)常被误解为“需要反复调用直到完成”。实际上,这个返回值只表示“还有内部操作要立即执行”,不是“还没跑完”。真正的并发推进靠的是 curl_multi_select() 配合循环等待。

典型翻车点:写成 while (CURLM_CALL_MULTI_PERFORM === curl_multi_exec($mh, $running)) {},这会疯狂空转 CPU,且无法感知 socket 就绪事件,导致超时、连接堆积甚至 PHP 进程卡死。

立即学习“PHP免费学习笔记(深入)”;

  • 正确模式是:先调一次 curl_multi_exec(),然后进入 while ($running) { curl_multi_select($mh); curl_multi_exec($mh, $running); }
  • curl_multi_select() 最多等待 1 秒(PHP 7.2.22+ 默认值),避免长时间挂起;若需更短等待,可传入毫秒级超时参数(如 0.01
  • 如果所有请求都很快完成(比如本地 mock 接口),$running 可能在第一次 exec 后就变为 0,select 根本不会被调用——这是正常现象,不代表逻辑出错

获取结果时 curl_multi_getcontent 返回 null?检查 CURLOPT_RETURNTRANSFER 是否启用

curl_multi_getcontent($ch) 拿不到响应体,大概率是因为对应句柄没设 CURLOPT_RETURNTRANSFER => true。这个选项必须在 curl_init() 后、curl_multi_add_handle() 前设置,否则 getcontent 永远返回 null,而 curl_exec() 也早已失效(因为 multi 模式下不能调 curl_exec)。

另一个隐藏坑:如果设置了 CURLOPT_HEADER => truecurl_multi_getcontent() 会把响应头和响应体一起返回,但很多人没意识到 header 是原始字符串格式(含 rn),直接 json_decode() 会失败。

  • 务必确认每个 $ch 都显式设置了 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true)
  • 不要依赖 curl_setopt_array() 的顺序——数组键名无序,应单独调用或使用索引数组保证执行顺序
  • 调试时可用 curl_getinfo($ch, CURLINFO_HTTP_CODE)curl_error($ch) 辅助判断是否真成功,而非只看 getcontent 是否非空

并发数超过 100 时容易触发系统资源限制,别硬扛

Linux 默认单进程最多打开 1024 个文件描述符,而每个 cURL 连接至少占 1 个 fd(TCP socket + 可能的 DNS socket)。设 200 并发,还没跑就可能报 curl_easy_perform() failed: Couldn't resolve host nameToo many open files ——这不是 PHP 错,是系统拦住了。

更隐蔽的问题是:即使调大 ulimit,高并发下 DNS 解析、TIME_WAIT 连接堆积、远程服务限流都会让成功率断崖下跌。盲目堆数量不如稳住节奏。

  • 上线前用 ulimit -n 查当前限制,用 ulimit -n 65536 临时提升(需 root 权限或 shell 配置)
  • curl_setopt($ch, CURLOPT_TCP_KEEPALIVE, 1) 复用连接,配合 CURLOPT_FORBID_REUSE => false 减少握手开销
  • 实际生产建议控制在 20–50 并发,配合 usleep(10000) 在每次 curl_multi_exec() 后微调节奏,比硬上 200 更稳

multi 句柄本身不解决 DNS 阻塞,如果大量域名不同,记得加 CURLOPT_DNS_CACHE_TIMEOUT 或换用 c-ares 异步 DNS(需编译支持)。这些细节不处理,再多的并发也只是在等超时。

热门栏目