Flask服务端如何处理JS请求解决游戏用户同步的并发问题?
解决Flask服务端与JavaScript客户端的游戏同步问题
需求与现状
需要通过JavaScript定期向Flask服务端发起请求,同步同一游戏实例内玩家的操作:服务端返回最新玩家命令,客户端对比本地记录,若有更新则执行同步。
原客户端JavaScript代码:
function checkUpdate() { setInterval(updateState, 1000, document.getElementById('serverId').textContent); } function updateState(serverId) { var request = new XMLHttpRequest(); request.onreadystatechange = function() { if (request.readyState == XMLHttpRequest.DONE) { const response = request.responseText if (response != document.getElementById('lastCommand').textContent) { document.getElementById('lastCommand').textContent = response eval(document.getElementById('lastCommand').textContent) } } } request.open('GET', '/updateState?serverId='+serverId, true); request.send(null); }
原Flask服务端代码:
@app.route('/updateState') def updateState(): serverId = int(request.args['serverId']) for instance in instances: try: if serverId == instance.id: return instance.lastCommand[-1] except IndexError: return 'Error' return 'Error'
现存问题
短轮询间隔1秒,当玩家命令请求与轮询请求并发时,服务端可能未完成命令处理就返回轮询结果,导致客户端未检测到更新,同步失败。
解决方案:长轮询(Long Polling)
改用长轮询模式:客户端发起请求后,服务端不立即返回结果,等待有新命令或超时后再响应;客户端收到响应后立刻发起下一次请求,维持持续连接,避免并发冲突。
客户端改造
替换setInterval为递归调用,确保每次请求完成后再发起下一次:
function checkUpdate(serverId) { // 递归发起长轮询请求 function poll() { var request = new XMLHttpRequest(); request.onreadystatechange = function() { if (request.readyState == XMLHttpRequest.DONE) { if (request.status === 200) { const response = request.responseText if (response != document.getElementById('lastCommand').textContent) { document.getElementById('lastCommand').textContent = response eval(document.getElementById('lastCommand').textContent) } // 请求完成后立刻发起下一次轮询(加短延迟避免频繁请求) setTimeout(poll, 100); } else { // 请求失败时延迟重试 setTimeout(poll, 2000); } } } request.open('GET', '/updateState?serverId='+serverId, true); request.send(null); } poll(); } // 初始化调用 checkUpdate(document.getElementById('serverId').textContent);
服务端改造(异步Flask实现)
Flask 2.0+支持异步路由,结合asyncio.Event实现非阻塞等待新命令:
from flask import Flask, request import asyncio from collections import defaultdict app = Flask(__name__) # 存储每个游戏实例的状态:key为serverId,value为{event: asyncio.Event, last_command: str} game_instances = defaultdict(lambda: {"event": asyncio.Event(), "last_command": ""}) # 处理玩家命令的接口(示例) @app.route('/sendCommand', methods=['POST']) async def send_command(): server_id = int(request.form['serverId']) command = request.form['command'] instance = game_instances[server_id] instance['last_command'] = command # 触发事件,通知等待的轮询请求 instance['event'].set() instance['event'].clear() # 重置事件,等待下一次命令 return "OK" # 长轮询接口 @app.route('/updateState') async def update_state(): server_id = int(request.args['serverId']) instance = game_instances[server_id] current_command = instance['last_command'] try: # 等待新命令或超时(15秒) await asyncio.wait_for(instance['event'].wait(), timeout=15) except asyncio.TimeoutError: # 超时返回当前命令,客户端会重新发起请求 return current_command # 有新命令时返回最新值 return instance['last_command'] if __name__ == '__main__': app.run(debug=True)
关键说明
- 异步路由:用
async def定义路由,Flask会自动用异步事件循环处理,避免阻塞服务端主线程。 - 事件通知:每个游戏实例维护
asyncio.Event,当新命令到达时触发事件,唤醒等待的轮询请求。 - 超时处理:设置15秒超时,防止客户端因网络问题一直挂起,超时后返回当前命令,客户端自动重试。
- 状态存储:用
defaultdict统一管理游戏实例状态,确保多请求间的状态共享。
内容的提问来源于stack exchange,提问作者spots 727
相关产品推荐
相关产品推荐

