Mit Redis Pub/Sub in Echtzeit gehen

Veröffentlicht: 2022-03-11

Die Skalierung einer Web-App ist fast immer eine interessante Herausforderung, unabhängig von der damit verbundenen Komplexität. Echtzeit-Web-Apps werfen jedoch einzigartige Skalierbarkeitsprobleme auf. Um beispielsweise eine Messaging-Webanwendung, die WebSockets zur Kommunikation mit ihren Clients verwendet, horizontal skalieren zu können, muss sie alle ihre Serverknoten irgendwie synchronisieren. Wenn die App nicht von Grund auf mit diesem Gedanken entwickelt wurde, ist die horizontale Skalierung möglicherweise keine einfache Option.

In diesem Artikel gehen wir durch die Architektur einer einfachen Echtzeit-Bildfreigabe- und Messaging-Web-App. Hier konzentrieren wir uns auf die verschiedenen Komponenten wie Redis Pub/Sub, die am Aufbau einer Echtzeit-App beteiligt sind, und sehen, wie sie alle ihre Rolle in der Gesamtarchitektur spielen.

Mit Redis Pub/Sub in Echtzeit gehen

Mit Redis Pub/Sub in Echtzeit gehen
Twittern

In Bezug auf die Funktionalität ist die Anwendung sehr leicht. Es ermöglicht das Hochladen von Bildern und Echtzeit-Kommentaren zu diesen Bildern. Darüber hinaus kann jeder Benutzer auf das Bild tippen und andere Benutzer können einen Welleneffekt auf ihrem Bildschirm sehen.

Der gesamte Quellcode dieser App ist auf GitHub verfügbar.

Dinge, die wir brauchen

gehen

Wir verwenden die Programmiersprache Go. Es gibt keinen besonderen Grund, warum wir uns für diesen Artikel für Go entschieden haben, abgesehen davon, dass Gos Syntax sauber und seine Semantik leichter zu verstehen ist. Und dann gibt es natürlich die Voreingenommenheit des Autors. Alle in diesem Artikel behandelten Konzepte können jedoch problemlos in die Sprache Ihrer Wahl übersetzt werden.

Der Einstieg in Go ist einfach. Die Binärdistribution kann von der offiziellen Website heruntergeladen werden. Falls Sie Windows verwenden, finden Sie auf der Download-Seite ein MSI-Installationsprogramm für Go. Oder, falls Ihr Betriebssystem (zum Glück) einen Paketmanager anbietet:

Arch-Linux:

 pacman -S go

Ubuntu:

 apt-get install golang

Mac OS X:

 brew install go

Dieser funktioniert nur, wenn wir Homebrew installiert haben.

MongoDB

Warum MongoDB verwenden, wenn wir Redis haben, fragen Sie? Wie bereits erwähnt, ist Redis ein In-Memory-Datenspeicher. Obwohl es Daten auf der Festplatte speichern kann, ist die Verwendung von Redis für diesen Zweck wahrscheinlich nicht der beste Weg. Wir werden MongoDB verwenden, um hochgeladene Bildmetadaten und Nachrichten zu speichern.

Wir können MongoDB von ihrer offiziellen Website herunterladen. In einigen Linux-Distributionen ist dies die bevorzugte Methode zur Installation von MongoDB. Es sollte dennoch mit den Paketmanagern der meisten Distributionen installierbar sein.

Arch-Linux:

 pacman -S mongodb

Ubuntu:

 apt-get install mongodb

Mac OS X:

 brew install mongodb

In unserem Go-Code verwenden wir das Paket mgo (ausgesprochen mango). Es ist nicht nur kampferprobt, das Treiberpaket bietet auch eine wirklich saubere und einfache API.

Wenn Sie kein MongoDB-Experte sind, machen Sie sich keine Sorgen. Die Verwendung dieses Datenbankdienstes ist in unserer Beispiel-App minimal und für den Schwerpunkt dieses Artikels fast irrelevant: Pub/Sub-Architektur.

Amazon S3

Wir werden Amazon S3 verwenden, um die vom Benutzer hochgeladenen Bilder zu speichern. Hier gibt es nicht viel zu tun, außer sicherzustellen, dass wir ein Amazon Web Services-fähiges Konto und einen temporären Bucket erstellt haben.

Das Speichern der hochgeladenen Dateien auf einer lokalen Festplatte ist keine Option, da wir uns in keiner Weise auf die Identität unserer Webknoten verlassen wollen. Wir möchten, dass die Benutzer sich mit jedem der verfügbaren Webknoten verbinden können und trotzdem denselben Inhalt sehen können.

