使用curl里面关于CURLOPT配置中的WRITEFUNCTION读取sse流
CURLOPT_WRITEFUNCTION
CURLOPT_WRITEFUNCTION 是 PHP 中的 cURL 库的一个选项,它用于指定一个回调函数,这个回调函数将会接收 cURL 下载的数据。这个回调函数应该有两个参数,第一个是 data,第二个是 mimetype。
这个选项的值应该是一个有两个参数的函数名,这个函数会在 cURL 接收到数据时被调用。
以下是一些使用 CURLOPT_WRITEFUNCTION 的示例:
function write_data($data) {
$file = fopen('output.txt', 'a');
fwrite($file, $data);
fclose($file);
return strlen($data);
}
$ch = curl_init('http://example.com');
curl_setopt($ch, CURLOPT_WRITEFUNCTION, 'write_data');
curl_exec($ch);
curl_close($ch);
在这个例子中,我们创建了一个函数 write_data,这个函数接收数据并将其写入到 output.txt 文件中。然后我们用 curl_setopt 函数将 CURLOPT_WRITEFUNCTION 的值设置为 'write_data'。
$ch = curl_init('http://example.com');
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($handle, $data) {
$file = fopen('output.txt', 'a');
fwrite($file, $data);
fclose($file);
return strlen($data);
});
curl_exec($ch);
curl_close($ch);
在这个例子中,我们使用了一个匿名函数作为 CURLOPT_WRITEFUNCTION 的值。这个函数的功能和上一个例子中的 write_data 函数一样,也是接收数据并写入到 output.txt 文件中。
注意:在这两个例子中,我们都在 curl_exec 函数执行期间将数据写入到 output.txt 文件中。如果你需要在多线程环境中使用这个功能,你可能需要使用其他方法来保证数据的同步写入,因为 curl_multi 可能会在多个线程中调用你的回调函数,这可能会导致数据竞争和数据损坏。
CURLOPT_WRITEDATA
在 PHP 的 cURL 扩展中,CURLOPT_WRITEDATA 是一个选项,它用于指定一个资源(比如文件句柄或者字符串变量)来接收 cURL 下载的数据。当你使用 CURLOPT_WRITEFUNCTION 选项来自定义数据写入的方式时,这个资源通常会作为该函数的第一个参数传递。
如果你没有设置 CURLOPT_WRITEFUNCTION,那么 CURLOPT_WRITEDATA 将默认为 STDOUT,这意味着数据会被直接输出到标准输出。
以下是一些使用 CURLOPT_WRITEDATA 的示例:
示例 1:将数据写入文件
$fileHandle = fopen('output.txt', 'w');
$ch = curl_init('http://example.com');
curl_setopt($ch, CURLOPT_WRITEDATA, $fileHandle);
curl_exec($ch);
fclose($fileHandle);
curl_close($ch);
在这个例子中,我们打开一个文件句柄 $fileHandle,并将其设置为 CURLOPT_WRITEDATA 的值。cURL 将自动把下载的数据写入到这个文件中。
示例 2:将数据写入字符串变量
$responseData = '';
$ch = curl_init('http://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 这一行不是必需的,但通常用于确保数据不被直接输出
curl_setopt($ch, CURLOPT_WRITEDATA, fopen('php://temp', 'r+'));
$response = curl_exec($ch);
// 获取临时流中的数据
rewind($responseData);
$responseData = stream_get_contents($responseData);
curl_close($ch);
echo $responseData; // 输出下载的数据
在这个例子中,我们使用 php://temp 来创建一个临时流,并将其设置为 CURLOPT_WRITEDATA 的值。当 cURL 下载完数据后,我们可以从临时流中读取数据到字符串变量 $responseData 中。
请注意,CURLOPT_RETURNTRANSFER 和 CURLOPT_WRITEDATA 通常是互斥的。如果你设置了 CURLOPT_RETURNTRANSFER 为 true,cURL 会将响应内容返回给 curl_exec() 函数,而不是写入到 CURLOPT_WRITEDATA 指定的资源中。在大多数情况下,你只需要使用其中一个选项。如果你使用 CURLOPT_WRITEFUNCTION 自定义了数据写入,CURLOPT_RETURNTRANSFER 将被忽略。
最后,记得在完成后关闭所有的文件句柄和 cURL 句柄,以释放资源。
开发中遇到的问题
CURL 的 WRITEFUNCTION 回调需要返回准确处理的数据长度
现在有这么个情况
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) {
if (strpos($chunk, '{"code"') !== false) {
$chunk = $this->getStreamMsg('抱歉,服务器繁忙,请稍后重试。');
}
echo $chunk;
ob_flush(); // 刷新输出缓冲区
flush(); // 发送缓冲区内容到浏览器
return strlen($chunk);
}
// 执行cURL请求
curl_exec($ch);
if(curl_error($ch)) {
$errMsg = 'Error: ' . curl_error($ch);
$this->throwError('这里出现报错了' . $errMsg);
}
// 关闭cURL资源
curl_close($ch);
以下是当第三方返回错误的时候,$chunk打印的内容:
{"code":"InvalidApiKey","message":"Invalid API-key provided.","request_id":"0d381c05-aefb-95eb-9f5f-a749b75a120a"}
这段代码 本地是检测 返回的字符串中是否含有{”code" 字符串,如果有的话代表这是个第三方传输报错。这里执行之后,会发现,出现了如下错误:
(getStreamMsg就只是一个简单构建自定义的sse流字符串的函数)发现不仅输出了,自定义的错误,同时也执行了curl_error。
经过排查,发现问题出在错误处理的流程上。
当使用 getStreamMsg() 替换 $chunk 后,实际上改变了返回给 curl 的数据长度。在 WRITEFUNCTION 回调中,返回值 strlen($chunk) 必须准确反映处理的数据长度,否则会导致 curl 认为数据传输出错。
因为我使用了$chunk =$this->getStreamMsg(),对$chunk重新赋值,因此改变了$chunk的原有长度!导致报错,从而执行了curl_error流程!!
做如下修改:
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) {
// 简单检查是否包含错误码
if (strpos($chunk, '{"code"') !== false) {
$errorMsg = $this->originalStream('抱歉,服务器繁忙,请稍后重试。');
echo $errorMsg;
} else {
echo $chunk;
}
ob_flush(); // 刷新输出缓冲区
flush(); // 发送缓冲区内容到浏览器
return strlen($chunk);
});
// 执行cURL请求
curl_exec($ch);
if(curl_error($ch)) {
$errMsg = 'Error: ' . curl_error($ch);
$this->throwError($errMsg);
// echo $errMsg;
}
主要改动:
1. 保持原始 $chunk 不变
2. 只在输出时根据条件选择输出错误信息还是原始内容
3. 返回值始终使用原始 $chunk 的长度
如此修改后就不会影响数据的传输了。
CURL 的 WRITEFUNCTION 回调需要返回准确处理的数据长度,这是用来确认数据是否完整传输的重要依据。当我们改变了 $chunk 的内容但返回的仍是原始长度时,就会导致长度不匹配,从而触发 curl_error。
针对这个问题,再让我们改一下:
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) {
// 简单检查是否包含错误码
if (strpos($chunk, '{"code"') !== false) {
$errorMsg = $this->originalStream('抱歉,服务器繁忙,请稍后重试。');
echo $errorMsg;
ob_flush();
flush();
return 0;
} else {
ob_flush();
flush();
echo $chunk;
return strlen($chunk);
}
});
// 执行cURL请求
curl_exec($ch);
if(curl_error($ch)) {
$errMsg = 'Error: ' . curl_error($ch);
$this->throwError($errMsg);
// echo $errMsg;
}
这次我们在检测到传输回来的数据,出现问题的时候,直接return 0;
在 CURL 的 WRITEFUNCTION 回调中:
1. return 0 表示写入操作失败,会导致 CURL 传输中断,并且会触发 curl_error()
2. return strlen($chunk) 表示写入成功,继续处理后续数据
所以在本次修改代码中:
- 当检测到 {"code"} 时返回 0,会立即中断传输并触发 curl_error()
- 这种情况下 curl_error($ch) 会返回类似 "Failed writing body" 的错误
所以,想要让返回成功,并且让请求不中断,一定要给了什么数据,就要return回相应的长度!
WRITEFUNCTION 回调中遇到错误,会断开链接
另外,如果WRITEFUNCTION 回调中执行的函数抛错了,也同样会中断sse流的接收!
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) {
// 比如这里解析错误了 那么会直接中断链接!走curl_error流程!
$json = json_decode($chunk, true);
echo $json['some_attr'];
return strlen($chunk);
});
// 执行cURL请求
curl_exec($ch);
if(curl_error($ch)) {
$errMsg = 'Error: ' . curl_error($ch);
$this->throwError($errMsg);
}
另外,如果某些sb服务器设置禁止了程序抛错,那么使用try-catch也无法捕获回调中的错误!出现的效果就是请求直接中断,并且走curl_error流程!如果在禁止抛错的服务器上,发现SSE输出,最后结果不完整,不全的,那么大概率就是回调中执行的代码遇到错误了,导致链接的中断!!