Praca w czasie rzeczywistym z Redis Pub/Sub

Opublikowany: 2022-03-11

Skalowanie aplikacji internetowej jest prawie zawsze ciekawym wyzwaniem, niezależnie od złożoności. Jednak aplikacje internetowe działające w czasie rzeczywistym stwarzają wyjątkowe problemy ze skalowalnością. Na przykład, aby móc skalować poziomo aplikację internetową do przesyłania wiadomości, która używa protokołu WebSockets do komunikacji z klientami, będzie musiała w jakiś sposób zsynchronizować wszystkie węzły serwera. Jeśli aplikacja nie została zbudowana z myślą o tym, skalowanie jej w poziomie może nie być łatwą opcją.

W tym artykule omówimy architekturę prostej aplikacji internetowej do udostępniania obrazów i wiadomości w czasie rzeczywistym. W tym miejscu skupimy się na różnych komponentach, takich jak Redis Pub/Sub, zaangażowanych w tworzenie aplikacji czasu rzeczywistego i zobaczymy, jak wszystkie odgrywają swoją rolę w ogólnej architekturze.

Praca w czasie rzeczywistym z Redis Pub/Sub

Praca w czasie rzeczywistym z Redis Pub/Sub
Ćwierkać

Pod względem funkcjonalności aplikacja jest bardzo lekka. Pozwala na przesyłanie obrazów i komentarze w czasie rzeczywistym do tych obrazów. Co więcej, każdy użytkownik może dotknąć obrazu, a inni użytkownicy będą mogli zobaczyć efekt falowania na swoim ekranie.

Cały kod źródłowy tej aplikacji jest dostępny na GitHub.

Rzeczy, których potrzebujemy

Udać się

Będziemy używać języka programowania Go. Nie ma specjalnego powodu, dla którego wybieramy Go w tym artykule, poza tym, że składnia Go jest czysta, a jej semantyka jest łatwiejsza do zrozumienia. Do tego dochodzi oczywiście stronniczość autora. Jednak wszystkie koncepcje omawiane w tym artykule można łatwo przetłumaczyć na wybrany przez Ciebie język.

Rozpoczęcie korzystania z Go jest łatwe. Jego dystrybucję binarną można pobrać z oficjalnej strony. Jeśli korzystasz z systemu Windows, na ich stronie pobierania znajduje się instalator MSI dla Go. Lub, jeśli Twój system operacyjny (na szczęście) oferuje menedżera pakietów:

Arch Linux:

 pacman -S go

Ubuntu:

 apt-get install golang

Mac OS X:

 brew install go

Ten będzie działał tylko wtedy, gdy mamy zainstalowany Homebrew.

MongoDB

Po co korzystać z MongoDB, jeśli mamy Redis, pytasz? Jak wspomniano wcześniej, Redis to magazyn danych w pamięci. Chociaż może utrzymywać dane na dysku, używanie Redis do tego celu prawdopodobnie nie jest najlepszym sposobem. Użyjemy MongoDB do przechowywania przesłanych metadanych obrazu i wiadomości.

MongoDB możemy pobrać z ich oficjalnej strony internetowej. W niektórych dystrybucjach Linuksa jest to preferowany sposób instalacji MongoDB. Mimo to powinno być możliwe do zainstalowania przy użyciu menedżera pakietów większości dystrybucji.

Arch Linux:

 pacman -S mongodb

Ubuntu:

 apt-get install mongodb

Mac OS X:

 brew install mongodb

W naszym kodzie Go użyjemy pakietu mgo (wymawiane mango). Nie tylko został przetestowany w boju, ale pakiet sterowników oferuje naprawdę czyste i proste API.

Jeśli nie jesteś ekspertem MongoDB, nie martw się. Użycie tej usługi bazy danych w naszej przykładowej aplikacji jest minimalne i prawie nieistotne w kontekście tego artykułu: Architektura Pub/Sub.

Amazonka S3

Będziemy używać Amazon S3 do przechowywania zdjęć przesłanych przez użytkownika. Nie ma tu wiele do zrobienia, poza upewnieniem się, że mamy gotowe konto Amazon Web Services i utworzony tymczasowy zasobnik.

