简体   繁体   English

如何发出带有回调的 Flask-SocketIO 请求,在用户重新加入并且他们的 sid 更改后仍然有效?

[英]How can I emit Flask-SocketIO requests with callbacks that still work after a user rejoins and their sid changes?

Summarize the Problem总结问题

I am using Flask-SocketIO for a project and am basically trying to make it so that users can rejoin a room and "pick up where they left off."我在一个项目中使用 Flask-SocketIO,基本上是在尝试让用户可以重新加入房间并“从他们离开的地方继续”。 To be more specific:更具体:

  1. The server emits a request to the client, with a callback to process the response and a timeout of 1 second.服务器向客户端发出请求,回调处理响应和 1 秒超时。 This is done in a loop so that the request is resent if a user rejoins the room.这是在循环中完成的,以便在用户重新加入房间时重新发送请求。
  2. A user "rejoining" a room is defined as a user joining a room with the same name as a user who has previously been disconnected from that room.用户“重新加入”房间被定义为用户加入了与先前与该房间断开连接的用户同名的房间。 The user is given their new SID in this case and the request to the client is sent to the new SID.在这种情况下,用户会获得新的 SID,并且对客户端的请求会发送到新的 SID。

What I am seeing is this:我看到的是这样的:

  1. If the user joins the room and does everything normally, the callback is processed correctly on the server.如果用户加入房间并正常执行所有操作,则回调在服务器上得到正确处理。

  2. It a user rejoins the room while the server is sending requests and then submits a response, everything on the JavaScript side works fine, the server receives an ack but does not actually run the callback that it is supposed to:如果用户在服务器发送请求然后提交响应时重新加入房间,JavaScript 端的一切工作正常,服务器收到确认但实际上并未运行它应该运行的回调:

     uV7BTVtBXwQ6oopnAAAE: Received packet MESSAGE data 313["#000000"] received ack from Ac8wmpy2lK-kTQL7AAAF [/]

This question is similar to mine but the solution for them was to update Flask-SocketIO and I am running a version newer than theirs: python flask-socketio server receives message but doesn't trigger event这个问题与我的类似,但他们的解决方案是更新 Flask-SocketIO 而我正在运行比他们更新的版本: python flask-socketio server receives message but doesn't trigger event

Show Some Code显示一些代码

I have created a repository with a "minimal" example here: https://github.com/eshapiro42/socketio-example .我在这里创建了一个带有“最小”示例的存储库: https://github.com/eshapiro42/socketio-example

In case something happens to that link in the future, here are the relevant bits:万一将来该链接发生问题,以下是相关位:

# 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>

Edit编辑

Steps to Reproduce重现步骤

  1. Start the server python app.py and visit localhost:5000 in your browser.启动服务器python app.py并在浏览器中访问localhost:5000
  2. Enter any username and Room ID and click "Join Room."输入任何用户名和房间 ID,然后单击“加入房间”。
  3. Click "Collect Colors."点击“领取Colors”。
  4. Select a color and click "Send." Select一种颜色,点击“发送”。 The selector should disappear and the server should print out a confirmation.选择器应该消失并且服务器应该打印出确认信息。
  5. Reload everything.重新加载一切。
  6. Repeat steps 2 and 3 and copy the Room ID.重复步骤 2 和 3 并复制房间 ID。
  7. Exit the page and then navigate back to it.退出该页面,然后导航回该页面。
  8. Enter the same username and Room ID as you did in step 6 and click "Join Room."输入与第 6 步中相同的用户名和房间 ID ,然后单击“加入房间”。
  9. Select a color and click "Send." Select一种颜色,点击“发送”。 The selector disappears briefly but then comes back, since the server did not correctly process the response and keeps sending requests instead.选择器短暂消失但随后又回来,因为服务器没有正确处理响应并继续发送请求。

Edit 2编辑 2

I managed to work around (not solve) the problem by adding more state variables on the server side and implementing a few more events to avoid using callbacks entirely.我设法通过在服务器端添加更多 state 变量并实施更多事件来避免完全使用回调来解决(而不是解决)问题。 I would still love to know what was going wrong with the callback-based approach though since using that seems cleaner to me.我仍然很想知道基于回调的方法出了什么问题,因为使用它对我来说似乎更干净。

The reason why those callbacks do not work is that you are making the emits from a context that is based on the old and disconnected socket.这些回调不起作用的原因是您正在从基于旧的和断开连接的套接字的上下文中进行发出。

The callback is associated with the socket identified by request.sid .回调与request.sid标识的套接字相关联。 Associating the callback with a socket allows Flask-SocketIO to install the correct app and request contexts when the callback is invoked.将回调与套接字相关联允许 Flask-SocketIO 在调用回调时安装正确的应用程序和请求上下文。

The way that you coded your color prompt is not great, because you have a long running event handler that continues to run after the client goes aways and reconnects on a different socket.您对颜色提示进行编码的方式不是很好,因为您有一个长时间运行的事件处理程序,该处理程序在客户端离开并在不同的套接字上重新连接后继续运行。 A better design would be for the client to send the selected color in its own event instead of as a callback response to the server.更好的设计是客户端在其自己的事件中发送选定的颜色,而不是作为对服务器的回调响应。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM