Flask Socket.io Chat | Einige Fragen

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

Hallo zusammen,

ich arbeite nun mit Websockets (Flask-Socketio) um einen Chat zu entwickeln, es soll ein privater Chat sein (immer 2 Personen reden miteinander).

Die Nachrichten speichere ich in einer PostgreSQL DB, hier schonmal das Model:

Code: Alles auswählen

class ChatMessage(Base):
    __tablename__ = 'chatmessages'
    id = Column(Integer, primary_key=True)
    chat_message = Column(Text, nullable=False)
    date_added = Column(DateTime, nullable=False)
    message_seen_by_adressed_user = Column(Boolean, nullable=False, default=False)
    # Foreign Keys
    message_from_user_id = Column(Integer, ForeignKey('users.id'))
    # Foreign Keys
    message_to_user_id = Column(Integer, ForeignKey('users.id'))
Der User selbst hat ebenfalls eine Relation auf das Chat Modell:

Code: Alles auswählen

class User(UserMixin, Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    email = Column(Text, nullable=False, unique=True)
    password = Column(Text, nullable=False)
    chat_room = Column(Text, nullable=False, unique=True)
    # etc.
    # Foreign Keys for chat
    message_from_user_id_addresses = relationship('ChatMessage', backref='message_from', primaryjoin=id==ChatMessage.message_from_user_id)
    message_to_user_id_addresses = relationship('ChatMessage', backref='message_to', primaryjoin=id==ChatMessage.message_to_user_id)
Grundsätzlich hier schon mal die Frage, ist die Modellierung so in Ordnung?

Ich komme mit Socket.io ganz gut zu recht, habe schon einen funktionierenden global chat, und für den private chat hat jeder Nutzer einen unique chat_room string. Dieser wird random generated wenn ein Nutzer sich registriert und ich plane diesen zu nutzen, wenn private messages geschickt werden sollen.

Ich habe gerade Schwierigkeiten die Chats der jeweiligen User korrekt zu laden und zu gruppieren.

Ich kann Chat Nachtichten entweder über die User Relationen laden:

Code: Alles auswählen

    for a_user in current_user.message_from_user_id_addresses:
        print ('sent', a_user.chat_message, 'to', a_user.message_to_user_id)
        
    for a_user in current_user.message_to_user_id_addresses:
        print ('received', a_user.chat_message, 'from', a_user.message_from_user_id)
oder ich query alle chats und filter nach Nachrichten, wo der User Absender oder Empfänger ist:

Code: Alles auswählen

all_messages_user = ChatMessage.query.filter(or_(ChatMessage.message_from_user_id == current_user.id, ChatMessage.message_to_user_id == current_user.id)).order_by(asc('date_added')).all()

for message in all_messages_user:
        print (message.chat_message, 'from', message.message_from_user_id, 'to', message.message_to_user_id)
Ich habe einige Test Nachrichten angelegt:

Bild

Ich möchte nun in der view mit einem for if loop gruppieren können und alle nachrichten zwischen zwei Nutzern laden

D.h. in diesem Fall möchte ich alle Nachrichten von 2 zu 3 und 3 zu 2 seperat haben von den Nachtichten von 1 zu 3 usw., so dass ich in der view die dazugehörigen Userpärchen habe und die Chat History anzeigen kann.

Ich weiß es ist eine lange Frage, falls sich jemand tatsächlich die Zeit nimmt, wäre ich sehr dankbar!

P.S. Ich würde Nachrichten auch nur für einen bestimmten Zeitraum in der DB lassen, zum beispiel 1 Woche, ältere Nachrichten würde ich mit einem Cronjob löschen, so dass es nicht in Zukunft zu Performance Problemen kommt, wenn es mehere tausend nachrichten von einem User gibt
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Zoja: so ganz versteh ich nicht, worauf Du hinauswillst. Wo ist also konkret Dein Problem? Du hast doch schon Deinen Filter, der nur leicht angepasst werden muß:

Code: Alles auswählen

all_messages_user = ChatMessage.query.filter(and_(ChatMessage.message_from_user_id == from_user.id, ChatMessage.message_to_user_id == to_user.id)).order_by(dec('date_added')).limit(30).all()
Ich glaube, Du kannst gar nicht so viele Nachrichten schreiben, als dass Du jemals Performanceprobleme bekommst, wenn Du (message_from_user_id, message_to_user_id und date_added noch in einen Index packst). Datumsstempel sollten generell mit Zeitzoneninformation gespeichert werden, sonst bekommst Du einmal oder zweimal im Jahr Probleme.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

Sirius3 hat geschrieben:@Zoja: so ganz versteh ich nicht, worauf Du hinauswillst. Wo ist also konkret Dein Problem? Du hast doch schon Deinen Filter, der nur leicht angepasst werden muß:

Code: Alles auswählen

all_messages_user = ChatMessage.query.filter(and_(ChatMessage.message_from_user_id == from_user.id, ChatMessage.message_to_user_id == to_user.id)).order_by(dec('date_added')).limit(30).all()
Ich glaube, Du kannst gar nicht so viele Nachrichten schreiben, als dass Du jemals Performanceprobleme bekommst, wenn Du (message_from_user_id, message_to_user_id und date_added noch in einen Index packst). Datumsstempel sollten generell mit Zeitzoneninformation gespeichert werden, sonst bekommst Du einmal oder zweimal im Jahr Probleme.
Was sind denn from_user.id und to_user.id? Die sind undefined.

Warum bekomme ich Probleme bei timestamp without timezone? Ich nutzde das überall.

Danke!

P.S. das query problem habe ich bereits gelöst. Ich übergebe die query results gar nicht in den view, vorher baue ich mir ein JSON objekt mit allen werten, die ich brauche und gebe dieses in die view. In der view verteile ich die pärchen zu den richtigen divs dann mit jQuery, indem ich das JSON Objekt parse.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Du fragst doch:
Zoja hat geschrieben:möchte ich alle Nachrichten von 2 zu 3 und 3 zu 2 seperat haben
daraus sollte doch klar sein, was from_user und to_user sind.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

Sirius3 hat geschrieben:Du fragst doch:
Zoja hat geschrieben:möchte ich alle Nachrichten von 2 zu 3 und 3 zu 2 seperat haben
daraus sollte doch klar sein, was from_user und to_user sind.
Ja das ist mir klas, es muss aber dynamisch alle Pärchen selecten. Auf der Chat Page habe ich nicht nur die Kommunikation von user 3 und 2, sondern von allen usern, die mit current_user kommunizieren. Diese sollen gruppiert sein, so dass ich die jeweiligen Chats anzeigen/verstecken kann.

Außerdem sollen die Pärchen in der view eindeutig identifizierbar sein, weil neue Nachrichten mit via Socket.io kommen, die response muss dann das richtige div finden und dort die neue nachricht appenden.

Hier ein Beispiel. Ich bin jetzt user 3.
Ich kann zwischen den nachrichten mit user 2 und user 1 switchen:

Bild
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Zoja: also ist das eine JavaScript-Frage und keine Python-Frage. Dann mußt Du in JavaScript alle Nachrichten nach den anderen Usern sortieren und dem jeweiligen DIV hinzufügen.

Bevor Du aber anfängst, hier mit jQuery das DOM zu manipulieren, solltest Du Dir Vue anschauen.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

Sirius3 hat geschrieben:@Zoja: also ist das eine JavaScript-Frage und keine Python-Frage. Dann mußt Du in JavaScript alle Nachrichten nach den anderen Usern sortieren und dem jeweiligen DIV hinzufügen.

Bevor Du aber anfängst, hier mit jQuery das DOM zu manipulieren, solltest Du Dir Vue anschauen.
Okay vielen Dank, das hilft mir schon. Dann bin ich auf dem richtigen Weg.

Kannst du mir nochmal erklären warum timestamps ohne Zeitzome schlecht sind und wie ich probleme kriegen kann? Ich hab dazu noch nichts gefunden.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Zoja: zweimal im Jahr wird von Sommerzeit auf Winterzeit und umgekehrt gewechselt. Im einen Fall wird von 2 Uhr auf 3 Uhr gesprungen, so dass alle Zeitdifferenzen über diese Grenze hinweg falsch sind. Das kann man schön an diesem Forum sehen. Wo ich mal um 18:20Uhr registriert habe, und mal um 19:20Uhr. Im Herbst ist es dann noch schlimmer, da dann bestimmte Zeiten doppelt auftreten. Eine Nachricht die um 2:40Uhr SZ geschickt taucht dann später auf als eine Nachricht 2:25Uhr WZ. Da kann man sich bestimmt einen lustigen Gesprächsverlauf ausdenken, wo im nachhinein das Gegenteil gesagt wurde, als ursprünglich, weil die Nachrichten zeitlich neu gemischt wurden.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

Sirius3 hat geschrieben:@Zoja: zweimal im Jahr wird von Sommerzeit auf Winterzeit und umgekehrt gewechselt. Im einen Fall wird von 2 Uhr auf 3 Uhr gesprungen, so dass alle Zeitdifferenzen über diese Grenze hinweg falsch sind. Das kann man schön an diesem Forum sehen. Wo ich mal um 18:20Uhr registriert habe, und mal um 19:20Uhr. Im Herbst ist es dann noch schlimmer, da dann bestimmte Zeiten doppelt auftreten. Eine Nachricht die um 2:40Uhr SZ geschickt taucht dann später auf als eine Nachricht 2:25Uhr WZ. Da kann man sich bestimmt einen lustigen Gesprächsverlauf ausdenken, wo im nachhinein das Gegenteil gesagt wurde, als ursprünglich, weil die Nachrichten zeitlich neu gemischt wurden.
Okay vielen Dank. Wie Schlimm ist das nun? D.h. wenn die Angaben in der Winterzeit sind, werden sie für die Dauer der Sommerzeit falsch sein, wenn es wieder zur Winterzeit geht richtig. Im großen und Ganzen halbes Jahr korrekt und halbes Jahr um eine Stunde falsch.

Warum nutzt man dann nicht immer mit Zeitzone? Wann ist der Gebrauch des timestamps ohne Zeitzone sinnvoll?
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Zoja hat geschrieben:Warum nutzt man dann nicht immer mit Zeitzone?
Das ist eine gute Frage.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Zoja hat geschrieben: Okay vielen Dank. Wie Schlimm ist das nun? D.h. wenn die Angaben in der Winterzeit sind, werden sie für die Dauer der Sommerzeit falsch sein, wenn es wieder zur Winterzeit geht richtig. Im großen und Ganzen halbes Jahr korrekt und halbes Jahr um eine Stunde falsch.

Warum nutzt man dann nicht immer mit Zeitzone? Wann ist der Gebrauch des timestamps ohne Zeitzone sinnvoll?
Ich habe das Problem das mein iPhone manchmal Nachrichten falsch rum sortiert. Antworten stehen vor Fragen. Das nervt ziemlich. Wenn das ganze Projekt nur ein Testballon ist, kannst du das natuerlich ignorieren. Aber es richtig zu machen um sich mit dem Problem mal wirklich auseinander zu setzen hilft, es in der Zukunft bei einem aehnlichen Thema eben gleich richtig zu machen oder dafuer sensiblisiert zu sein. Womit wir bei der Antwort auf deine Frage waeren: genauso wie bei Encodings und vielen anderen Themen ist es schlichte Unkenntnis. Es ist halt nichts, was so ohne weiteres beim entwickeln und testen auffaellt, und irgendwann kracht's dann aus mysterioesen Gruenden.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

UPDATE:

Verstehe ich folgendes richtig?

Ich möchte nun, dass ein Nutzer eine notification bekommt, dass er angeschrieben wurde und zwar egal auf welcher Unterseite sich der Nutzer befindet. So wie ich das verstehe, müsste der socket dann auf jeder Seite offen sein und der Nutzer zum Chat connected sein. Nur so kann man das server event dann kriegen.

Zurzeit habe ich das so, dass der Chat NUR auf der profil/chat Unterseite funktioniert, wenn jemand die Seite schließt, dann schließt der socket und der user ist disconnected.

Ist es also nötig den Socket die ganze Zeit aufzulassen für die neue Funktionalität, die ich haben will? Oder gibt es einen klügere Lösung? Ich meine gunicorn mit gevent schaffen bis zu 1000 gleichzeitige sockets, aber mehr geht nicht. Das ist zwar schon ne ganze Menge, aber mich würde interessieren was man sonst machen kann?

Oder können gevent / gunicorn mehr als 1k sockets handeln mit mehr workern? Ich meine ich hätte gelesen mehr geht nicht.
Der Heroku procfile gerade:

Code: Alles auswählen

web: gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 main:app
würde -w 2 bedeuten bis zu 2k connections?
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Zoja: aus diesem Grund macht man Single-Page-Anwendungen. Linux limitiert die Anzahl offener Filehandles, so dass mehr als 1000 Verbindungen daran scheitern. Mehr Worker bedeutet dann auch mehr Filehandles.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

Sirius3 hat geschrieben:@Zoja: aus diesem Grund macht man Single-Page-Anwendungen. Linux limitiert die Anzahl offener Filehandles, so dass mehr als 1000 Verbindungen daran scheitern. Mehr Worker bedeutet dann auch mehr Filehandles.
Was würde denn eine Single Page Anwendung ändern? Es würden doch dann auch alle User gleichzeitig connected sein. Es macht doch kein Unterschied ob der User sich auf Unterseite A und Unterseite B connected, oder er ist Auf Unterseite A connected, aber der content der Unterseite B wird ausgetauscht/nachgeladen.

Das mit mehr worker und filehanlder verstehe ich nicht ganz, würde dass also klappen mehr als 1 k connections zu haben mit mehr workern?
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Das c10k Problem, also 10000 Verbindungen gleichzeitig zu haben ist schon eine Weile gelöst, bei 1000 Verbindungen ist man also noch lange nicht am Limit.

Grundsätzlich würde ich aber einfach empfehlen keine Websockets zu nutzen sondern einfach alle Clients pollen zu lassen, fragen die alle den Server nach updates einmal pro Sekunde dürfte die Latenz aus User Sicht durchaus akzeptabel sein und es ist viel einfacher damit serverseitig klar zu kommen.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Einmal pro Sekunde ist ziemlich langsam aus client Sicht, und gleichzeitig 10000 Verbindungsanfragen pro Sekunde für den Server. Worin liegt der Vorteil gegenüber der Aufrechterhaltung dieser Verbindungen? Ob gunicorn das kann steht auf einem anderen Blatt.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@__deets__: Das Problem liegt nicht an gunicorn. CPU und RAM kommen locker mit ein paar Hundertausend Verbindungen klar. Auf der einen Seite ist es eine Hardware-Frage (wie viele Pakete kann ich pro Sekunde durch die Leitung quetschen) und eine Resourcenfrage des Linuxkernels (wie viele offene Verbindungen können in den internen Tabellen gehalten werden). Zweiteres kann man durch Tricks hochtreiben, bei ersterem taugt die Hardware für den Hausgebrauch nichts und man muß zu den teuren Karten greifen.
Zoja
User
Beiträge: 145
Registriert: Freitag 28. Februar 2014, 14:04

DasIch hat geschrieben:Das c10k Problem, also 10000 Verbindungen gleichzeitig zu haben ist schon eine Weile gelöst, bei 1000 Verbindungen ist man also noch lange nicht am Limit.

Grundsätzlich würde ich aber einfach empfehlen keine Websockets zu nutzen sondern einfach alle Clients pollen zu lassen, fragen die alle den Server nach updates einmal pro Sekunde dürfte die Latenz aus User Sicht durchaus akzeptabel sein und es ist viel einfacher damit serverseitig klar zu kommen.
So etwas habe ich mir gestern Abend auch überlegt. Ich könnte auf jeder Unterseite außer auf der Chat Seite eine AJAX Funktion einbauen, die alle 10 Sekunden ausgeführt wird und checkt, ob es ungelesene Nachrichten gibt. Falls ja, wird irgendwo eine Notification angezeigt, falls nein wird die Notification removed oder erst gar nicht angezeigt.

Ist dieser Ansatz nun besser / effizienter als mehrere Sockets auf zu haben? Muss man hier für nicht multithreaden? Ich nutze Flask und so weit ich weiß ist es nicht threaded.

Ich denke aber, dass es viel länger dauern wird mit diesem Ansatz, bis man schlechte Performance hat. Ich meine 1k Sockets ist auch schon eine ganze Menge. Man muss es erstmal schaffen 1k User gleichzeitig online zu haben. Für das AJAX polling sollte es doch auch weit über 1k gleichzeitgen Usern leicht sein alle 10 sekunden den Server anzufragen, oder?
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sirius3 hat geschrieben:@__deets__: Das Problem liegt nicht an gunicorn. CPU und RAM kommen locker mit ein paar Hundertausend Verbindungen klar. Auf der einen Seite ist es eine Hardware-Frage (wie viele Pakete kann ich pro Sekunde durch die Leitung quetschen) und eine Resourcenfrage des Linuxkernels (wie viele offene Verbindungen können in den internen Tabellen gehalten werden). Zweiteres kann man durch Tricks hochtreiben, bei ersterem taugt die Hardware für den Hausgebrauch nichts und man muß zu den teuren Karten greifen.
Das kann ich nachvollziehen. Mir ging es um den Kontrast zwischen diese 10000 Verbindungen offen zu halten, was zwar internes Housekeeping bedeutet, aber zb keine Interruptfluten für eingehende Verbindungsanfragen erzeugt, und dem permanenten Pollen. Das das besser verkraftet wird kann ich mir schwer vorstellen.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

__deets__ hat geschrieben:Einmal pro Sekunde ist ziemlich langsam aus client Sicht, und gleichzeitig 10000 Verbindungsanfragen pro Sekunde für den Server. Worin liegt der Vorteil gegenüber der Aufrechterhaltung dieser Verbindungen? Ob gunicorn das kann steht auf einem anderen Blatt.
Der Vorteil ist dass du die Verbindungen wesentlich leichter über einen Load Balancer auf mehrere Server verteilen kannst. Deployments werden einfacher weil du dir keine Gedanken darüber machen musst dass Verbindungen verloren werden usw.
Antworten