Um mit dem Amazon S3-Bucket aus unserem Go-Code zu interagieren, verwenden wir AdRoll/goamz, einen Fork des goamz-Pakets von Canonical mit einigen Unterschieden.

Redis

Zu guter Letzt: Redis. Wir können es mit dem Paketmanager unserer Distribution installieren:

Arch-Linux:

 pacman -S redis

Ubuntu:

 apt-get install redis-server

Mac OS X:

 brew install redis

Oder holen Sie sich den Quellcode und kompilieren Sie ihn selbst. Redis hat keine anderen Abhängigkeiten als GCC und libc, um es zu erstellen:

 wget http://download.redis.io/redis-stable.tar.gz tar xvzf redis-stable.tar.gz cd redis-stable make

Sobald Redis installiert ist und ausgeführt wird, starten Sie ein Terminal und geben Sie die CLI von Redis ein:

 redis-cli

Versuchen Sie, die folgenden Befehle einzugeben, und prüfen Sie, ob Sie die erwartete Ausgabe erhalten:

 SET answer 41 INCR answer GET answer

Der erste Befehl speichert „41“ für die Taste „Antwort“, der zweite Befehl erhöht den Wert, der dritte Befehl gibt den gespeicherten Wert für die angegebene Taste aus. Das Ergebnis sollte „42“ lauten.

Auf der offiziellen Website von Redis erfahren Sie mehr über alle Befehle, die Redis unterstützt.

Wir werden das Go-Paket redigo verwenden, um von unserem App-Code aus eine Verbindung zu Redis herzustellen.

Werfen Sie einen Blick auf Redis Pub/Sub

Das Publish-Subscribe-Muster ist eine Möglichkeit, Nachrichten an eine beliebige Anzahl von Absendern weiterzuleiten. Die Absender dieser Nachrichten (Publisher) geben die Zielempfänger nicht explizit an. Stattdessen werden die Nachrichten auf einem Kanal versendet, auf dem beliebig viele Empfänger (Abonnenten) auf sie warten können.

Einfache Publish-Subscribe-Konfiguration

In unserem Fall können hinter einem Load Balancer beliebig viele Webknoten laufen. Zu einem bestimmten Zeitpunkt können zwei Benutzer, die dasselbe Bild betrachten, nicht mit demselben Knoten verbunden sein. Hier kommt Redis Pub/Sub ins Spiel. Immer wenn ein Webknoten eine Änderung feststellen muss (z. B. wenn eine neue Nachricht vom Benutzer erstellt wird), verwendet er Redis Pub/Sub, um diese Informationen an alle relevanten Webknoten zu senden. Die wiederum werden die Informationen an die relevanten Clients weitergeben, damit sie die aktualisierte Liste von Nachrichtenredis abrufen können.

Da das Publish-Subscribe-Muster es uns ermöglicht, Nachrichten auf benannten Kanälen zu versenden, können wir jeden Webknoten mit Redis verbinden und nur die Kanäle abonnieren, an denen ihre verbundenen Benutzer interessiert sind. Wenn beispielsweise zwei Benutzer beide die dasselbe Bild haben, aber mit zwei verschiedenen Webknoten von vielen Webknoten verbunden sind, dann müssen nur diese beiden Webknoten den entsprechenden Kanal abonnieren. Jede auf diesem Kanal veröffentlichte Nachricht wird nur an diese beiden Webknoten übermittelt.

Klingt zu gut um wahr zu sein? Wir können es mit der CLI von Redis ausprobieren. Starten Sie drei Instanzen von redis-cli . Führen Sie zunächst den folgenden Befehl aus:

 SUBSCRIBE somechannel

Führen Sie den folgenden Befehl in der zweiten Redis-CLI-Instanz aus:

 SUBSCRIBE someotherchannel

Führen Sie die folgenden Befehle in der dritten Instanz von Redis CLI aus:

 PUBLISH somechannel lorem PUBLISH someotherchannel ipsum

Beachten Sie, wie die erste Instanz „lorem“, aber nicht „ipsum“ erhielt, und wie die zweite Instanz „ipsum“, aber nicht „lorem“ erhielt.

Redis Pub/Sub in Aktion

