如何发出带有回调的 Flask-SocketIO 请求,在用户重新加入并且他们的 sid 更改后仍然有效?
How can I emit Flask-SocketIO requests with callbacks that still work after a user rejoins and their sid changes?
总结问题
我正在为一个项目使用 Flask-SocketIO,并且我基本上是在努力做到这一点,以便用户可以重新加入房间并“从他们离开的地方继续”。更具体地说:
- 服务器向客户端发出请求,回调处理响应,超时时间为 1 秒。这是在循环中完成的,以便在用户重新加入房间时重新发送请求。
- “重新加入”房间的用户定义为用户加入了与之前断开连接的用户同名的房间。在这种情况下,用户将获得他们的新 SID,并且对客户端的请求将发送到新的 SID。
我看到的是这样的:
如果用户加入房间并且一切正常,则回调在服务器上正确处理。
如果用户在服务器发送请求时重新加入房间,然后提交响应,JavaScript 端一切正常,服务器收到 ack 但实际上没有 运行 应该的回调:
uV7BTVtBXwQ6oopnAAAE: Received packet MESSAGE data 313["#000000"]
received ack from Ac8wmpy2lK-kTQL7AAAF [/]
这个问题与我的类似,但他们的解决方案是更新 Flask-SocketIO,我运行正在使用比他们更新的版本:python flask-socketio server receives message but doesn't trigger event
显示一些代码
我在这里创建了一个带有“最小”示例的存储库:https://github.com/eshapiro42/socketio-example。
万一 link 将来发生什么事,这里是相关位:
# app.py
from gevent import monkey
monkey.patch_all()
import flask_socketio
from collections import defaultdict
from flask import Flask, request, send_from_directory
from user import User
app = Flask(__name__)
socketio = flask_socketio.SocketIO(app, async_mode="gevent", logger=True, engineio_logger=True)
@app.route("/")
def base():
return send_from_directory("static", "index.html")
@app.route("/<path:path>")
def home(path):
return send_from_directory("static", path)
# Global dictionary of users, indexed by room
connected_users = defaultdict(list)
# Global dictionary of disconnected users, indexed by room
disconnected_users = defaultdict(list)
@socketio.on("join room")
def join_room(data):
sid = request.sid
username = data["username"]
room = data["room"]
flask_socketio.join_room(room)
# If the user is rejoining, change their sid
for room, users in disconnected_users.items():
for user in users:
if user.name == username:
socketio.send(f"{username} has rejoined the room.", room=room)
user.sid = sid
# Add the user back to the connected users list
connected_users[room].append(user)
# Remove the user from the disconnected list
disconnected_users[room].remove(user)
return True
# If the user is new, create a new user
socketio.send(f"{username} has joined the room.", room=room)
user = User(username, socketio, room, sid)
connected_users[room].append(user)
return True
@socketio.on("disconnect")
def disconnect():
sid = request.sid
# Find the room and user with this sid
user_found = False
for room, users in connected_users.items():
for user in users:
if user.sid == sid:
user_found = True
break
if user_found:
break
# If a matching user was not found, do nothing
if not user_found:
return
room = user.room
socketio.send(f"{user.name} has left the room.", room=room)
# Remove the user from the room
connected_users[room].remove(user)
# Add the user to the disconnected list
disconnected_users[room].append(user)
flask_socketio.leave_room(room)
@socketio.on("collect colors")
def collect_colors(data):
room = data["room"]
for user in connected_users[room]:
color = user.call("send color", data)
print(f"{user.name}'s color is {color}.")
if __name__ == "__main__":
socketio.run(app, debug=True)
# user.py
from threading import Event # Monkey patched
class User:
def __init__(self, name, socketio, room, sid):
self.name = name
self.socketio = socketio
self.room = room
self._sid = sid
@property
def sid(self):
return self._sid
@sid.setter
def sid(self, new_sid):
self._sid = new_sid
def call(self, event_name, data):
"""
Send a request to the player and wait for a response.
"""
event = Event()
response = None
# Create callback to run when a response is received
def ack(response_data):
print("WHY DOES THIS NOT RUN AFTER A REJOIN?")
nonlocal event
nonlocal response
response = response_data
event.set()
# Try in a loop with a one second timeout in case an event gets missed or a network error occurs
tries = 0
while True:
# Send request
self.socketio.emit(
event_name,
data,
to=self.sid,
callback=ack,
)
# Wait for response
if event.wait(1):
# Response was received
break
tries += 1
if tries % 10 == 0:
print(f"Still waiting for input after {tries} seconds")
return response
// static/client.js
var socket = io.connect();
var username = null;
var room = null;
var joined = false;
var colorCallback = null;
function joinedRoom(success) {
if (success) {
joined = true;
$("#joinForm").hide();
$("#collectColorsButton").show();
$("#gameRoom").text(`Room: ${room}`);
}
}
socket.on("connect", () => {
console.log("You are connected to the server.");
});
socket.on("connect_error", (data) => {
console.log(`Unable to connect to the server: ${data}.`);
});
socket.on("disconnect", () => {
console.log("You have been disconnected from the server.");
});
socket.on("message", (data) => {
console.log(data);
});
socket.on("send color", (data, callback) => {
$("#collectColorsButton").hide();
$("#colorForm").show();
console.log(`Callback set to ${callback}`);
colorCallback = callback;
});
$("#joinForm").on("submit", (event) => {
event.preventDefault();
username = $("#usernameInput").val();
room = $("#roomInput").val()
socket.emit("join room", {username: username, room: room}, joinedRoom);
});
$("#colorForm").on("submit", (event) => {
event.preventDefault();
var color = $("#colorInput").val();
$("#colorForm").hide();
colorCallback(color);
});
$("#collectColorsButton").on("click", () => {
socket.emit("collect colors", {username: username, room: room});
});
<!-- static/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Socket.IO Example</title>
</head>
<body>
<p id="gameRoom"></p>
<form id="joinForm">
<input id="usernameInput" type="text" placeholder="Your Name" autocomplete="off" required>
<input id="roomInput" type="text" placeholder="Room ID" autocomplete="off" required>
<button id="joinGameSubmitButton" type="submit" btn btn-dark">Join Room</button>
</form>
<button id="collectColorsButton" style="display: none;">Collect Colors</button>
<form id="colorForm" style="display: none;">
<p>Please select a color.</p>
<input id="colorInput" type="color" required>
<button id="colorSubmitButton" type="submit">Send Color</button>
</form>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script src="https://cdn.socket.io/4.4.1/socket.io.min.js" integrity="sha384-fKnu0iswBIqkjxrhQCTZ7qlLHOFEgNkRmK2vaO/LbTZSXdJfAu6ewRBdwHPhBo/H" crossorigin="anonymous"></script>
<script src="client.js"></script>
</body>
</html>
编辑
重现步骤
- 启动服务器
python app.py
并在浏览器中访问 localhost:5000
。
- 输入任何用户名和房间 ID,然后单击“加入房间”。
- 点击“收集颜色”。
- Select一种颜色,然后单击“发送”。选择器应该消失,服务器应该打印出确认信息。
- 重新加载所有内容。
- 重复第 2 步和第 3 步并复制房间 ID。
- 退出页面,然后导航返回。
- 输入与第 6 步相同的用户名和房间 ID,然后单击“加入房间”。
- Select一种颜色,然后单击“发送”。选择器短暂消失但随后又回来,因为服务器没有正确处理响应并继续发送请求。
编辑 2
我设法通过在服务器端添加更多状态变量并实现更多事件来避免完全使用回调来解决(而不是解决)问题。我仍然很想知道基于回调的方法出了什么问题,因为使用它对我来说似乎更干净。
这些回调不起作用的原因是您从基于旧的和断开连接的套接字的上下文中进行发出。
回调与 request.sid
标识的套接字关联。将回调与套接字相关联允许 Flask-SocketIO 在调用回调时安装正确的应用程序和请求上下文。
您对颜色提示进行编码的方式不是很好,因为您有一个很长的 运行ning 事件处理程序,在客户端离开并在不同的套接字上重新连接后,它会继续 运行。更好的设计是让客户端在其自己的事件中发送选定的颜色,而不是作为对服务器的回调响应。
总结问题
我正在为一个项目使用 Flask-SocketIO,并且我基本上是在努力做到这一点,以便用户可以重新加入房间并“从他们离开的地方继续”。更具体地说:
- 服务器向客户端发出请求,回调处理响应,超时时间为 1 秒。这是在循环中完成的,以便在用户重新加入房间时重新发送请求。
- “重新加入”房间的用户定义为用户加入了与之前断开连接的用户同名的房间。在这种情况下,用户将获得他们的新 SID,并且对客户端的请求将发送到新的 SID。
我看到的是这样的:
如果用户加入房间并且一切正常,则回调在服务器上正确处理。
如果用户在服务器发送请求时重新加入房间,然后提交响应,JavaScript 端一切正常,服务器收到 ack 但实际上没有 运行 应该的回调:
uV7BTVtBXwQ6oopnAAAE: Received packet MESSAGE data 313["#000000"] received ack from Ac8wmpy2lK-kTQL7AAAF [/]
这个问题与我的类似,但他们的解决方案是更新 Flask-SocketIO,我运行正在使用比他们更新的版本:python flask-socketio server receives message but doesn't trigger event
显示一些代码
我在这里创建了一个带有“最小”示例的存储库:https://github.com/eshapiro42/socketio-example。
万一 link 将来发生什么事,这里是相关位:
# app.py
from gevent import monkey
monkey.patch_all()
import flask_socketio
from collections import defaultdict
from flask import Flask, request, send_from_directory
from user import User
app = Flask(__name__)
socketio = flask_socketio.SocketIO(app, async_mode="gevent", logger=True, engineio_logger=True)
@app.route("/")
def base():
return send_from_directory("static", "index.html")
@app.route("/<path:path>")
def home(path):
return send_from_directory("static", path)
# Global dictionary of users, indexed by room
connected_users = defaultdict(list)
# Global dictionary of disconnected users, indexed by room
disconnected_users = defaultdict(list)
@socketio.on("join room")
def join_room(data):
sid = request.sid
username = data["username"]
room = data["room"]
flask_socketio.join_room(room)
# If the user is rejoining, change their sid
for room, users in disconnected_users.items():
for user in users:
if user.name == username:
socketio.send(f"{username} has rejoined the room.", room=room)
user.sid = sid
# Add the user back to the connected users list
connected_users[room].append(user)
# Remove the user from the disconnected list
disconnected_users[room].remove(user)
return True
# If the user is new, create a new user
socketio.send(f"{username} has joined the room.", room=room)
user = User(username, socketio, room, sid)
connected_users[room].append(user)
return True
@socketio.on("disconnect")
def disconnect():
sid = request.sid
# Find the room and user with this sid
user_found = False
for room, users in connected_users.items():
for user in users:
if user.sid == sid:
user_found = True
break
if user_found:
break
# If a matching user was not found, do nothing
if not user_found:
return
room = user.room
socketio.send(f"{user.name} has left the room.", room=room)
# Remove the user from the room
connected_users[room].remove(user)
# Add the user to the disconnected list
disconnected_users[room].append(user)
flask_socketio.leave_room(room)
@socketio.on("collect colors")
def collect_colors(data):
room = data["room"]
for user in connected_users[room]:
color = user.call("send color", data)
print(f"{user.name}'s color is {color}.")
if __name__ == "__main__":
socketio.run(app, debug=True)
# user.py
from threading import Event # Monkey patched
class User:
def __init__(self, name, socketio, room, sid):
self.name = name
self.socketio = socketio
self.room = room
self._sid = sid
@property
def sid(self):
return self._sid
@sid.setter
def sid(self, new_sid):
self._sid = new_sid
def call(self, event_name, data):
"""
Send a request to the player and wait for a response.
"""
event = Event()
response = None
# Create callback to run when a response is received
def ack(response_data):
print("WHY DOES THIS NOT RUN AFTER A REJOIN?")
nonlocal event
nonlocal response
response = response_data
event.set()
# Try in a loop with a one second timeout in case an event gets missed or a network error occurs
tries = 0
while True:
# Send request
self.socketio.emit(
event_name,
data,
to=self.sid,
callback=ack,
)
# Wait for response
if event.wait(1):
# Response was received
break
tries += 1
if tries % 10 == 0:
print(f"Still waiting for input after {tries} seconds")
return response
// static/client.js
var socket = io.connect();
var username = null;
var room = null;
var joined = false;
var colorCallback = null;
function joinedRoom(success) {
if (success) {
joined = true;
$("#joinForm").hide();
$("#collectColorsButton").show();
$("#gameRoom").text(`Room: ${room}`);
}
}
socket.on("connect", () => {
console.log("You are connected to the server.");
});
socket.on("connect_error", (data) => {
console.log(`Unable to connect to the server: ${data}.`);
});
socket.on("disconnect", () => {
console.log("You have been disconnected from the server.");
});
socket.on("message", (data) => {
console.log(data);
});
socket.on("send color", (data, callback) => {
$("#collectColorsButton").hide();
$("#colorForm").show();
console.log(`Callback set to ${callback}`);
colorCallback = callback;
});
$("#joinForm").on("submit", (event) => {
event.preventDefault();
username = $("#usernameInput").val();
room = $("#roomInput").val()
socket.emit("join room", {username: username, room: room}, joinedRoom);
});
$("#colorForm").on("submit", (event) => {
event.preventDefault();
var color = $("#colorInput").val();
$("#colorForm").hide();
colorCallback(color);
});
$("#collectColorsButton").on("click", () => {
socket.emit("collect colors", {username: username, room: room});
});
<!-- static/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Socket.IO Example</title>
</head>
<body>
<p id="gameRoom"></p>
<form id="joinForm">
<input id="usernameInput" type="text" placeholder="Your Name" autocomplete="off" required>
<input id="roomInput" type="text" placeholder="Room ID" autocomplete="off" required>
<button id="joinGameSubmitButton" type="submit" btn btn-dark">Join Room</button>
</form>
<button id="collectColorsButton" style="display: none;">Collect Colors</button>
<form id="colorForm" style="display: none;">
<p>Please select a color.</p>
<input id="colorInput" type="color" required>
<button id="colorSubmitButton" type="submit">Send Color</button>
</form>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script src="https://cdn.socket.io/4.4.1/socket.io.min.js" integrity="sha384-fKnu0iswBIqkjxrhQCTZ7qlLHOFEgNkRmK2vaO/LbTZSXdJfAu6ewRBdwHPhBo/H" crossorigin="anonymous"></script>
<script src="client.js"></script>
</body>
</html>
编辑
重现步骤
- 启动服务器
python app.py
并在浏览器中访问localhost:5000
。 - 输入任何用户名和房间 ID,然后单击“加入房间”。
- 点击“收集颜色”。
- Select一种颜色,然后单击“发送”。选择器应该消失,服务器应该打印出确认信息。
- 重新加载所有内容。
- 重复第 2 步和第 3 步并复制房间 ID。
- 退出页面,然后导航返回。
- 输入与第 6 步相同的用户名和房间 ID,然后单击“加入房间”。
- Select一种颜色,然后单击“发送”。选择器短暂消失但随后又回来,因为服务器没有正确处理响应并继续发送请求。
编辑 2
我设法通过在服务器端添加更多状态变量并实现更多事件来避免完全使用回调来解决(而不是解决)问题。我仍然很想知道基于回调的方法出了什么问题,因为使用它对我来说似乎更干净。
这些回调不起作用的原因是您从基于旧的和断开连接的套接字的上下文中进行发出。
回调与 request.sid
标识的套接字关联。将回调与套接字相关联允许 Flask-SocketIO 在调用回调时安装正确的应用程序和请求上下文。
您对颜色提示进行编码的方式不是很好,因为您有一个很长的 运行ning 事件处理程序,在客户端离开并在不同的套接字上重新连接后,它会继续 运行。更好的设计是让客户端在其自己的事件中发送选定的颜色,而不是作为对服务器的回调响应。