Verständnisfrage zu Fastapi/Websocket - Clientdaten

Django, Flask, Bottle, WSGI, CGI…
Antworten
Benutzeravatar
Lechim
User
Beiträge: 6
Registriert: Montag 7. Oktober 2013, 20:39
Wohnort: Berlin

Hallöle,

bin auf dieses Beispiel

https://fastapi.tiangolo.com/advanced/w ... le-clients

gestoßen, und habe etwas experimentiert, woraus sich die ein oder andere Frage ergibt.
Es soll kein ernsthaftes / praktikables Projekt werden sondern lediglich Übung.

Hätte es gerne so dass User über ihren Benutzernamen angesprochen werden können: z.B Flüstern, Nick-Farbe ändern.
Siehe im Code Unten

Code: Alles auswählen

#Benutzername1 an Benutzername2 (Kommando: /whisper Benutzername2 Hallo du Nudel!)
Dachte erst an ein Dict k=Username v=Socket, allerdings sind der noch mehr Informationen wie z.B Hexfarbe für den Namen daher vermute ich, ist wohl eine Userclass nötig, allerdings habe ich noch keinen wirklichen Ansatz dass umzusetzen.

Code: Alles auswählen

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse

app = FastAPI()

html = """
<!DOCTYPE html>
<html>
    <head>
        <title>Chat</title>
    </head>
    <body>
        <h1>WebSocket Chat</h1>
        <h2>Your ID: <span id="ws-id"></span></h2>
        <form action="" onsubmit="sendMessage(event)">
            <input type="text" id="messageText" autocomplete="off"/>
            <button>Send</button>
        </form>
        <ul id='messages'>
        </ul>
        <script>
            var client_id = Date.now()
            document.querySelector("#ws-id").textContent = client_id;
            var ws = new WebSocket(`ws://localhost:8000/ws/${client_id}`);
            ws.onmessage = function(event) {
                var messages = document.getElementById('messages')
                var message = document.createElement('li')
                var content = document.createTextNode(event.data)
                message.appendChild(content)
                messages.appendChild(message)
            };
            function sendMessage(event) {
                var input = document.getElementById("messageText")
                ws.send(input.value)
                input.value = ''
                event.preventDefault()
            }
        </script>
    </body>
</html>
"""

#Benutzerklasse
class User(Username: str, Color: str, Socket: WebSocket):
	def __init__(self):
		self.Benutzername = Username
		self.Color = Color
		self.Socket = Socket
		
		...
		
		
class ConnectionManager:
    def __init__(self):
        self.active_connections: list[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)

    def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)

    async def send_personal_message(self, message: str, websocket: WebSocket):
        await websocket.send_text(message)

    async def broadcast(self, message: str):
        for connection in self.active_connections:
            await connection.send_text(message)


manager = ConnectionManager()


@app.get("/")
async def get():
    return HTMLResponse(html)


@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
    await manager.connect(websocket)
    
    newUser = User(client_id, "#DD00DD", websocket)
    
    try:
        while True:
            data = await websocket.receive_text()
            
            #Benutzername1 an Benutzername2 (Kommando: /whisper Benutzername2 Hallo du Nudel!)
            
            if data[0:3] == "/whisper ":
                    
                    data2 = data.split(" ", 2)
                    
                    Empfaenger_Name = data2[1]
                    Empfaenger_Message = data2[2]
                    
                    #Hier müsste nun rausgefunden werden welches websocket-Objekt Benutzer2 hat.
                    
                    
            await manager.send_personal_message(f"You wrote: {data}", websocket)
            await manager.broadcast(f"Client #{client_id} says: {data}")
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        await manager.broadcast(f"Client #{client_id} left the chat")
Sirius3
User
Beiträge: 18250
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Klasse User ist syntaktisch falsch. Programmieren ist nicht raten. Außerdem rückt man immer mit 4 Leerzeichen ein und nicht mal Tabs und mal Leerzeichen.
Variablennamen und Attribute schreibt man komplett klein.
Für `User` würde sich ja dataclass eignen, dann haben die Typannotationen wenigstens irgendeinen Zweck.
Du hast schon einen ConnectionManager. Der ist dazu da, die Nutzer als `active_connections` zu speichern, und nicht nur die WebSockets.
Dann fehlt ja nur noch eine Methode manager.get_user.
Benutzeravatar
Lechim
User
Beiträge: 6
Registriert: Montag 7. Oktober 2013, 20:39
Wohnort: Berlin

Danke für die Rückmeldung.

Hatte es so wie unten gemacht, aber da mir das mit den zwei listen nicht gefallen hat, dachte ich es gibt eine elegantere Lösung

Zumal die Listen in keiner Beziehung stehen außer dem Index und wer garantiert dass da nicht was durcheinanderkommen kann:

Code: Alles auswählen

class ConnectionManager:
    
    def __init__(self):
        
        self.active_connections: list[WebSocket] = []
        self.active_usernames: list[str] = []
        
    async def change_username(self, old: str, new: str):
        
        if new in self.active_usernames:
        
            return False
        
        else:
            
            index = 0
            for item in self.active_usernames:

                if old.lower() == str(item).lower():
                    
                    self.active_usernames[index] = new
                    
                    return True
                    
                index += 1
    
    async def get_socket_from_name(self, name: str):
        
        index = 0
        for item in self.active_usernames:

            if name.lower() == str(item).lower():

                return self.active_connections[index]
                
            index += 1
Benutzeravatar
__blackjack__
User
Beiträge: 13997
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Lechim: Das mit den parallelen Listen ist in der Tat falsch. Dafür verwendet man ein Wörterbuch.

`change_username()` ist fehlerhaft, denn beim ersten Test ob es den neuen Namen schon gibt, wird Gross- und Kleinschreibung berücksichtigt, bei `get_socket_from_name()` dann aber nicht. Also bei dem Wörterbuch dann am besten in Kleinbuchstaben umgewandelte Zeichenketten auf Objekte abbilden die den Benutzernamen enthalten wie er angezeigt werden soll.

Bei ``str(item).lower()`` macht das `str()` keinen Sinn. `item` ist bereits eine Zeichenektte. Die wird durch einen `str()`-Aufruf nicht irgendwie noch „zeichenkettiger“. 😎

Zum vorherigen Beitrag:

Man sollte Namen in einer Sprache benennen. ``self.benutzername = username`` ist schlecht und das wartet nur darauf das man da durcheinander kommt und nicht mehr weiss wo im Programm das jetzt auf Deutsch und wo auf Englisch benannt ist.

Der Zusatz `Manager` in Klassennamen ist ein Warnzeichen. Im Grunde verwaltet jede Klasse etwas, also ist jede Klasse ein ”Manager” und der Zusatz bietet keinen wirklichen Mehrwert. Die Klasse könnte einfach `Connections` heissen. Noch ein bisschen ungünstiger ist es das daraus erstellte Objekt einfach `manager` zu nennen. Hier ist doch für den Leser interessanter *was* da verwaltet wird, statt zu sagen hey hier wird irgend etwas verwaltet.

Das sollte auch nicht einfach so auf Modulebene definiert werden, sondern als Argument in die Routen-Funktionen kommen. FastAPI bietet da „dependency injection“ über die Typannotation. (Oder `starlette` über den `request.state` und den „Lifespan“ der Anwendung.)

`connect()` und `disconnect()` sind vom Namen inhaltlich nicht richtig. Da wird nichts verbunden und keine Verbindung gelöst. Da wird eine Verbindung hinzugefügt oder entfernt. Also `add()` und `remove()` wären passender.

`send_personal_message()` ist keine Methode sondern eine Funktion und da sollte man einen guten Grund haben warum das in der Klasse definiert ist. Oder es eben als Funktion definieren. Oder das so umbauen, dass es zu einer Methode wird. Zum Beispiel in dem da nicht das Websocket-Objekt übergeben wird, sondern die Benutzer-ID an den die Nachricht raus soll.

Bei `get()` macht ``async`` keinen Sinn. Das enthält selbst ja nix asynchrones.

Wie müsste denn eine Zeichenkette `data` aussehen, damit ``data[:3] == "/whisper "`` jemals wahr werden kann? Für diesen Test gibt es auch eine Methode, damit man nicht mit magischen Indexwerten operieren muss.

Man nummeriert keine Namen. Dann will man entweder bessere Namen oder gar keine Einzelnamen- und Werte, sondern eine Datenstruktur. Oft eine Liste.

Magische Indexwerte sollte man so gut es geht vermeiden. Zum Beispiel in dem man bei der Zuweisung mehrere Ziele angibt, statt mehrere Zuweisungen wo man sich per Indexwert Elemente heraus pickt:

Code: Alles auswählen

                data2 = data.split(" ", 2)
                recipient_name = data2[1]
                message_text = data2[2]
                
                # =>
                
                _, recipient_name, message_text = data.split(" ", 2)
Ich sehe da jetzt im Moment nicht wirklich einen Sinn von FastAPI bei dem Code. Das was da gemacht wird, kann man letztlich genau so gut mit `starlette` machen, denn was anderes ist das `WebSocket` “von“ FastAPI auch nicht.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Benutzeravatar
DeaD_EyE
User
Beiträge: 1217
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

basierend auf dem Beispiel:

Code: Alles auswählen

import uuid
from typing import TypeAlias

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from pydantic import UUID4, BaseModel, Field

app = FastAPI()

html = """
<!DOCTYPE html>
<html>
    <head>
        <title>Chat</title>
    </head>
    <body>
        <h1>WebSocket Chat</h1>
        <h2>Your ID: <span id="ws-id"></span></h2>
        <form action="" onsubmit="sendMessage(event)">
            <input type="text" id="messageText" autocomplete="off"/>
            <button>Send</button>
        </form>
        <ul id='messages'>
        </ul>
        <script>
            var ws = new WebSocket(`ws://localhost:8000/ws`);
            ws.onmessage = function(event) {
                if (event.data.startsWith("/uuid ")) {
                    var nick = event.data.replace("/uuid ", "").trimRight();
                    var client_element = document.getElementById('ws-id');
                    client_element.innerText = nick;
                    return;
                };                
                var messages = document.getElementById('messages');
                var message = document.createElement('li');
                var content = document.createTextNode(event.data);
                message.appendChild(content);
                messages.appendChild(message);
            };
            function sendMessage(event) {
                var input = document.getElementById("messageText");
                if (input.value.startsWith("/nick ")) {
                    var nick = input.value.replace("/nick ", "").trimRight();
                    var client_element = document.getElementById('ws-id');
                    client_element.innerText = nick;
                };
                ws.send(input.value);
                input.value = '';
                event.preventDefault();
            }
        </script>
    </body>
</html>
"""


class MetaData(BaseModel):
    nick: str = ""
    uuid: UUID4 = Field(default_factory=(uuid.uuid4))


Connections: TypeAlias = dict[WebSocket, MetaData]


class ConnectionManager:
    def __init__(self):
        self.active_connections = Connections()

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        metadata = MetaData()
        self.active_connections[websocket] = metadata
        await websocket.send_text(f"/uuid {metadata.uuid}")
        await self.broadcast(f"{metadata.uuid} entered the chat.", websocket)

    def disconnect(self, websocket: WebSocket):
        self.active_connections.pop(websocket)

    async def send_personal_message(self, message: str, websocket: WebSocket):
        await websocket.send_text(message)

    async def broadcast(self, message: str, exclude: WebSocket | None = None):
        for connection in self.active_connections:
            if exclude and exclude is connection:
                continue
            await connection.send_text(message)


manager = ConnectionManager()


@app.get("/")
async def get():
    return HTMLResponse(html)


@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    nick_set = False
    await manager.connect(websocket)
    metadata = manager.active_connections[websocket]
    nick: str = metadata.uuid.hex

    try:
        while True:
            data = await websocket.receive_text()

            if data.startswith("/nick"):
                new_nick = data.removeprefix("/nick").strip()
                if new_nick:
                    metadata.nick = nick
                nick_set = True

            await manager.send_personal_message(f"You wrote: {data}", websocket)
            msg = f"Client #{nick} says: {data}"
            await manager.broadcast(msg)

            if nick_set:
                nick_set = False
                nick = metadata.nick

    except WebSocketDisconnect:
        manager.disconnect(websocket)
        await manager.broadcast(f"Client #{nick} left the chat")

  • Server vergibt uuids
  • Teilnehmer kann sein Nick ändern
  • keinerlei Überprüfung der Eingabe. Kann beliebig lang sein.
  • schwierig zu erweitern
  • Nachrichten zwischen Client und Server sollten als json ausgetauscht werden.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Antworten