Es ist erwähnenswert, dass ein Redis-Client, sobald er in den Abonnentenmodus wechselt, keine anderen Vorgänge mehr ausführen kann, als mehr Kanäle zu abonnieren oder abonnierte Kanäle abzubestellen. Das bedeutet, dass jeder Webknoten zwei Verbindungen zu Redis aufrechterhalten muss, eine, um sich als Abonnent mit Redis zu verbinden, und die andere, um Nachrichten auf Kanälen zu veröffentlichen, damit jeder Webknoten, der diese Kanäle abonniert hat, sie empfangen kann.

Echtzeit und skalierbar

Bevor wir anfangen zu untersuchen, was hinter den Kulissen vor sich geht, lassen Sie uns das Repository klonen:

 mkdir tonesa cd tonesa export GOPATH=`pwd` mkdir -p src/github.com/hjr265/tonesa cd src/github.com/hjr265/tonesa git clone https://github.com/hjr265/tonesa.git . go get ./...

… und kompilieren:

 go build ./cmd/tonesad

Um die App auszuführen, erstellen Sie zunächst eine Datei namens .env (am besten durch Kopieren der Datei env-sample.txt):

 cp env-sample.txt .env

Füllen Sie die .env-Datei mit allen erforderlichen Umgebungsvariablen aus:

 MONGO_URL=mongodb://127.0.0.1/tonesa REDIS_URL=redis://127.0.0.1 AWS_ACCESS_KEY_ID={Your-AWS-Access-Key-ID-Goes-Here} AWS_SECRET_ACCESS_KEY={And-Your-AWS-Secret-Access-Key} S3_BUCKET_NAME={And-S3-Bucket-Name}

Führen Sie schließlich die erstellte Binärdatei aus:

 PORT=9091 ./tonesad -env-file=.env

Der Webknoten sollte nun laufen und über http://localhost:9091 erreichbar sein.

Live-Beispiel

Um zu testen, ob es auch bei horizontaler Skalierung funktioniert, können Sie mehrere Webknoten hochfahren, indem Sie es mit unterschiedlichen Portnummern starten:

 PORT=9092 ./tonesad -env-file=.env
 PORT=9093 ./tonesad -env-file=.env

… und über die entsprechenden URLs darauf zugreifen: http://localhost:9092 und http://localhost:9093.

Live-Beispiel

Hinter den Kulissen

Anstatt jeden Schritt in der Entwicklung der App durchzugehen, werden wir uns auf einige der wichtigsten Teile konzentrieren. Obwohl nicht alle davon zu 100 % für Redis Pub/Sub und seine Echtzeitauswirkungen relevant sind, sind sie dennoch für die Gesamtstruktur der App relevant und werden es einfacher machen, ihnen zu folgen, wenn wir tiefer eintauchen.

Um die Dinge einfach zu halten, werden wir uns nicht um die Benutzerauthentifizierung kümmern. Uploads sind anonym und für jeden verfügbar, der die URL kennt. Alle Zuschauer können Nachrichten senden und haben die Möglichkeit, ihren eigenen Alias ​​auszuwählen. Das Anpassen des richtigen Authentifizierungsmechanismus und der Datenschutzfunktionen sollte trivial sein und würde den Rahmen dieses Artikels sprengen.

Persistente Daten

Dieser ist einfach.

Immer wenn ein Benutzer ein Bild hochlädt, speichern wir es in Amazon S3 und speichern dann den Pfad dazu in MongoDB gegen zwei IDs: eine BSON-Objekt-ID (MongoDBs Favorit) und eine weitere kurze 8-Zeichen lange ID (etwas angenehm für die Augen). Dies geht in die „Uploads“-Sammlung unserer Datenbank und hat eine Struktur wie diese:

 type Upload struct { ID bson.ObjectId `bson:"_id"` ShortID string `bson:"shortID"` Kind Kind `bson:"kind"` Content Blob `bson:"content"` CreatedAt time.Time `bson:"createdAt"` ModifiedAt time.Time `bson:"modifiedAt"` } type Blob struct { Path string `bson:"path"` Size int64 `bson:"size"` }

Das Feld Art wird verwendet, um anzugeben, welche Art von Medien dieser „Upload“ enthält. Bedeutet das, dass wir andere Medien als Bilder unterstützen? Unglücklicherweise nicht. Aber das Feld wurde dort belassen, um daran zu erinnern, dass wir hier nicht unbedingt auf Bilder beschränkt sind.

Wenn Benutzer einander Nachrichten senden, werden sie in einer anderen Sammlung gespeichert. Ja, Sie haben es erraten: „Nachrichten“.

 type Message struct { ID bson.ObjectId `bson:"_id"` UploadID bson.ObjectId `bson:"uploadID"` AuthorName string `bson:"anonName"` Content string `bson:"content"` CreatedAt time.Time `bson:"createdAt"` ModifiedAt time.Time `bson:"modifiedAt"` }

