PHP异步SOAP服务器如何先发送自定义ACK再返回响应?
PHP实现SOAP服务先返回自定义确认消息的方案
首先得明确:标准的SOAP/HTTP是单请求单响应的无状态模型,PHP内置的SOAP扩展和Zend SOAP都是基于这个模型设计的,所以直接用它们确实没法实现“先返回确认消息再返回最终响应”的需求——这也是你找不到现成示例的原因。不过我们可以通过两种思路绕开这个限制,下面是具体的实现方案:
方案一:异步处理+主动发送确认与最终响应(推荐)
这是最可靠、兼容性最好的方案,完全符合HTTP和SOAP的设计逻辑。核心思路是把整个流程拆成两个独立的SOAP交互:
- 收到客户端的SubmitInv/RequestInv请求后,立即同步返回自定义的确认消息
- 将请求放入异步任务队列,后台处理业务逻辑
- 处理完成后,主动调用客户端的SOAP接口返回最终的业务响应
具体实现步骤
- 禁用输出缓冲并手动发送确认响应
要绕过标准SOAP库的响应处理,我们需要直接控制HTTP输出:
// 禁用所有输出缓冲 ob_end_clean(); ignore_user_abort(true); // 确保后台处理不受客户端断开影响 set_time_limit(0); // 取消脚本执行时间限制 // 从原始SOAP请求中解析出RqUID和PKey(示例用DOM解析,你可以根据实际请求调整) $requestXml = file_get_contents('php://input'); $dom = new DOMDocument(); $dom->loadXML($requestXml); $rqUid = $dom->getElementsByTagName('RqUID')->item(0)->nodeValue; $pKey = $dom->getElementsByTagName('PKey')->item(0)->nodeValue; // 构建确认消息的SOAP XML $ackXml = <<<XML <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <PAddRs> <RqUID>$rqUid</RqUID> <PKey>$pKey</PKey> <AppStatus> <AppStatusCode>Accept</AppStatusCode> </AppStatus> </PAddRs> </soap:Body> </soap:Envelope> XML; // 设置HTTP响应头并发送确认消息 header('Content-Type: text/xml; charset=utf-8'); header('Content-Length: ' . strlen($ackXml)); echo $ackXml; flush(); // 强制输出到客户端 // PHP-FPM环境下,立即结束HTTP连接,后续代码后台执行 if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); }
- 异步处理请求
把请求数据存入任务队列(比如Redis、RabbitMQ,或者简单的数据库队列),然后用后台进程(比如Crontab定时脚本、Supervisor守护进程)处理队列中的任务:
// 将请求数据存入Redis队列 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $requestData = [ 'raw_request' => $requestXml, 'rq_uid' => $rqUid, 'client_endpoint' => 'https://partner.example.com/soap-response-endpoint' // 客户端接收最终响应的地址 ]; $redis->lPush('soap_task_queue', json_encode($requestData));
- 处理完成后主动发送最终响应
后台进程处理完任务后,调用客户端的SOAP接口返回最终结果:
// 后台处理脚本示例 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); while ($task = $redis->rPop('soap_task_queue')) { $taskData = json_decode($task, true); // 这里替换成你的业务逻辑处理(比如生成发票、查询数据等) $finalResponseXml = $this->generateFinalSoapResponse($taskData['raw_request']); // 调用客户端SOAP接口发送最终响应 $ch = curl_init($taskData['client_endpoint']); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $finalResponseXml); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: text/xml; charset=utf-8']); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); // 处理调用结果(比如记录日志、重试失败请求) if (curl_errno($ch)) { // 可以将失败任务重新放回队列重试 $redis->lPush('soap_task_queue', $task); } curl_close($ch); }
方案二:HTTP分块传输编码(兼容性有限)
如果必须在同一个HTTP请求中返回确认消息和最终响应,可以利用HTTP的分块传输编码(Chunked Transfer Encoding),把两个SOAP消息作为不同的块发送。但注意:大部分标准SOAP客户端不支持这种方式,需要确认对方客户端能正确解析分块中的多个SOAP消息。
具体实现代码
// 禁用输出缓冲 ob_end_clean(); // 设置分块传输的HTTP头 header('Transfer-Encoding: chunked'); header('Content-Type: text/xml; charset=utf-8'); // 输出确认消息的块 $ackXml = <<<XML <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <PAddRs> <RqUID>f11958c8-3fde-42ca-bd94-94fdfca316ef</RqUID> <PKey>46dba062-2105-4851-831f-a1d364741329</PKey> <AppStatus> <AppStatusCode>Accept</AppStatusCode> </AppStatus> </PAddRs> </soap:Body> </soap:Envelope> XML; printf("%x\r\n%s\r\n", strlen($ackXml), $ackXml); flush(); // 强制发送第一个块 // 模拟业务处理耗时(替换成你的实际业务逻辑) sleep(5); // 输出最终响应的块 $finalResponseXml = $this->generateFinalSoapResponse(); // 生成你的最终SOAP响应 printf("%x\r\n%s\r\n", strlen($finalResponseXml), $finalResponseXml); flush(); // 发送空块结束响应 echo "0\r\n\r\n"; flush();
关键注意事项
- 优先选择方案一:方案一完全符合HTTP/SOAP的设计,兼容性和可靠性都远高于方案二,也避免了客户端解析分块响应的问题。
- 异步任务的可靠性:使用队列时要考虑失败重试机制,比如设置任务重试次数、死信队列,避免任务丢失。
- SOAP XML的正确性:手动构建SOAP XML时要确保命名空间、标签结构完全符合需求,最好用DOMDocument或XMLWriter生成,避免字符串拼接的错误。
- 连接管理:在方案一中,使用
fastcgi_finish_request()(PHP-FPM环境)确保后台处理不受客户端断开连接的影响。
内容的提问来源于stack exchange,提问作者MB34
相关产品推荐
相关产品推荐