Przechowywanie przesłanych plików na dysk lokalny nie wchodzi w grę, ponieważ nie chcemy w żaden sposób polegać na tożsamości naszych węzłów sieciowych. Chcemy, aby użytkownicy mogli łączyć się z dowolnymi dostępnymi węzłami sieciowymi i nadal widzieć tę samą zawartość.

Do interakcji z zasobnikiem Amazon S3 z naszego kodu Go użyjemy AdRoll/goamz, rozwidlenia pakietu goamz firmy Canonical z pewnymi różnicami.

Redis

Ostatni, ale nie mniej ważny: Redis. Możemy go zainstalować za pomocą menedżera pakietów naszej dystrybucji:

Arch Linux:

 pacman -S redis

Ubuntu:

 apt-get install redis-server

Mac OS X:

 brew install redis

Lub pobierz jego kod źródłowy i skompiluj go samodzielnie. Redis nie ma zależności innych niż GCC i libc do jego budowania:

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

Po zainstalowaniu i uruchomieniu Redis uruchom terminal i wprowadź CLI Redis:

 redis-cli

Spróbuj wprowadzić następujące polecenia i sprawdź, czy uzyskasz oczekiwane wyniki:

 SET answer 41 INCR answer GET answer

Pierwsze polecenie przechowuje „41” przy klawiszu „odpowiedź”, drugie polecenie zwiększa wartość, trzecie polecenie drukuje wartość zapisaną pod podanym kluczem. Wynik powinien brzmieć „42”.

Możesz dowiedzieć się więcej o wszystkich poleceniach obsługiwanych przez Redis na ich oficjalnej stronie internetowej.

Użyjemy pakietu Go redigo, aby połączyć się z Redis z poziomu kodu naszej aplikacji.

Zajrzyj do Redis Pub/Sub

Wzorzec publikuj-subskrybuj to sposób przekazywania wiadomości do dowolnej liczby nadawców. Nadawcy tych wiadomości (wydawcy) nie identyfikują jednoznacznie docelowych odbiorców. Zamiast tego wiadomości wysyłane są na kanale, na którym może na nie czekać dowolna liczba odbiorców (abonentów).

Prosta konfiguracja publikowania-subskrypcji

W naszym przypadku możemy mieć dowolną liczbę węzłów sieciowych działających za load balancerem. W danym momencie dwóch użytkowników patrzących na ten sam obraz może nie być podłączonych do tego samego węzła. W tym miejscu do gry wkracza Redis Pub/Sub. Za każdym razem, gdy węzeł sieciowy potrzebuje zaobserwować zmianę (na przykład nowa wiadomość jest tworzona przez użytkownika), użyje Redis Pub/Sub, aby rozesłać te informacje do wszystkich odpowiednich węzłów sieci. Co z kolei przekaże informacje do odpowiednich klientów, aby mogli pobrać zaktualizowaną listę wiadomości redis.

Ponieważ wzorzec publikuj-subskrybuj pozwala nam na wysyłanie wiadomości na nazwanych kanałach, każdy węzeł sieciowy może być połączony z Redis i subskrybować tylko te kanały, którymi zainteresowani są ich połączeni użytkownicy. Na przykład, jeśli dwóch użytkowników patrzy na ten sam obraz, ale są połączone z dwoma różnymi węzłami sieciowymi z wielu węzłów sieciowych, wtedy tylko te dwa węzły sieciowe muszą subskrybować odpowiedni kanał. Każda wiadomość opublikowana na tym kanale zostanie dostarczona tylko do tych dwóch węzłów sieciowych.

Brzmi zbyt pięknie, aby mogło być prawdziwe? Możemy to wypróbować za pomocą CLI Redisa. Uruchom trzy wystąpienia redis-cli . W pierwszej kolejności wykonaj następujące polecenie:

 SUBSCRIBE somechannel

Wykonaj następujące polecenie w drugiej instancji Redis CLI:

 SUBSCRIBE someotherchannel

Wykonaj następujące polecenia w trzecim wystąpieniu Redis CLI:

 PUBLISH somechannel lorem PUBLISH someotherchannel ipsum