Das einzig Interessante hier ist das UploadID-Feld, das verwendet wird, um Nachrichten einem bestimmten Upload zuzuordnen.

API-Endpunkte

Diese Anwendung hat im Wesentlichen drei Endpunkte.

POST /api/uploads

Der Handler für diesen Endpunkt erwartet eine „multipart/form-data“-Übermittlung mit dem Bild im „file“-Feld. Das Verhalten des Handlers ist ungefähr wie folgt:

 func HandleUploadCreate(w http.ResponseWriter, r *http.Request) { f, h, _ := r.FormFile("file") b := bytes.Buffer{} n, _ := io.Copy(&b, io.LimitReader(f, data.MaxUploadContentSize+10)) if n > data.MaxUploadContentSize { ServeBadRequest(w, r) return } id := bson.NewObjectId() upl := data.Upload{ ID: id, Kind: data.Image, Content: data.Blob{ Path: "/uploads/" + id.Hex(), Size: n, }, } data.Bucket.Put(upl.Content.Path, b.Bytes(), h.Header.Get("Content-Type"), s3.Private, s3.Options{}) upl.Put() // Respond with newly created upload entity (JSON encoded) }

Go erfordert, dass alle Fehler explizit behandelt werden. Dies wurde im Prototyp durchgeführt, wird jedoch in den Snippets in diesem Artikel weggelassen, um den Fokus auf die kritischen Teile zu lenken.

Im Handler dieses API-Endpunkts lesen wir im Wesentlichen die Datei, beschränken ihre Größe jedoch auf einen bestimmten Wert. Übersteigt der Upload diesen Wert, wird die Anfrage abgelehnt. Andernfalls wird eine BSON-ID generiert und verwendet, um das Bild in Amazon S3 hochzuladen, bevor die Upload-Entität in MongoDB gespeichert wird.

Die Art und Weise, wie BSON-Objekt-IDs generiert werden, hat Vor- und Nachteile. Sie werden auf der Client-Seite generiert. Die zur Generierung von Objekt-IDs verwendete Strategie macht die Kollisionswahrscheinlichkeit jedoch so gering, dass es sicher ist, sie auf Client-Seite zu generieren. Andererseits sind die Werte der generierten Objekt-IDs normalerweise sequentiell, und das ist etwas, was Amazon S3 nicht besonders mag. Eine einfache Problemumgehung besteht darin, dem Dateinamen eine zufällige Zeichenfolge voranzustellen.

GET /api/uploads/{id}/messages

Diese API wird verwendet, um aktuelle Nachrichten und Nachrichten abzurufen, die nach einer bestimmten Zeit gepostet wurden.

 func ServeMessageList(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) idStr := vars["id"] if !bson.IsObjectIdHex(idStr) { ServeNotFound(w, r) return } upl, _ := data.GetUpload(bson.ObjectIdHex(idStr)) if upl == nil { ServeNotFound(w, r) return } sinceStr := r.URL.Query().Get("since") var msgs []data.Message if sinceStr != "" { since, _ := time.Parse(time.RFC3339, sinceStr) msgs, _ = data.ListMessagesByUploadID(upl.ID, since, 16) } else { msgs, _ = data.ListRecentMessagesByUploadID(upl.ID, 16) } // Respond with message entities (JSON encoded) }

Wenn der Browser eines Benutzers über eine neue Nachricht zu einem Upload benachrichtigt wird, den der Benutzer gerade ansieht, ruft er die neuen Nachrichten über diesen Endpunkt ab.

POST /api/uploads/{id}/messages

Und schließlich der Handler, der Nachrichten erstellt und alle benachrichtigt:

 func HandleMessageCreate(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) idStr := vars["id"] if !bson.IsObjectIdHex(idStr) { ServeNotFound(w, r) return } upl, _ := data.GetUpload(bson.ObjectIdHex(idStr)) if upl == nil { ServeNotFound(w, r) return } body := Message{} json.NewDecoder(r.Body).Decode(&body) msg := data.Message{} msg.UploadID = upl.ID msg.AuthorName = body.AuthorName msg.Content = body.Content msg.Put() // Respond with newly created message entity (JSON encoded) hub.Emit("upload:"+upl.ID.Hex(), "message:"+msg.ID.Hex()) }

Dieser Handler ist den anderen so ähnlich, dass es fast langweilig ist, ihn hier überhaupt aufzunehmen. Oder ist es? Beachten Sie den Funktionsaufruf hub.Emit() ganz am Ende der Funktion. Was ist Hub, sagst du? Dort passiert die ganze Pub/Sub-Magie.

Hub: Wo WebSockets auf Redis treffen

Hub ist der Ort, an dem wir WebSockets mit den Pub/Sub-Kanälen von Redis verbinden. Und zufälligerweise heißt das Paket, das wir verwenden, um WebSockets auf unseren Webservern zu handhaben, glue.

Hub verwaltet im Wesentlichen einige Datenstrukturen, die eine Zuordnung zwischen allen verbundenen WebSockets zu allen Kanälen erstellen, an denen sie interessiert sind. Beispielsweise sollte ein WebSocket auf dem Browser-Tab des Benutzers, der auf ein bestimmtes hochgeladenes Bild zeigt, natürlich an allen relevanten Benachrichtigungen interessiert sein dazu.

Das Hub-Paket implementiert sechs Funktionen:

  • Abonnieren
  • AbmeldenAlle
  • Emittieren
  • EmitLocal
  • InitHub
  • HandleSocket

Abonnieren und AbbestellenAlle

 func Subscribe(s *glue.Socket, t string) error { l.Lock() defer l.Unlock() _, ok := sockets[s] if !ok { sockets[s] = map[string]bool{} } sockets[s][t] = true _, ok = topics[t] if !ok { topics[t] = map[*glue.Socket]bool{} err := subconn.Subscribe(t) if err != nil { return err } } topics[t][s] = true return nil }

Diese Funktion hält, genau wie die meisten anderen in diesem Paket, eine Sperre für einen Lese-/Schreib-Mutex, während er ausgeführt wird. Auf diese Weise können wir die primitiven Datenstrukturen, Variablen, Sockets und Themen sicher ändern. Die erste Variable, sockets , ordnet Sockets Channel-Namen zu, während die zweite, themen , Channel-Namen Sockets zuordnet. In dieser Funktion erstellen wir diese Zuordnung. Wann immer wir sehen, dass socket einen neuen Kanalnamen abonniert, stellen wir unsere Redis-Verbindung subconn her und abonnieren diesen Kanal auf Redis mit subconn.Subscribe . Dadurch leitet Redis alle Benachrichtigungen auf diesem Kanal an diesen Webknoten weiter.

Und ebenso reißen wir in der UnsubscribeAll- Funktion das Mapping herunter:

 func UnsubscribeAll(s *glue.Socket) error { l.Lock() defer l.Unlock() for t := range sockets[s] { delete(topics[t], s) if len(topics[t]) == 0 { delete(topics, t) err := subconn.Unsubscribe(t) if err != nil { return err } } } delete(sockets, s) return nil }

Wenn wir den letzten Socket aus der Datenstruktur entfernen, der an einem bestimmten Kanal interessiert ist, kündigen wir den Kanal in Redis mit subconn.Unsubscribe .

Emittieren

 func Emit(t string, m string) error { _, err := pubconn.Do("PUBLISH", t, m) return err }

Diese Funktion veröffentlicht eine Nachricht m auf Kanal t unter Verwendung der Veröffentlichungsverbindung zu Redis.

EmitLocal

 func EmitLocal(t string, m string) { l.RLock() defer l.RUnlock() for s := range topics[t] { s.Write(m) } }

InitHub

 func InitHub(url string) error { c, _ := redis.DialURL(url) pubconn = c c, _ = redis.DialURL(url) subconn = redis.PubSubConn{c} go func() { for { switch v := subconn.Receive().(type) { case redis.Message: EmitLocal(v.Channel, string(v.Data)) case error: panic(v) } } }() return nil }

In der InitHub- Funktion erstellen wir zwei Verbindungen zu Redis: eine zum Abonnieren der Kanäle, an denen dieser Webknoten interessiert ist, und die andere zum Veröffentlichen von Nachrichten. Sobald die Verbindungen hergestellt sind, starten wir eine neue Go-Routine mit einer endlos laufenden Schleife, die darauf wartet, Nachrichten über die Abonnentenverbindung zu Redis zu erhalten. Jedes Mal, wenn es eine Nachricht empfängt, sendet es sie lokal (dh an alle WebSockets, die mit diesem Webknoten verbunden sind).

HandleSocket

Und schließlich ist HandleSocket der Ort, an dem wir darauf warten, dass Nachrichten durch WebSockets kommen oder aufräumen, nachdem die Verbindung geschlossen wurde:

 func HandleSocket(s *glue.Socket) { s.OnClose(func() { UnsubscribeAll(s) }) s.OnRead(func(data string) { fields := strings.Fields(data) if len(fields) == 0 { return } switch fields[0] { case "watch": if len(fields) != 2 { return } Subscribe(s, fields[1]) case "touch": if len(fields) != 4 { return } Emit(fields[1], "touch:"+fields[2]+","+fields[3]) } }) }

Front-End-JavaScript

Da Glue mit einer eigenen Front-End-JavaScript-Bibliothek geliefert wird, ist es viel einfacher, WebSockets zu handhaben (oder auf XHR-Polling zurückzugreifen, wenn WebSockets nicht verfügbar sind):

 var socket = glue() socket.onMessage(function(data) { data = data.split(':') switch(data[0]) { case 'message': messages.fetch({ data: { since: _.first(messages.pluck('createdAt')) || '' }, add: true, remove: false }) break case 'touch': var coords = data[1].split(',') showTouchBubble(coords) break } }) socket.send('watch upload:'+upload.id)

Auf der Clientseite hören wir auf jede Nachricht, die über WebSocket eingeht. Da Glue alle Nachrichten als Strings überträgt, kodieren wir alle darin enthaltenen Informationen nach bestimmten Mustern:

  • Neue Nachricht: „message:{messageID}“
  • Klicken Sie auf das Bild: „touch:{coordX},{coordY}“, wobei coordX und coordY die prozentualen Koordinaten der Klickposition des Benutzers auf dem Bild sind

Wenn der Benutzer eine neue Nachricht erstellt, verwenden wir die API „POST /api/uploads/{uploadID}/messages“, um eine neue Nachricht zu erstellen. Dies geschieht mit der create- Methode in der Backbone-Sammlung für Nachrichten:

 messages.create({ authorName: $messageAuthorNameEl.val(), content: $messageContentEl.val(), createdAt: '' }, { at: 0 })

Wenn der Benutzer auf das Bild klickt, berechnen wir die Position des Klicks in Prozent der Breite und Höhe des Bildes und senden die Informationen direkt über den WebSocket.

 socket.send('touch upload:'+upload.id+' '+(event.pageX - offset.left) / $contentImgEl.width()+' '+(event.pageY - offset.top) / $contentImgEl.height())

Der Überblick

Überblick über die Architektur der Anwendung

Wenn der Benutzer eine Nachricht eingibt und die Eingabetaste drückt, ruft der Client den API-Endpunkt „POST /api/uploads/{id}/messages“ auf. Dies wiederum erstellt eine Nachrichtenentität in der Datenbank und veröffentlicht einen String „message:{messageID}“ über Redis Pub/Sub auf dem Kanal „upload:{uploadID}“ durch das Hub-Paket.

Redis leitet diesen String an jeden Webknoten (Abonnenten) weiter, der am Kanal „upload:{uploadID}“ interessiert ist. Webknoten, die diesen String empfangen, iterieren über alle für den Kanal relevanten WebSockets und senden den String über ihre WebSocket-Verbindungen an den Client. Clients, die diese Zeichenfolge erhalten, beginnen mit dem Abrufen neuer Nachrichten vom Server mithilfe von „GET /api/uploads/{id}/messages“.

In ähnlicher Weise sendet der Client zur Weitergabe von Klickereignissen auf dem Bild direkt eine Nachricht über den WebSocket, die etwa so aussieht: „touch upload:{uploadID} {coordX} {coordY}“. Diese Nachricht landet im Hub-Paket, wo sie auf demselben Kanal „upload:{uploadID}“ veröffentlicht wird. Als Ergebnis wird die Zeichenfolge an alle Benutzer verteilt, die das hochgeladene Bild betrachten. Beim Empfang dieser Zeichenkette parst der Client sie, um die Koordinaten zu extrahieren, und rendert einen Kreis, der größer und kleiner wird, um die Klickstelle vorübergehend hervorzuheben.

Einpacken

In diesem Artikel haben wir einen flüchtigen Blick darauf geworfen, wie das Publish-Subscribe-Muster dazu beitragen kann, das Problem der Skalierung von Echtzeit-Web-Apps weitgehend und relativ einfach zu lösen.

Die Beispiel-App dient als Spielplatz zum Experimentieren mit Redis Pub/Sub. Aber wie bereits erwähnt, können die Ideen in fast jeder anderen gängigen Programmiersprache implementiert werden.