服务端保留变量与临时文件时,如何实现客户端与服务端多请求交互?
解决方案:中断式文件处理+上下文持久化
核心逻辑
HTTP是无状态协议,单次请求结束后服务端内存变量会被销毁,所以必须把处理过程拆分为多阶段请求,同时在服务端持久化每个阶段的上下文(临时文件、中间变量、处理状态)。客户端收到决策请求后,主动携带上下文标识发起新请求,服务端通过标识恢复上下文继续处理。
PHP 实现方案
1. 第一阶段:上传文件+触发决策请求
处理上传并生成唯一process_id,把临时文件、中间变量存入会话(Session),返回决策提示和process_id:
session_start(); // 生成唯一处理ID,避免冲突 $process_id = uniqid('file_proc_', true); // 接收上传数据(示例为POST参数,实际可改为$_FILES处理文件) $fileData = $_POST['initRequest']; // 保存临时文件到服务器临时目录 $temp_file = sys_get_temp_dir() . "/{$process_id}_temp.dat"; file_put_contents($temp_file, $fileData); // 上下文存入Session $_SESSION[$process_id] = [ 'temp_file' => $temp_file, 'stage' => 'await_decision', // 可添加其他需要保留的中间变量 ]; // 返回决策请求 echo json_encode([ 'code' => 200, 'msg' => 'Make Decision', 'process_id' => $process_id ]);
2. 客户端处理决策并发起续处理请求
拿到process_id后展示决策UI,用户选择后携带ID发送请求:
fetch('/api/init', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({initRequest: 'your_file_data'}) }) .then(res => res.json()) .then(res => { if(res.code === 200 && res.msg === 'Make Decision'){ // 渲染决策按钮 const decisionPanel = document.createElement('div'); decisionPanel.innerHTML = ` <button onclick="submitDecision('${res.process_id}', 'option1')">选项1</button> <button onclick="submitDecision('${res.process_id}', 'option2')">选项2</button> `; document.body.appendChild(decisionPanel); } }); function submitDecision(processId, decision){ fetch('/api/continue', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({process_id: processId, decision: decision}) }) .then(res => res.json()) .then(res => { if(res.code === 200){ // 触发文件下载 window.location.href = res.download_url; // 清理资源 fetch('/api/cleanup', { method: 'POST', body: JSON.stringify({process_id: processId}) }); } }); }
3. 第二阶段:恢复上下文继续处理
通过process_id从Session读取上下文,根据用户决策完成处理:
session_start(); $input = json_decode(file_get_contents('php://input'), true); $process_id = $input['process_id']; $decision = $input['decision']; // 校验上下文是否存在 if(!isset($_SESSION[$process_id])){ echo json_encode(['code' => 400, 'msg' => '无效的处理ID']); exit; } $context = $_SESSION[$process_id]; $temp_file = $context['temp_file']; $fileData = file_get_contents($temp_file); // 根据决策执行后续逻辑 if($decision === 'option1'){ $output_file = sys_get_temp_dir() . "/{$process_id}_output1.pdf"; file_put_contents($output_file, "按选项1处理:{$fileData}"); } else { $output_file = sys_get_temp_dir() . "/{$process_id}_output2.pdf"; file_put_contents($output_file, "按选项2处理:{$fileData}"); } // 返回下载链接 echo json_encode([ 'code' => 200, 'download_url' => "/api/download?file=" . basename($output_file) ]);
4. 资源清理接口
避免临时文件和会话占用资源:
session_start(); $input = json_decode(file_get_contents('php://input'), true); $process_id = $input['process_id']; if(isset($_SESSION[$process_id])){ $context = $_SESSION[$process_id]; // 删除临时文件 if(file_exists($context['temp_file'])) unlink($context['temp_file']); // 删除输出文件 $output_file = sys_get_temp_dir() . "/" . basename($_GET['file'] ?? ''); if(file_exists($output_file)) unlink($output_file); // 清除会话上下文 unset($_SESSION[$process_id]); } echo json_encode(['code' => 200, 'msg' => '清理完成']);
Node.js (Express) 实现方案
1. 依赖安装
npm install express express-session multer
2. 服务端核心代码
const express = require('express'); const session = require('express-session'); const multer = require('multer'); const fs = require('fs'); const path = require('path'); const app = express(); const upload = multer({ dest: path.join(__dirname, 'temp') }); // 配置会话存储 app.use(session({ secret: 'your_secret_key', resave: false, saveUninitialized: false, cookie: { maxAge: 3600000 } // 1小时过期 })); app.use(express.json()); // 第一阶段:上传文件并触发决策请求 app.post('/api/init', upload.single('file'), (req, res) => { const processId = `file_proc_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; const tempFile = req.file.path; // 保存上下文到会话 req.session[processId] = { tempFile, originalName: req.file.originalname, stage: 'await_decision' }; res.json({ code: 200, msg: 'Make Decision', process_id: processId }); }); // 第二阶段:接收决策并继续处理 app.post('/api/continue', (req, res) => { const { process_id, decision } = req.body; const context = req.session[process_id]; if(!context) return res.status(400).json({ code: 400, msg: '无效的处理ID' }); fs.readFile(context.tempFile, (err, fileData) => { if(err) return res.status(500).json({ code: 500, msg: '读取临时文件失败' }); // 根据决策生成输出文件 const outputDir = path.join(__dirname, 'output'); if(!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); const outputFile = path.join(outputDir, `${processId}_${decision}_${context.originalName}`); fs.writeFileSync(outputFile, `按${decision}处理:${fileData.toString()}`); res.json({ code: 200, download_url: `/api/download?file=${path.basename(outputFile)}` }); }); }); // 文件下载接口 app.get('/api/download', (req, res) => { const fileName = req.query.file; const filePath = path.join(__dirname, 'output', fileName); res.download(filePath, err => { if(err) res.status(404).json({ code: 404, msg: '文件不存在' }); }); }); // 资源清理接口 app.post('/api/cleanup', (req, res) => { const { process_id } = req.body; const context = req.session[process_id]; if(context){ // 删除临时文件 if(fs.existsSync(context.tempFile)) fs.unlinkSync(context.tempFile); // 删除输出文件 const outputFiles = fs.readdirSync(path.join(__dirname, 'output')); outputFiles.forEach(file => { if(file.startsWith(process_id)) fs.unlinkSync(path.join(__dirname, 'output', file)); }); // 清除会话上下文 delete req.session[process_id]; } res.json({ code: 200, msg: '清理完成' }); }); // 创建临时目录和输出目录 ['temp', 'output'].forEach(dir => { const dirPath = path.join(__dirname, dir); if(!fs.existsSync(dirPath)) fs.mkdirSync(dirPath); }); app.listen(3000, () => console.log('服务运行在端口3000'));
WebSocket 实时方案(可选)
如果需要服务端异步触发决策请求(比如处理过程中动态遇到分支),可以用WebSocket保持长连接,无需多次发起HTTP请求:
服务端(Node.js + ws)
const WebSocket = require('ws'); const fs = require('fs'); const path = require('path'); const wss = new WebSocket.Server({ port: 8080 }); const connections = new Map(); // 存储连接上下文 wss.on('connection', ws => { const connId = `conn_${Date.now()}`; connections.set(connId, { ws, stage: 'init' }); ws.on('message', msg => { const data = JSON.parse(msg); const context = connections.get(connId); switch(data.type){ case 'upload': // 保存临时文件 const tempFile = path.join(__dirname, 'temp', `${connId}_temp.dat`); fs.writeFileSync(tempFile, data.fileData); context.tempFile = tempFile; context.stage = 'processing'; // 模拟处理到分支点 setTimeout(() => { ws.send(JSON.stringify({ type: 'decision_request', options: ['option1', 'option2'] })); context.stage = 'await_decision'; }, 1000); break; case 'decision': if(context.stage !== 'await_decision'){ ws.send(JSON.stringify({ type: 'error', msg: '无效阶段' })); return; } // 读取临时文件并处理 const fileData = fs.readFileSync(context.tempFile); const outputFile = path.join(__dirname, 'output', `${connId}_${data.decision}.dat`); fs.writeFileSync(outputFile, `按${data.decision}处理:${fileData.toString()}`); // 返回下载链接 ws.send(JSON.stringify({ type: 'done', download_url: `http://localhost:3000/download?file=${path.basename(outputFile)}` })); // 清理资源 fs.unlinkSync(context.tempFile); connections.delete(connId); break; } }); ws.on('close', () => { // 清理未处理资源 const context = connections.get(connId); if(context?.tempFile && fs.existsSync(context.tempFile)){ fs.unlinkSync(context.tempFile); } connections.delete(connId); }); });
客户端代码
const ws = new WebSocket('ws://localhost:8080'); ws.onopen = () => { // 上传文件数据(实际场景可读取本地文件) ws.send(JSON.stringify({ type: 'upload', fileData: 'test_file_content' })); }; ws.onmessage = event => { const data = JSON.parse(event.data); switch(data.type){ case 'decision_request': // 渲染决策UI const panel = document.createElement('div'); data.options.forEach(opt => { const btn = document.createElement('button'); btn.textContent = opt; btn.onclick = () => { ws.send(JSON.stringify({ type: 'decision', decision: opt })); panel.remove(); }; panel.appendChild(btn); }); document.body.appendChild(panel); break; case 'done': window.location.href = data.download_url; ws.close(); break; } };
内容的提问来源于stack exchange,提问作者Jill John
相关产品推荐
相关产品推荐