Zwróć uwagę, jak pierwsza instancja otrzymała „lorem”, ale nie „ipsum”, a druga instancja otrzymała „ipsum”, ale nie „lorem”.

Redis Pub/Sub w akcji

Warto wspomnieć, że gdy klient Redis wejdzie w tryb abonencki, nie może już wykonywać żadnej operacji poza zasubskrybowaniem większej liczby kanałów lub wypisaniem się z subskrybowanych. Oznacza to, że każdy węzeł sieciowy będzie musiał utrzymywać dwa połączenia z Redis, jedno do łączenia się z Redis jako subskrybent, a drugie do publikowania wiadomości na kanałach, aby każdy węzeł sieciowy subskrybujący te kanały mógł je odbierać.

Skalowalny i działający w czasie rzeczywistym

Zanim zaczniemy badać, co dzieje się za kulisami, sklonujmy repozytorium:

 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 ./...

… i skompiluj go:

 go build ./cmd/tonesad

Aby uruchomić aplikację, najpierw utwórz plik o nazwie .env (najlepiej kopiując plik env-sample.txt):

 cp env-sample.txt .env

Wypełnij plik .env wszystkimi niezbędnymi zmiennymi środowiskowymi:

 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}

Na koniec uruchom zbudowany plik binarny:

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

Węzeł sieciowy powinien teraz działać i być dostępny pod adresem http://localhost:9091.

Przykład na żywo

Aby sprawdzić, czy nadal działa po skalowaniu w poziomie, możesz uruchomić wiele węzłów internetowych, uruchamiając je z różnymi numerami portów:

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

… i dostęp do nich za pośrednictwem odpowiednich adresów URL: http://localhost:9092 i http://localhost:9093.

Przykład na żywo

Za kulisami

Zamiast przechodzić przez każdy etap rozwoju aplikacji, skupimy się na kilku najważniejszych elementach. Chociaż nie wszystkie z nich są w 100% związane z Redis Pub/Sub i jego implikacjami w czasie rzeczywistym, nadal mają znaczenie dla ogólnej struktury aplikacji i ułatwią śledzenie, gdy zagłębimy się głębiej.

Aby wszystko było proste, nie będziemy zajmować się uwierzytelnianiem użytkowników. Przesyłane pliki będą anonimowe i dostępne dla każdego, kto zna adres URL. Wszyscy widzowie mogą wysyłać wiadomości i mieć możliwość wybrania własnego aliasu. Dostosowanie odpowiedniego mechanizmu uwierzytelniania i funkcji prywatności powinno być trywialne i wykracza poza zakres tego artykułu.

Trwałe dane

Ten jest łatwy.

Za każdym razem, gdy użytkownik przesyła obraz, przechowujemy go w Amazon S3, a następnie przechowujemy ścieżkę do niego w MongoDB w oparciu o dwa identyfikatory: jeden identyfikator obiektu BSON (ulubiony przez MongoDB) i drugi krótki, 8-znakowy identyfikator (nieco przyjemny dla oka). To trafia do kolekcji „przesłanych” naszej bazy danych i ma taką strukturę:

 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"` }

Pole Rodzaj służy do wskazania rodzaju mediów, które zawiera ten „przesyłany” plik. Czy to oznacza, że ​​obsługujemy media inne niż obrazy? Niestety nie. Ale pole zostało tam pozostawione, aby przypominać, że niekoniecznie ograniczamy się tutaj do obrazów.

Gdy użytkownicy wysyłają do siebie wiadomości, są one przechowywane w innej kolekcji. Tak, zgadłeś: „wiadomości”.

 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"` }

Jedynym interesującym elementem jest tutaj pole UploadID, które służy do kojarzenia wiadomości z konkretnym przesyłaniem.

Punkty końcowe API

Ta aplikacja ma zasadniczo trzy punkty końcowe.

POST /api/przesyłania

Program obsługi tego punktu końcowego oczekuje przesłania „wieloczęściowych/formularzy danych” z obrazem w polu „plik”. Zachowanie programu obsługi wygląda mniej więcej tak:

 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 wymaga, aby wszystkie błędy były obsługiwane w sposób jawny. Zostało to zrobione w prototypie, ale zostało pominięte we fragmentach tego artykułu, aby skupić się na krytycznych częściach.

