小程序长连接被中断时导致php进程被强制退出
前言
微信小程序请求长连接时,请求还在接受数据的时候关闭小程序,php后端会出现很奇怪的数据不全问题。
问题
通常,我们请求后端时,某个客户端关闭后php进程开启后不会退出(我们习惯上的认知是这样的)。
但当我们假设服务端有一个接口stream.php
写作:
<?php
set_time_limit(0);
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', false);
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
if (ob_get_level() > 0) {
@ob_end_flush();
}
foreach (array_fill(1, 20, 1) as $item) {
echo 'data: {"id":"6wpvHt69PzgJr83YYa8GwZzoxl3Lp","object":"chat.completion.chunk","created":1679480635,"model":"3.5-turbo-0301","choices":[{"delta":{"content":"础"},"index":0,"finish_reason":null}]}' . PHP_EOL . PHP_EOL;
flush();
sleep(1);
}
echo "[DONE]" . PHP_EOL . PHP_EOL;
此时我们再实现一个接口answer.php
去请求上述接口:
<?php
set_time_limit(0);
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', false);
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
if (ob_get_level() > 0) {
@ob_end_flush();
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://a.com/stream.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'aaa=1');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json',]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) {
$len = strlen($data);
echo $data;
return $len;
});
$response = curl_exec($ch);
curl_close($ch);
file_put_contents('finish.txt', 'finish');
然后写一个小程序请求案例:
<template>
<view class="content">
<view class="text-area">
<view @click="handleTest">提交</view>
</view>
<view>{{result}}</view>
</view>
</template>
<script setup>
import {
ref
} from "vue";
let result = ref([]);
let handleTest = () => {
result.value = [];
let url = 'https://a.com/answer.php';
let requestTask = wx.request({
url: url,
enableChunked: true,
responseType: 'text',
method: 'POST',
data: {
'id': 2,
},
});
requestTask.onChunkReceived((data) => {
let res = String.fromCharCode(...(new Int8Array(data.data)));
result.value.push(res);
console.log(res);
});
}
</script>
<style>
</style>
运行小程序,点击“提交”,等有数据显示时,马上退出小程序。
正常来说,即时小程序关闭了,20秒后服务器依然会生成一个finish.txt
。
但是持续操作几遍后都没有发现finish.txt
文件
过程
如果没生成finish.txt
文件,那一定是程序运行到写文件
前就退出了:
如果是要退出文件要么是
die
函数或者抛出异常
,代码里我没有写die
这类函数,那么就有可能是抛出异常了,改动代码:<?php try { set_time_limit(0); ini_set('output_buffering', 'off'); ini_set('zlib.output_compression', false); header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); if (ob_get_level() > 0) { @ob_end_flush(); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://a.com/stream.php'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); curl_setopt($ch, CURLOPT_POSTFIELDS, 'aaa=1'); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json',]); curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) { $len = strlen($data); echo $data; return $len; }); $response = curl_exec($ch); curl_close($ch); } catch (Throwable $e) { var_dump($e->getMessage()); } file_put_contents('finish.txt', 'finish');
尝试再操作一遍,发现依然没有文件产生。
还是围绕进程退出分析,如果不是代码问题,那就只有是有某个服务将这个进程kill掉了,我选择把锅甩给nginx。按照猜想,退出进程那就一定会有一个信号传进来,所以使用
pcntl_signal
捕获这个信号,顺便使用register_shutdown_function
来辅助分析,继续改写代码:<?php pcntl_async_signals(true); $finish = false; register_shutdown_function(function () use (&$finish) { file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . 'shutdown.txt', 'shutdown'); if (!$finish) file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . 'interupt.txt', 'interupt'); }); foreach (array( SIGTERM, SIGHUP, SIGUSR1, SIGUSR2, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGIOT, SIGBUS, SIGFPE, // SIGKILL, SIGSEGV, SIGPIPE, SIGALRM, SIGCHLD, SIGCONT, // SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGWINCH, SIGIO, SIGPWR, ) as $signal) { pcntl_signal($signal, function ($signal) { file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . 'signal.txt', $signal); }); } set_time_limit(0); ini_set('output_buffering', 'off'); ini_set('zlib.output_compression', false); header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); if (ob_get_level() > 0) { @ob_end_flush(); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://a.com/stream.php'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); curl_setopt($ch, CURLOPT_POSTFIELDS, 'aaa=1'); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json',]); curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) { $len = strlen($data); echo $data; return $len; }); $response = curl_exec($ch); curl_close($ch); $finish = true; file_put_contents('finish.txt', 'finish');
操作一遍小程序,这次服务器产生了2个文件
shutdown.txt
、interupt.txt
,没有signal.txt
文件,但说明程序确实是被意外终止
了,且代码没有执行完。进一步分析程序退出时产生了什么signal,如果小程序能够输出内容,那么代码一定是执行到了
CURLOPT_WRITEFUNCTION
回调,那么就有可能是这个回调机制导致程序信号被拦截(猜想),现在我直接去掉curl,改动代码:<?php pcntl_async_signals(true); $finish = false; register_shutdown_function(function () use (&$finish) { file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . 'shutdown.txt', 'shutdown'); if (!$finish) file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . 'interupt.txt', 'interupt'); }); foreach (array( SIGTERM, SIGHUP, SIGUSR1, SIGUSR2, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGIOT, SIGBUS, SIGFPE, // SIGKILL, SIGSEGV, SIGPIPE, SIGALRM, SIGCHLD, SIGCONT, // SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGWINCH, SIGIO, SIGPWR, ) as $signal) { pcntl_signal($signal, function ($signal) { file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . 'signal.txt', $signal); }); } set_time_limit(0); ini_set('output_buffering', 'off'); ini_set('zlib.output_compression', false); header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); if (ob_get_level() > 0) { @ob_end_flush(); } while ($i < 20) { echo 'data: {"name": "mike", "i": "' . $i . '"}' . PHP_EOL . PHP_EOL; $i++; flush(); sleep(1); } $finish = true; file_put_contents('finish.txt', 'finish');
继续操作小程序,这时候服务器产生了3个文件
shutdown.txt
、interupt.txt
、signal.txt
,其中signal.txt
的内容是13
,对照信号表得知是SIGPIPE
信号。我尝试调试为什么
CURLOPT_WRITEFUNCTION
回调为什么会导致pcntl_signal
收不到进程信号,但是没有找到原因,下次再看看。
关于SIGPIPE信号
当服务器close一个连接时,若client端接着发数据。根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。
解决
通过SIGPIPE
得知,客户端是在某种情况下关闭了连接,导致进程退出了。如果想在程序退出的时候,无论什么原因退出的都要保存一些数据,可以直接套用下方例子代码:
<?php
pcntl_async_signals(true);
$finish = false;
register_shutdown_function(function () use (&$finish) {
// 进程退出时要做些什么
if (!$finish) {
// 如果意外退出时要做些什么
}
});
set_time_limit(0);
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', false);
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
if (ob_get_level() > 0) {
@ob_end_flush();
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://a.com/stream.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'aaa=1');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json',]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) {
$len = strlen($data);
echo $data;
return $len;
});
$response = curl_exec($ch);
curl_close($ch);
$finish = true;
// 程序执行完成