curl_multi_remove_handle() 函数用于从cURL多句柄中移除一个单独的cURL句柄。移除后,该句柄可以单独关闭或重新用于其他请求。这是cURL多句柄资源管理的关键步骤,防止资源泄漏。
int curl_multi_remove_handle ( resource $mh , resource $ch )
| 参数 | 描述 | 类型 | 必需 |
|---|---|---|---|
| mh | 由 curl_multi_init() 返回的cURL多句柄 |
resource | 是 |
| ch | 要移除的cURL句柄,必须已添加到该多句柄中 | resource | 是 |
成功时返回 0 (CURLM_OK),失败时返回错误代码。
@php
// 创建多句柄
$mh = curl_multi_init();
// 创建并添加多个cURL句柄
$handles = [];
$urls = [
'https://httpbin.org/get',
'https://httpbin.org/post',
'https://httpbin.org/put'
];
foreach ($urls as $i => $url) {
$handles[$i] = curl_init($url);
curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
// 添加到多句柄
curl_multi_add_handle($mh, $handles[$i]);
echo "已添加句柄 {$i} 到多句柄\n";
}
echo "\n当前多句柄中的句柄数量: " . count($handles) . "\n\n";
// 执行并行请求
$active = null;
do {
curl_multi_exec($mh, $active);
curl_multi_select($mh, 0.1);
} while ($active > 0);
// 处理完成请求并移除句柄
foreach ($handles as $i => $handle) {
// 获取内容
$content = curl_multi_getcontent($handle);
$info = curl_getinfo($handle);
echo "请求 {$i} 完成: HTTP {$info['http_code']}, 耗时: " . round($info['total_time'], 3) . "秒\n";
// 关键步骤:从多句柄中移除句柄
$removeResult = curl_multi_remove_handle($mh, $handle);
if ($removeResult === CURLM_OK) {
echo "✓ 成功从多句柄移除句柄 {$i}\n";
} else {
echo "✗ 移除句柄 {$i} 失败,错误代码: {$removeResult}\n";
}
// 关闭单个句柄
curl_close($handle);
echo "✓ 已关闭句柄 {$i}\n\n";
}
echo "所有句柄已移除并关闭\n";
// 关闭多句柄
curl_multi_close($mh);
echo "多句柄已关闭\n";
echo "\n资源清理完成!\n";
@endphp
@php
class SafeMultiHandler {
private $multiHandle;
private $activeHandles = [];
private $completedHandles = [];
public function __construct() {
$this->multiHandle = curl_multi_init();
if ($this->multiHandle === false) {
throw new Exception('无法初始化cURL多句柄');
}
}
public function addRequest($url, $id = null) {
if ($id === null) {
$id = uniqid('req_');
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30
]);
$this->activeHandles[$id] = $ch;
curl_multi_add_handle($this->multiHandle, $ch);
return $id;
}
public function execute() {
echo "开始执行并行请求...\n";
echo "初始句柄数量: " . count($this->activeHandles) . "\n\n";
$active = null;
do {
// 执行多句柄
$status = curl_multi_exec($this->multiHandle, $active);
// 实时处理完成请求
$this->processCompletedRequests();
// 显示进度
$completed = count($this->completedHandles);
$remaining = count($this->activeHandles);
if ($remaining > 0) {
echo "进度: {$completed} 完成, {$remaining} 进行中\n";
}
// 等待活动
if ($active > 0) {
curl_multi_select($this->multiHandle, 0.1);
}
} while ($active > 0 || !empty($this->activeHandles));
echo "\n所有请求执行完成!\n";
return $this->completedHandles;
}
private function processCompletedRequests() {
$msgsInQueue = null;
while ($info = curl_multi_info_read($this->multiHandle, $msgsInQueue)) {
$handle = $info['handle'];
$id = array_search($handle, $this->activeHandles, true);
if ($id !== false) {
$this->safelyRemoveAndProcess($id, $handle, $info);
} else {
echo "警告: 找不到对应的句柄ID\n";
}
}
}
private function safelyRemoveAndProcess($id, $handle, $info) {
try {
// 获取响应内容
$content = curl_multi_getcontent($handle);
$curlInfo = curl_getinfo($handle);
$error = curl_error($handle);
// 关键步骤:安全地从多句柄移除句柄
$removeResult = curl_multi_remove_handle($this->multiHandle, $handle);
if ($removeResult !== CURLM_OK) {
throw new Exception("移除句柄失败,错误代码: {$removeResult}");
}
echo "✓ 安全移除句柄: {$id}\n";
// 存储结果
$this->completedHandles[$id] = [
'success' => ($info['result'] === CURLE_OK),
'content' => $content,
'info' => $curlInfo,
'error' => $error,
'content_length' => strlen($content)
];
// 关闭单个句柄
curl_close($handle);
echo "✓ 已关闭句柄: {$id}\n";
// 从活动句柄列表中移除
unset($this->activeHandles[$id]);
} catch (Exception $e) {
echo "✗ 处理句柄 {$id} 时出错: " . $e->getMessage() . "\n";
// 尝试强制清理
$this->forceCleanupHandle($id, $handle);
}
}
private function forceCleanupHandle($id, $handle) {
// 尝试从多句柄移除(忽略错误)
@curl_multi_remove_handle($this->multiHandle, $handle);
// 尝试关闭句柄
if (is_resource($handle)) {
@curl_close($handle);
}
// 从活动列表中移除
unset($this->activeHandles[$id]);
echo "✓ 已强制清理句柄: {$id}\n";
}
public function getRemainingHandles() {
return $this->activeHandles;
}
public function close() {
echo "\n开始清理剩余资源...\n";
// 清理所有剩余的活动句柄
foreach ($this->activeHandles as $id => $handle) {
echo "清理剩余句柄: {$id}\n";
$this->forceCleanupHandle($id, $handle);
}
// 关闭多句柄
if (is_resource($this->multiHandle)) {
curl_multi_close($this->multiHandle);
echo "多句柄已关闭\n";
}
$this->activeHandles = [];
$this->completedHandles = [];
echo "资源清理完成!\n";
}
public function __destruct() {
$this->close();
}
}
// 使用安全的处理器
try {
$handler = new SafeMultiHandler();
// 添加多个请求
$requests = [
'api1' => 'https://httpbin.org/delay/1',
'api2' => 'https://httpbin.org/delay/2',
'api3' => 'https://httpbin.org/json',
'api4' => 'https://httpbin.org/xml'
];
foreach ($requests as $id => $url) {
$handler->addRequest($url, $id);
}
// 执行请求
$results = $handler->execute();
// 显示统计信息
$successCount = count(array_filter($results, function($r) { return $r['success']; }));
$totalCount = count($results);
echo "\n执行结果统计:\n";
echo "总请求数: {$totalCount}\n";
echo "成功: {$successCount}\n";
echo "失败: " . ($totalCount - $successCount) . "\n";
echo "成功率: " . round(($successCount / $totalCount) * 100, 1) . "%\n";
// 显式关闭(析构函数也会调用)
$handler->close();
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "\n";
}
@endphp
@php
class CurlHandlePool {
private $multiHandle;
private $handlePool = [];
private $activeHandles = [];
private $maxPoolSize;
public function __construct($maxPoolSize = 10) {
$this->multiHandle = curl_multi_init();
$this->maxPoolSize = $maxPoolSize;
}
/**
* 从池中获取或创建句柄
*/
public function getHandle($url, $options = []) {
$id = uniqid('req_');
// 尝试从池中获取空闲句柄
$ch = $this->getHandleFromPool();
if ($ch === null) {
// 池中没有可用句柄,创建新句柄
$ch = curl_init();
echo "创建新句柄: {$id}\n";
} else {
echo "重用池中句柄: {$id}\n";
}
// 重置并配置句柄
curl_reset($ch);
$defaultOptions = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30
];
$finalOptions = $options + $defaultOptions;
curl_setopt_array($ch, $finalOptions);
// 添加到多句柄
curl_multi_add_handle($this->multiHandle, $ch);
$this->activeHandles[$id] = $ch;
return $id;
}
private function getHandleFromPool() {
if (empty($this->handlePool)) {
return null;
}
// 从池中取出一个句柄
return array_shift($this->handlePool);
}
private function returnHandleToPool($ch) {
// 检查池大小限制
if (count($this->handlePool) < $this->maxPoolSize) {
$this->handlePool[] = $ch;
echo "句柄已返回池中,当前池大小: " . count($this->handlePool) . "\n";
} else {
// 池已满,关闭句柄
curl_close($ch);
echo "池已满,关闭句柄\n";
}
}
public function executeBatch() {
echo "开始执行批处理请求...\n";
echo "活动句柄数量: " . count($this->activeHandles) . "\n\n";
$active = null;
$results = [];
do {
// 执行多句柄
$status = curl_multi_exec($this->multiHandle, $active);
// 处理完成请求
$this->processCompletedRequests($results);
if ($active > 0) {
curl_multi_select($this->multiHandle, 0.1);
}
} while ($active > 0 || !empty($this->activeHandles));
echo "\n批处理完成!\n";
return $results;
}
private function processCompletedRequests(&$results) {
$msgsInQueue = null;
while ($info = curl_multi_info_read($this->multiHandle, $msgsInQueue)) {
$handle = $info['handle'];
$id = array_search($handle, $this->activeHandles, true);
if ($id !== false) {
$this->processSingleCompletion($id, $handle, $info, $results);
}
}
}
private function processSingleCompletion($id, $handle, $info, &$results) {
// 获取响应内容
$content = curl_multi_getcontent($handle);
$curlInfo = curl_getinfo($handle);
// 关键:从多句柄中移除句柄
$removeResult = curl_multi_remove_handle($this->multiHandle, $handle);
if ($removeResult === CURLM_OK) {
echo "✓ 从多句柄移除句柄: {$id}\n";
} else {
echo "✗ 移除句柄失败: {$id}, 错误代码: {$removeResult}\n";
}
// 存储结果
$results[$id] = [
'success' => ($info['result'] === CURLE_OK),
'content' => $content,
'info' => $curlInfo,
'content_length' => strlen($content)
];
// 从活动句柄中移除
unset($this->activeHandles[$id]);
// 将句柄返回池中或关闭
$this->returnHandleToPool($handle);
}
public function getPoolStats() {
return [
'pool_size' => count($this->handlePool),
'active_handles' => count($this->activeHandles),
'max_pool_size' => $this->maxPoolSize
];
}
public function close() {
echo "\n开始关闭资源池...\n";
// 清理活动句柄
foreach ($this->activeHandles as $id => $handle) {
echo "清理活动句柄: {$id}\n";
curl_multi_remove_handle($this->multiHandle, $handle);
curl_close($handle);
}
$this->activeHandles = [];
// 清理池中句柄
foreach ($this->handlePool as $handle) {
curl_close($handle);
}
$this->handlePool = [];
// 关闭多句柄
if (is_resource($this->multiHandle)) {
curl_multi_close($this->multiHandle);
}
echo "资源池已关闭\n";
}
public function __destruct() {
$this->close();
}
}
// 使用句柄池
$pool = new CurlHandlePool(5); // 最大池大小5
// 第一轮请求
echo "=== 第一轮请求 ===\n";
$urls1 = [
'req1' => 'https://httpbin.org/delay/1',
'req2' => 'https://httpbin.org/delay/1',
'req3' => 'https://httpbin.org/json'
];
foreach ($urls1 as $id => $url) {
$pool->getHandle($url, [], $id);
}
$results1 = $pool->executeBatch();
$stats1 = $pool->getPoolStats();
echo "第一轮后池统计: " . json_encode($stats1) . "\n";
// 第二轮请求(应该重用句柄)
echo "\n=== 第二轮请求 ===\n";
$urls2 = [
'req4' => 'https://httpbin.org/xml',
'req5' => 'https://httpbin.org/headers',
'req6' => 'https://httpbin.org/uuid'
];
foreach ($urls2 as $id => $url) {
$pool->getHandle($url, [], $id);
}
$results2 = $pool->executeBatch();
$stats2 = $pool->getPoolStats();
echo "第二轮后池统计: " . json_encode($stats2) . "\n";
// 显示总结果
$totalRequests = count($results1) + count($results2);
$successfulRequests = count(array_filter($results1, function($r) { return $r['success']; })) +
count(array_filter($results2, function($r) { return $r['success']; }));
echo "\n=== 总统计 ===\n";
echo "总请求数: {$totalRequests}\n";
echo "成功请求: {$successfulRequests}\n";
echo "重用节省: " . ($totalRequests - $stats2['pool_size']) . " 个句柄创建\n";
$pool->close();
@endphp
@php
class AdvancedResourceManager {
private $multiHandle;
private $handles = [];
private $resourceLog = [];
private $startTime;
public function __construct() {
$this->multiHandle = curl_multi_init();
$this->startTime = microtime(true);
$this->logEvent('multi_handle_created', '多句柄已创建');
}
public function addRequest($url, $id = null) {
if ($id === null) {
$id = uniqid('req_');
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_VERBOSE => false // 设置为true可获取详细日志
]);
$this->handles[$id] = [
'handle' => $ch,
'url' => $url,
'added_time' => microtime(true),
'state' => 'added'
];
$addResult = curl_multi_add_handle($this->multiHandle, $ch);
if ($addResult === CURLM_OK) {
$this->logEvent('handle_added', "句柄 {$id} 已添加到多句柄", $id);
$this->handles[$id]['state'] = 'active';
} else {
$this->logEvent('handle_add_failed', "句柄 {$id} 添加失败,错误代码: {$addResult}", $id);
curl_close($ch);
unset($this->handles[$id]);
return false;
}
return $id;
}
public function execute() {
$this->logEvent('execution_started', '开始执行并行请求');
$active = null;
$cycleCount = 0;
do {
$cycleCount++;
// 执行多句柄
$status = curl_multi_exec($this->multiHandle, $active);
if ($status !== CURLM_OK) {
$this->logEvent('execution_error', "多句柄执行错误,代码: {$status}");
$this->handleExecutionError($status);
}
// 处理完成请求
$processed = $this->processCompletedRequests();
// 资源监控
if ($cycleCount % 10 === 0) {
$this->monitorResources();
}
// 超时检查
if ($this->checkTimeout()) {
$this->logEvent('timeout_reached', '执行超时,强制结束');
break;
}
// 等待活动
if ($active > 0) {
$ready = curl_multi_select($this->multiHandle, 0.1);
if ($ready === -1) {
usleep(100000); // 100ms
}
}
} while ($active > 0 || !empty($this->handles));
$this->logEvent('execution_completed', '所有请求执行完成');
return $this->generateReport();
}
private function processCompletedRequests() {
$processedCount = 0;
$msgsInQueue = null;
while ($info = curl_multi_info_read($this->multiHandle, $msgsInQueue)) {
$handle = $info['handle'];
$id = $this->findHandleId($handle);
if ($id !== null) {
$this->processSingleRequest($id, $handle, $info);
$processedCount++;
} else {
$this->logEvent('orphaned_handle', '发现孤儿句柄(找不到对应ID)');
}
}
return $processedCount;
}
private function findHandleId($handle) {
foreach ($this->handles as $id => $data) {
if ($data['handle'] === $handle) {
return $id;
}
}
return null;
}
private function processSingleRequest($id, $handle, $info) {
$requestData = $this->handles[$id];
try {
// 获取响应内容
$content = curl_multi_getcontent($handle);
$curlInfo = curl_getinfo($handle);
$error = curl_error($handle);
// 关键步骤:从多句柄移除句柄
$removeStart = microtime(true);
$removeResult = curl_multi_remove_handle($this->multiHandle, $handle);
$removeTime = microtime(true) - $removeStart;
if ($removeResult === CURLM_OK) {
$this->logEvent('handle_removed', "句柄 {$id} 已从多句柄移除", $id, [
'remove_time' => $removeTime
]);
// 记录请求结果
$requestData['state'] = 'completed';
$requestData['completed_time'] = microtime(true);
$requestData['duration'] = $requestData['completed_time'] - $requestData['added_time'];
$requestData['result'] = [
'success' => ($info['result'] === CURLE_OK),
'http_code' => $curlInfo['http_code'],
'content_length' => strlen($content),
'error' => $error,
'remove_result' => $removeResult
];
$this->handles[$id] = $requestData;
} else {
$this->logEvent('remove_failed', "句柄 {$id} 移除失败,错误代码: {$removeResult}", $id);
throw new Exception("移除句柄失败,错误代码: {$removeResult}");
}
// 关闭单个句柄
$closeStart = microtime(true);
curl_close($handle);
$closeTime = microtime(true) - $closeStart;
$this->logEvent('handle_closed', "句柄 {$id} 已关闭", $id, [
'close_time' => $closeTime
]);
// 从活动列表中移除
unset($this->handles[$id]);
} catch (Exception $e) {
$this->logEvent('processing_error', "处理句柄 {$id} 时出错: " . $e->getMessage(), $id);
$this->forceCleanup($id, $handle);
}
}
private function forceCleanup($id, $handle) {
$this->logEvent('force_cleanup', "强制清理句柄 {$id}", $id);
// 尝试移除句柄(忽略错误)
@curl_multi_remove_handle($this->multiHandle, $handle);
// 尝试关闭句柄
if (is_resource($handle)) {
@curl_close($handle);
}
// 从列表中移除
unset($this->handles[$id]);
}
private function handleExecutionError($errorCode) {
$errorMessages = [
CURLM_BAD_HANDLE => '传递了无效的多句柄',
CURLM_BAD_EASY_HANDLE => '传递了无效的简单句柄',
CURLM_OUT_OF_MEMORY => '内存不足',
CURLM_INTERNAL_ERROR => '内部错误'
];
$message = $errorMessages[$errorCode] ?? "未知错误代码: {$errorCode}";
$this->logEvent('execution_error_detail', "执行错误: {$message}");
// 根据错误类型决定是否继续执行
if ($errorCode === CURLM_OUT_OF_MEMORY) {
$this->emergencyCleanup();
}
}
private function emergencyCleanup() {
$this->logEvent('emergency_cleanup', '开始紧急清理');
foreach ($this->handles as $id => $data) {
$this->forceCleanup($id, $data['handle']);
}
}
private function monitorResources() {
$memoryUsage = memory_get_usage(true);
$memoryPeak = memory_get_peak_usage(true);
$activeHandles = count($this->handles);
$this->logEvent('resource_monitor', '资源监控', null, [
'memory_usage_mb' => round($memoryUsage / 1024 / 1024, 2),
'memory_peak_mb' => round($memoryPeak / 1024 / 1024, 2),
'active_handles' => $activeHandles,
'execution_time' => round(microtime(true) - $this->startTime, 2)
]);
}
private function checkTimeout() {
$executionTime = microtime(true) - $this->startTime;
return $executionTime > 60; // 60秒超时
}
private function logEvent($type, $message, $handleId = null, $data = []) {
$event = [
'timestamp' => microtime(true),
'type' => $type,
'message' => $message,
'handle_id' => $handleId,
'data' => $data
];
$this->resourceLog[] = $event;
echo "[" . date('H:i:s') . "] {$message}\n";
if (!empty($data)) {
foreach ($data as $key => $value) {
echo " {$key}: {$value}\n";
}
}
}
private function generateReport() {
$report = [
'summary' => $this->generateSummary(),
'resource_log' => $this->resourceLog,
'performance_metrics' => $this->calculateMetrics()
];
return $report;
}
private function generateSummary() {
$completed = array_filter($this->resourceLog, function($event) {
return $event['type'] === 'handle_removed';
});
$failed = array_filter($this->resourceLog, function($event) {
return in_array($event['type'], ['remove_failed', 'processing_error']);
});
return [
'total_requests' => count($completed) + count($failed),
'successfully_processed' => count($completed),
'failed_processing' => count($failed),
'total_execution_time' => round(microtime(true) - $this->startTime, 2)
];
}
private function calculateMetrics() {
$removeTimes = [];
$closeTimes = [];
foreach ($this->resourceLog as $event) {
if ($event['type'] === 'handle_removed') {
$removeTimes[] = $event['data']['remove_time'] ?? 0;
}
if ($event['type'] === 'handle_closed') {
$closeTimes[] = $event['data']['close_time'] ?? 0;
}
}
return [
'avg_remove_time' => !empty($removeTimes) ? round(array_sum($removeTimes) / count($removeTimes), 6) : 0,
'avg_close_time' => !empty($closeTimes) ? round(array_sum($closeTimes) / count($closeTimes), 6) : 0,
'total_remove_time' => round(array_sum($removeTimes), 6),
'total_close_time' => round(array_sum($closeTimes), 6)
];
}
public function close() {
$this->logEvent('cleanup_started', '开始资源清理');
// 清理剩余句柄
foreach ($this->handles as $id => $data) {
$this->logEvent('cleaning_handle', "清理剩余句柄 {$id}", $id);
$this->forceCleanup($id, $data['handle']);
}
// 关闭多句柄
if (is_resource($this->multiHandle)) {
curl_multi_close($this->multiHandle);
$this->logEvent('multi_handle_closed', '多句柄已关闭');
}
$this->logEvent('cleanup_completed', '资源清理完成');
$this->handles = [];
$this->resourceLog = [];
}
public function __destruct() {
$this->close();
}
}
// 使用高级资源管理器
$manager = new AdvancedResourceManager();
// 添加多个请求
$urls = [
'fast' => 'https://httpbin.org/delay/1',
'medium' => 'https://httpbin.org/delay/2',
'slow' => 'https://httpbin.org/delay/3',
'api1' => 'https://httpbin.org/json',
'api2' => 'https://httpbin.org/xml'
];
foreach ($urls as $id => $url) {
$manager->addRequest($url, $id);
}
// 执行请求
$report = $manager->execute();
// 显示报告摘要
$summary = $report['summary'];
echo "\n=== 执行报告摘要 ===\n";
echo "总请求数: {$summary['total_requests']}\n";
echo "成功处理: {$summary['successfully_processed']}\n";
echo "处理失败: {$summary['failed_processing']}\n";
echo "总执行时间: {$summary['total_execution_time']} 秒\n";
// 显示性能指标
$metrics = $report['performance_metrics'];
echo "\n=== 性能指标 ===\n";
echo "平均移除时间: " . ($metrics['avg_remove_time'] * 1000) . " 毫秒\n";
echo "平均关闭时间: " . ($metrics['avg_close_time'] * 1000) . " 毫秒\n";
echo "总移除时间: " . ($metrics['total_remove_time'] * 1000) . " 毫秒\n";
echo "总关闭时间: " . ($metrics['total_close_time'] * 1000) . " 毫秒\n";
$manager->close();
@endphp
防止资源泄漏,确保系统资源得到正确释放。
及时释放不再需要的网络连接和缓冲区。
减少多句柄的负担,提高后续请求的执行效率。
| 错误做法 | 问题 | 正确做法 |
|---|---|---|
| 忘记移除句柄 | 导致资源泄漏,多句柄中积累大量无用句柄 | 在处理完每个请求后立即调用curl_multi_remove_handle() |
| 先关闭句柄再移除 | 可能产生警告或错误,句柄状态不一致 | 先移除句柄,再关闭句柄:remove → close |
| 不移除句柄直接关闭多句柄 | PHP可能会自动清理,但这不是好习惯,可能在某些情况下出现问题 | 显式移除所有句柄后再关闭多句柄 |
| 忽略返回值 | 无法知道移除操作是否成功,可能隐藏潜在问题 | 检查curl_multi_remove_handle()的返回值,处理错误 |