W module obsługi tego punktu końcowego API zasadniczo czytamy plik, ale ograniczamy jego rozmiar do określonej wartości. Jeśli przesłany plik przekroczy tę wartość, żądanie zostanie odrzucone. W przeciwnym razie identyfikator BSON jest generowany i używany do przesyłania obrazu do Amazon S3 przed utrwaleniem encji przesyłania w MongoDB.

Sposób generowania identyfikatorów obiektów BSON ma swoje zalety i wady. Są generowane po stronie klienta. Jednak strategia użyta do wygenerowania Object ID sprawia, że ​​prawdopodobieństwo kolizji jest tak małe, że można je bezpiecznie wygenerować po stronie klienta. Z drugiej strony, wartości wygenerowanych identyfikatorów obiektów są zwykle sekwencyjne i jest to coś, za czym Amazon S3 nie do końca lubi. Prostym obejściem tego problemu jest dodanie do nazwy pliku losowego ciągu.

POBIERZ /api/uploads/{id}/messages

Ten interfejs API służy do pobierania najnowszych wiadomości oraz wiadomości, które zostały opublikowane po określonym czasie.

 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) }

Gdy przeglądarka użytkownika zostanie powiadomiona o nowej wiadomości podczas przesyłania, którą użytkownik aktualnie przegląda, pobiera nowe wiadomości przy użyciu tego punktu końcowego.

POST /api/uploads/{id}/wiadomości

I wreszcie handler, który tworzy wiadomości i powiadamia wszystkich:

 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()) }

Ten program obsługi jest tak podobny do innych, że nawet włączenie go tutaj jest prawie nudne. Albo to jest? Zwróć uwagę, że na samym końcu funkcji znajduje się wywołanie funkcji hub.Emit() . Co to jest piasta, mówisz? To tam dzieje się cała magia Pub/Sub.

Hub: Gdzie WebSockets spotykają Redis

Hub to miejsce, w którym sklejamy WebSockets z kanałami Redis Pub/Sub. I przypadkowo pakiet, którego używamy do obsługi WebSockets na naszych serwerach internetowych, nazywa się klejem.

Hub zasadniczo utrzymuje kilka struktur danych, które tworzą mapowanie między wszystkimi podłączonymi WebSocketami do wszystkich kanałów, którymi są zainteresowani. Na przykład WebSocket na karcie przeglądarki użytkownika wskazujący konkretny przesłany obraz powinien naturalnie być zainteresowany wszystkimi istotnymi powiadomieniami do niego.

Pakiet hub realizuje sześć funkcji:

  • Subskrybuj
  • Anuluj subskrypcjęWszystko
  • Wydzielać
  • EmitujLokalne
  • InitHub
  • UchwytGniazdo

Subskrybuj i anuluj subskrypcjęWszystko

 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 }

Ta funkcja, podobnie jak większość innych w tym pakiecie, blokuje muteks odczytu/zapisu podczas jego wykonywania. Dzieje się tak, abyśmy mogli bezpiecznie modyfikować pierwotne struktury danych zmienne gniazda i tematy . Pierwsza zmienna sockets odwzorowuje gniazda na nazwy kanałów, a druga topic odwzorowuje nazwy kanałów na gniazda. W tej funkcji budujemy te mapowania. Za każdym razem, gdy widzimy, że gniazdo subskrybuje nową nazwę kanału, tworzymy nasze połączenie Redis, subconn , subskrybujemy ten kanał na Redis za pomocą subconn.Subscribe . To sprawia, że ​​Redis przekazuje wszystkie powiadomienia na tym kanale do tego węzła sieciowego.

I podobnie w funkcji UnsubscribeAll usuwamy mapowanie:

 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 }

Kiedy usuniemy ostatnie gniazdo ze struktury danych zainteresowanej konkretnym kanałem, wypisujemy się z kanału w Redis za pomocą subconn.Unsubscribe .

Wydzielać

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

Ta funkcja publikuje wiadomość m na kanale t przy użyciu połączenia publikowania do Redis.

EmitujLokalne

 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 }

W funkcji InitHub tworzymy dwa połączenia z Redis: jedno do subskrybowania kanałów, którymi ten węzeł sieciowy jest zainteresowany, a drugie do publikowania wiadomości. Po ustanowieniu połączeń rozpoczynamy nową procedurę Go z pętlą działającą w nieskończoność, oczekującą na otrzymanie wiadomości przez połączenie subskrybenta do Redis. Za każdym razem, gdy otrzymuje wiadomość, emituje ją lokalnie (tj. do wszystkich WebSocketów podłączonych do tego węzła sieciowego).

UchwytGniazdo

I wreszcie, HandleSocket to miejsce, w którym czekamy na wiadomości przejdą przez WebSockets lub posprzątamy po zamknięciu połączenia:

 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]) } }) }

JavaScript w interfejsie użytkownika

Ponieważ Gluk posiada własną, front-endową bibliotekę JavaScript, znacznie łatwiej jest obsługiwać WebSockets (lub wrócić do odpytywania XHR, gdy WebSockets są niedostępne):

 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)

Po stronie klienta nasłuchujemy każdej wiadomości przychodzącej przez WebSocket. Ponieważ Gluk przesyła wszystkie wiadomości jako ciągi, kodujemy w nim wszystkie informacje za pomocą określonych wzorców:

  • Nowa wiadomość: „message:{messageID}”
  • Kliknij obraz: „touch:{coordX},{coordY}”, gdzie coordX i coordY to współrzędna procentowa lokalizacji kliknięcia przez użytkownika na obrazie

Kiedy użytkownik tworzy nową wiadomość, używamy API „POST /api/uploads/{uploadID}/messages”, aby utworzyć nową wiadomość. Odbywa się to za pomocą metody create w kolekcji szkieletowej dla wiadomości:

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

Kiedy użytkownik kliknie w obraz, obliczamy pozycję kliknięcia w procentach szerokości i wysokości obrazu i wysyłamy informacje bezpośrednio przez WebSocket.

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

Przegląd

Przegląd architektury aplikacji

Gdy użytkownik wpisze wiadomość i naciśnie klawisz Enter, klient wywołuje punkt końcowy interfejsu API „POST /api/uploads/{id}/messages”. To z kolei tworzy jednostkę wiadomości w bazie danych i publikuje ciąg „message: {messageID}” za pośrednictwem Redis Pub/Sub na kanale „upload: {uploadID}” za pośrednictwem pakietu centralnego.

Redis przekazuje ten ciąg do każdego węzła sieciowego (subskrybenta) zainteresowanego na kanale „upload:{uploadID}”. Węzły sieci Web otrzymujące ten ciąg iterują po wszystkich WebSocketach odpowiednich dla kanału i wysyłają ciąg do klienta za pośrednictwem swoich połączeń WebSocket. Klienci otrzymujący ten ciąg zaczynają pobierać nowe wiadomości z serwera za pomocą „GET /api/uploads/{id}/messages”.

Podobnie, aby propagować zdarzenia kliknięcia na obrazie, klient bezpośrednio wysyła wiadomość przez WebSocket, która wygląda mniej więcej tak: „przesyłanie dotykowe:{uploadID} {coordX} {coordY}”. Ta wiadomość kończy się w pakiecie centralnym, gdzie jest publikowana na tym samym kanale „upload: {uploadID}”. W rezultacie ciąg jest dystrybuowany do wszystkich użytkowników oglądających przesłany obraz. Klient, po otrzymaniu tego ciągu, analizuje go, aby wyodrębnić współrzędne i renderuje rosnący, zanikający okrąg, aby chwilowo podświetlić lokalizację kliknięcia.

Zakończyć

W tym artykule zobaczyliśmy, jak wzorzec publikuj-subskrybuj może pomóc rozwiązać problem skalowania aplikacji internetowych czasu rzeczywistego w dużym stopniu i stosunkowo łatwo.

Przykładowa aplikacja służy jako plac zabaw do eksperymentowania z Redis Pub/Sub. Ale, jak wspomniano wcześniej, pomysły można zaimplementować w prawie każdym innym popularnym języku programowania.