簡體   English   中英

如何發出帶有回調的 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. 服務器向客戶端發出請求,回調處理響應和 1 秒超時。 這是在循環中完成的,以便在用戶重新加入房間時重新發送請求。
  2. 用戶“重新加入”房間被定義為用戶加入了與先前與該房間斷開連接的用戶同名的房間。 在這種情況下,用戶會獲得新的 SID,並且對客戶端的請求會發送到新的 SID。

我看到的是這樣的:

  1. 如果用戶加入房間並正常執行所有操作,則回調在服務器上得到正確處理。

  2. 如果用戶在服務器發送請求然后提交響應時重新加入房間,JavaScript 端的一切工作正常,服務器收到確認但實際上並未運行它應該運行的回調:

     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

萬一將來該鏈接發生問題,以下是相關位:

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

編輯

重現步驟

  1. 啟動服務器python app.py並在瀏覽器中訪問localhost:5000
  2. 輸入任何用戶名和房間 ID,然后單擊“加入房間”。
  3. 點擊“領取Colors”。
  4. Select一種顏色,點擊“發送”。 選擇器應該消失並且服務器應該打印出確認信息。
  5. 重新加載一切。
  6. 重復步驟 2 和 3 並復制房間 ID。
  7. 退出該頁面,然后導航回該頁面。
  8. 輸入與第 6 步中相同的用戶名和房間 ID ,然后單擊“加入房間”。
  9. Select一種顏色,點擊“發送”。 選擇器短暫消失但隨后又回來,因為服務器沒有正確處理響應並繼續發送請求。

編輯 2

我設法通過在服務器端添加更多 state 變量並實施更多事件來避免完全使用回調來解決(而不是解決)問題。 我仍然很想知道基於回調的方法出了什么問題,因為使用它對我來說似乎更干凈。

這些回調不起作用的原因是您正在從基於舊的和斷開連接的套接字的上下文中進行發出。

回調與request.sid標識的套接字相關聯。 將回調與套接字相關聯允許 Flask-SocketIO 在調用回調時安裝正確的應用程序和請求上下文。

您對顏色提示進行編碼的方式不是很好,因為您有一個長時間運行的事件處理程序,該處理程序在客戶端離開並在不同的套接字上重新連接后繼續運行。 更好的設計是客戶端在其自己的事件中發送選定的顏色,而不是作為對服務器的回調響應。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM