Redis Pub/Sub로 실시간 전환

게시 됨: 2022-03-11

웹 앱 확장은 관련된 복잡성에 관계없이 거의 항상 흥미로운 과제입니다. 그러나 실시간 웹 앱에는 고유한 확장성 문제가 있습니다. 예를 들어 WebSocket을 사용하여 클라이언트와 통신하는 메시징 웹 앱을 수평으로 확장하려면 모든 서버 노드를 어떻게든 동기화해야 합니다. 앱이 이를 염두에 두고 구축되지 않았다면 수평으로 확장하는 것이 쉬운 선택이 아닐 수 있습니다.

이 기사에서는 간단한 실시간 이미지 공유 및 메시징 웹 앱의 아키텍처를 살펴보겠습니다. 여기에서는 실시간 앱 구축과 관련된 Redis Pub/Sub와 같은 다양한 구성 요소에 초점을 맞추고 전체 아키텍처에서 모든 구성 요소가 어떻게 역할을 하는지 살펴보겠습니다.

Redis Pub/Sub로 실시간 전환

Redis Pub/Sub로 실시간 전환
트위터

기능면에서 응용 프로그램은 매우 가볍습니다. 그것은 이미지를 업로드하고 해당 이미지에 대한 실시간 댓글을 허용합니다. 또한, 모든 사용자는 이미지를 탭할 수 있으며 다른 사용자는 화면에서 파급 효과를 볼 수 있습니다.

이 앱의 전체 소스 코드는 GitHub에서 사용할 수 있습니다.

우리에게 필요한 것들

가다

우리는 프로그래밍 언어 Go를 사용할 것입니다. Go의 구문이 깨끗하고 의미 체계를 따르기가 더 쉽다는 점 외에 이 기사에서 Go를 선택한 특별한 이유는 없습니다. 그리고 물론 작가의 편견이 있다. 그러나 이 기사에서 논의된 모든 개념은 원하는 언어로 쉽게 번역할 수 있습니다.

Go를 시작하는 것은 쉽습니다. 바이너리 배포판은 공식 사이트에서 다운로드할 수 있습니다. Windows를 사용하는 경우 다운로드 페이지에 Go용 MSI 설치 프로그램이 있습니다. 또는 운영 체제(다행히도)가 패키지 관리자를 제공하는 경우:

아치 리눅스:

 pacman -S go

우분투:

 apt-get install golang

맥 OS X:

 brew install go

이것은 Homebrew가 설치된 경우에만 작동합니다.

몽고DB

Redis가 있는데 왜 MongoDB를 사용합니까? 앞서 언급했듯이 Redis는 메모리 내 데이터 저장소입니다. 디스크에 데이터를 유지할 수 있지만 해당 목적으로 Redis를 사용하는 것이 가장 좋은 방법은 아닙니다. 업로드된 이미지 메타데이터와 메시지를 저장하기 위해 MongoDB를 사용할 것입니다.

MongoDB는 공식 웹사이트에서 다운로드할 수 있습니다. 일부 Linux 배포판에서는 이것이 MongoDB를 설치하는 데 선호되는 방법입니다. 그럼에도 불구하고 대부분의 배포판의 패키지 관리자를 사용하여 여전히 설치할 수 있습니다.

아치 리눅스:

 pacman -S mongodb

우분투:

 apt-get install mongodb

맥 OS X:

 brew install mongodb

Go 코드 내에서 mgo(망고로 발음) 패키지를 사용합니다. 전투 테스트를 거쳤을 뿐만 아니라 드라이버 패키지는 정말 깨끗하고 간단한 API를 제공합니다.

MongoDB 전문가가 아니더라도 전혀 걱정하지 마십시오. 이 데이터베이스 서비스의 사용은 샘플 앱에서 최소화되며 이 문서의 초점인 Pub/Sub 아키텍처와 거의 관련이 없습니다.

아마존 S3

Amazon S3를 사용하여 사용자가 업로드한 이미지를 저장합니다. Amazon Web Services 준비 계정과 임시 버킷이 생성되었는지 확인하는 것 외에는 여기서 할 일이별로 없습니다.

업로드된 파일을 로컬 디스크에 저장하는 것은 어떤 식으로든 웹 노드의 ID에 의존하고 싶지 않기 때문에 옵션이 아닙니다. 우리는 사용자가 사용 가능한 모든 웹 노드에 연결할 수 있기를 원하며 여전히 동일한 콘텐츠를 볼 수 있기를 바랍니다.

Go 코드의 Amazon S3 버킷과 상호 작용하기 위해 약간의 차이점이 있는 Canonical의 goamz 패키지 포크인 AdRoll/goamz를 사용합니다.

레디스

마지막으로 Redis입니다. 배포판의 패키지 관리자를 사용하여 설치할 수 있습니다.

아치 리눅스:

 pacman -S redis

우분투:

 apt-get install redis-server

맥 OS X:

 brew install redis

또는 소스 코드를 가져와서 직접 컴파일하십시오. Redis에는 빌드를 위한 GCC 및 libc 이외의 종속성이 없습니다.

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

Redis가 설치되고 실행되면 터미널을 시작하고 Redis의 CLI를 입력합니다.

 redis-cli

다음 명령을 입력하고 예상한 출력이 나오는지 확인하십시오.

 SET answer 41 INCR answer GET answer

첫 번째 명령은 키 "answer"에 대해 "41"을 저장하고, 두 번째 명령은 값을 증가시키며, 세 번째 명령은 지정된 키에 대해 저장된 값을 인쇄합니다. 결과는 "42"로 읽어야 합니다.

Redis가 지원하는 모든 명령에 대한 자세한 내용은 공식 웹사이트에서 확인할 수 있습니다.

Go 패키지 redigo를 사용하여 앱 코드 내에서 Redis에 연결합니다.

Redis Pub/Sub 살펴보기

발행-구독 패턴은 임의의 수의 발신자에게 메시지를 전달하는 방법입니다. 이러한 메시지의 보낸 사람(게시자)은 대상 받는 사람을 명시적으로 식별하지 않습니다. 대신 메시지는 원하는 수의 수신자(가입자)가 메시지를 기다릴 수 있는 채널로 전송됩니다.

간단한 게시-구독 구성

우리의 경우 로드 밸런서 뒤에서 실행 중인 웹 노드를 원하는 수만큼 가질 수 있습니다. 주어진 순간에 동일한 이미지를 보고 있는 두 명의 사용자가 동일한 노드에 연결되지 않을 수 있습니다. 여기에서 Redis Pub/Sub가 작동합니다. 웹 노드는 변경 사항을 관찰해야 할 때마다(예: 사용자가 새 메시지를 생성함) Redis Pub/Sub를 사용하여 해당 정보를 모든 관련 웹 노드에 브로드캐스트합니다. 그러면 관련 클라이언트에 정보를 전파하여 업데이트된 메시지 redis 목록을 가져올 수 있습니다.

게시-구독 패턴을 사용하면 명명된 채널에 메시지를 전달할 수 있으므로 각 웹 노드를 Redis에 연결하고 연결된 사용자가 관심이 있는 채널만 구독하도록 할 수 있습니다. 예를 들어 두 사용자가 둘 다 보고 있는 경우 동일한 이미지이지만 많은 웹 노드 중 두 개의 다른 웹 노드에 연결되어 있는 경우 해당 두 웹 노드만 해당 채널을 구독하면 됩니다. 해당 채널에 게시된 모든 메시지는 해당 두 웹 노드에만 전달됩니다.

사실이라고 하기에는 너무 좋은 것 같나요? Redis의 CLI를 사용하여 시도해 볼 수 있습니다. redis-cli 의 세 인스턴스를 시작합니다. 첫 번째 인스턴스에서 다음 명령을 실행합니다.

 SUBSCRIBE somechannel

두 번째 Redis CLI 인스턴스에서 다음 명령을 실행합니다.

 SUBSCRIBE someotherchannel

Redis CLI의 세 번째 인스턴스에서 다음 명령을 실행합니다.

 PUBLISH somechannel lorem PUBLISH someotherchannel ipsum

첫 번째 인스턴스가 "lorem"을 수신했지만 "ipsum"을 수신하지 않은 것과 두 번째 인스턴스가 "lorem"을 수신하지 않고 "ipsum"을 수신한 방법에 주목하십시오.

Redis Pub/Sub 실행 중

Redis 클라이언트가 구독자 모드에 들어가면 더 이상 더 많은 채널을 구독하거나 구독 취소하는 것 외에 다른 작업을 수행할 수 없습니다. 즉, 각 웹 노드는 Redis에 대한 두 가지 연결을 유지해야 합니다. 하나는 Redis에 구독자로 연결하고 다른 하나는 채널에 메시지를 게시하여 해당 채널에 구독하는 모든 웹 노드가 메시지를 받을 수 있습니다.

실시간 및 확장성

장면 뒤에서 무슨 일이 일어나고 있는지 살펴보기 전에 저장소를 복제해 보겠습니다.

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

... 그리고 컴파일:

 go build ./cmd/tonesad

앱을 실행하려면 우선 .env라는 이름의 파일을 만드십시오(가능하면 env-sample.txt 파일을 복사하여):

 cp env-sample.txt .env

필요한 모든 환경 변수로 .env 파일을 채우십시오.

 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}

마지막으로 빌드된 바이너리를 실행합니다.

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

이제 웹 노드가 실행 중이고 http://localhost:9091을 통해 액세스할 수 있습니다.

라이브 예시

수평으로 확장할 때 여전히 작동하는지 테스트하려면 다른 포트 번호로 시작하여 여러 웹 노드를 스핀업할 수 있습니다.

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

… 해당 URL을 통해 액세스합니다: http://localhost:9092 및 http://localhost:9093.

라이브 예시

무대 뒤에서

앱 개발의 모든 단계를 거치는 대신 가장 중요한 부분에 집중할 것입니다. 이들 모두가 Redis Pub/Sub 및 실시간 영향과 100% 관련이 있는 것은 아니지만 여전히 앱의 전체 구조와 관련이 있으며 더 깊이 파고들면 더 쉽게 따라갈 수 있습니다.

일을 단순하게 유지하기 위해 사용자 인증에 대해 신경 쓰지 않을 것입니다. 업로드는 익명으로 처리되며 URL을 아는 모든 사람이 사용할 수 있습니다. 모든 뷰어는 메시지를 보낼 수 있으며 자신의 별칭을 선택할 수 있습니다. 적절한 인증 메커니즘과 개인 정보 보호 기능을 적용하는 것은 간단해야 하며 이 문서의 범위를 벗어납니다.

지속 데이터

이것은 쉽습니다.

사용자가 이미지를 업로드할 때마다 우리는 그것을 Amazon S3에 저장한 다음 두 개의 ID, 즉 BSON 객체 ID(MongoDB가 선호하는 ID)와 8자 길이의 짧은 ID(눈에 보기에 다소 즐거운)에 대한 경로를 MongoDB에 저장합니다. 이것은 데이터베이스의 "업로드" 컬렉션으로 들어가고 다음과 같은 구조를 갖습니다.

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

종류 필드는 이 "업로드"에 포함된 미디어 종류를 나타내는 데 사용됩니다. 이것은 우리가 이미지 이외의 미디어를 지원한다는 것을 의미합니까? 불행하게도. 그러나 필드는 우리가 여기에 있는 이미지에만 국한되지 않는다는 것을 상기시키기 위해 남겨두었습니다.

사용자가 서로에게 메시지를 보내면 다른 컬렉션에 저장됩니다. 예, 당신은 그것을 추측했습니다 : "메시지".

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

여기서 흥미로운 부분은 메시지를 특정 업로드에 연결하는 데 사용되는 UploadID 필드입니다.

API 엔드포인트

이 애플리케이션에는 기본적으로 3개의 엔드포인트가 있습니다.

POST /api/업로드

이 끝점에 대한 처리기는 "파일" 필드에 이미지가 있는 "멀티파트/양식 데이터" 제출을 예상합니다. 핸들러의 동작은 대략 다음과 같습니다.

 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에서는 모든 오류를 명시적으로 처리해야 합니다. 이것은 프로토타입에서 수행되었지만 중요한 부분에 초점을 유지하기 위해 이 기사의 스니펫에서 생략되었습니다.

이 API 끝점의 처리기에서 기본적으로 파일을 읽고 있지만 크기를 특정 값으로 제한합니다. 업로드가 이 값을 초과하면 요청이 거부됩니다. 그렇지 않으면 BSON ID가 생성되어 MongoDB에 업로드 엔터티를 유지하기 전에 Amazon S3에 이미지를 업로드하는 데 사용됩니다.

BSON 개체 ID가 생성되는 방식에는 장단점이 있습니다. 클라이언트 측에서 생성됩니다. 그러나 생성된 개체 ID에 사용된 전략은 충돌 가능성을 매우 작게 만들어 클라이언트 측에서 생성하는 것이 안전합니다. 반면에 생성된 객체 ID의 값은 일반적으로 순차적이며 이는 Amazon S3가 그다지 좋아하지 않는 것입니다. 이에 대한 쉬운 해결 방법은 파일 이름 앞에 임의의 문자열을 추가하는 것입니다.

GET /api/uploads/{id}/messages

이 API는 최근 메시지 및 특정 시간 이후에 게시된 메시지를 가져오는 데 사용됩니다.

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

사용자의 브라우저는 사용자가 현재 보고 있는 업로드의 새 메시지에 대해 알림을 받으면 이 끝점을 사용하여 새 메시지를 가져옵니다.

POST /api/uploads/{id}/messages

그리고 마지막으로 메시지를 생성하고 모두에게 알리는 핸들러:

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

이 핸들러는 다른 핸들러와 너무 유사하여 여기에 포함시키는 것조차 거의 지루합니다. 아니면? 함수의 맨 끝에 함수 호출 hub.Emit() 이 있는 방법에 주목하십시오. 당신이 말하는 허브는 무엇입니까? 모든 Pub/Sub 마법이 일어나는 곳입니다.

허브: WebSocket이 Redis를 만나는 곳

Hub는 WebSocket을 Redis의 Pub/Sub 채널과 연결하는 곳입니다. 그리고 우연히도 웹 서버 내에서 WebSocket을 처리하는 데 사용하는 패키지를 글루라고 합니다.

Hub는 기본적으로 연결된 모든 WebSocket을 관심 있는 모든 채널에 매핑하는 몇 가지 데이터 구조를 유지 관리합니다. 예를 들어 사용자의 브라우저 탭에 있는 WebSocket이 특정 업로드된 이미지를 가리키면 자연스럽게 관련된 모든 알림에 관심을 가져야 합니다. 그것에.

허브 패키지는 6가지 기능을 구현합니다.

  • 구독하다
  • 모두 구독 취소
  • 방출
  • 에밋로컬
  • 이니트허브
  • 핸들소켓

구독 및 구독 취소모두

 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 }

이 패키지의 다른 대부분의 함수와 마찬가지로 이 함수는 실행되는 동안 읽기/쓰기 뮤텍스에 대한 잠금을 유지합니다. 이것은 원시 데이터 구조 변수 sockettopic 을 안전하게 수정할 수 있도록 하기 위한 것입니다. 첫 번째 변수 sockets 는 소켓을 채널 이름에 매핑하고 두 번째 변수 topic 은 채널 이름을 소켓에 매핑합니다. 이 함수에서 우리는 이러한 매핑을 구축합니다. 소켓이 새 채널 이름을 구독하는 것을 볼 때마다 Redis 연결 subconn만들고 subconn.Subscribe 를 사용하여 Redis에서 해당 채널을 구독합니다. 이렇게 하면 Redis가 해당 채널의 모든 알림을 이 웹 노드로 전달합니다.

그리고 마찬가지로 UnsubscribeAll 함수에서 매핑을 분해합니다.

 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 }

특정 채널에 관심이 있는 데이터 구조에서 마지막 소켓을 제거할 때 subconn.Unsubscribe 를 사용하여 Redis의 채널에서 구독을 취소합니다.

방출

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

이 기능은 Redis에 대한 게시 연결을 사용하여 채널 t 에 메시지 m 을 게시합니다.

에밋로컬

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

이니트허브

 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 }

InitHub 기능에서 Redis에 대한 두 가지 연결을 생성합니다. 하나는 이 웹 노드가 관심을 갖고 있는 채널을 구독하기 위한 것이고 다른 하나는 메시지를 게시하기 위한 것입니다. 연결이 설정되면 Redis에 대한 구독자 연결을 통해 메시지 수신을 영원히 기다리는 루프로 새로운 Go 루틴을 시작합니다. 메시지를 수신할 때마다 로컬에서(즉, 이 웹 노드에 연결된 모든 WebSocket으로) 메시지를 내보냅니다.

핸들소켓

마지막으로 HandleSocket 은 메시지가 WebSocket을 통해 들어오거나 연결이 닫힌 후 정리되기를 기다리는 곳입니다.

 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 라이브러리와 함께 제공되기 때문에 WebSocket을 처리하는 것이 훨씬 쉽습니다(또는 WebSocket을 사용할 수 없는 경우 XHR 폴백으로 폴백).

 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)

클라이언트 측에서는 WebSocket을 통해 들어오는 모든 메시지를 수신합니다. 글루는 모든 메시지를 문자열로 전송하기 때문에 특정 패턴을 사용하여 모든 정보를 인코딩합니다.

  • 새 메시지: "메시지:{messageID}"
  • 이미지 클릭: "touch:{coordX},{coordY}", 여기서 coordX 및 coordY는 이미지에서 사용자가 클릭한 위치의 백분율 기반 좌표입니다.

사용자가 새 메시지를 만들 때 "POST /api/uploads/{uploadID}/messages" API를 사용하여 새 메시지를 만듭니다. 이것은 메시지에 대한 백본 컬렉션의 create 메소드를 사용하여 수행됩니다.

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

사용자가 이미지를 클릭하면 이미지 너비와 높이의 백분율로 클릭 위치를 계산하고 WebSocket을 통해 직접 정보를 보냅니다.

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

개요

애플리케이션 아키텍처 개요

사용자가 메시지를 입력하고 Enter 키를 누르면 클라이언트는 "POST /api/uploads/{id}/messages" API 엔드포인트를 호출합니다. 그러면 데이터베이스에 메시지 엔터티가 생성되고 허브 패키지를 통해 "upload:{uploadID}" 채널의 Redis Pub/Sub를 통해 "message:{messageID}" 문자열이 게시됩니다.

Redis는 "upload:{uploadID}" 채널에 관심이 있는 모든 웹 노드(구독자)에게 이 문자열을 전달합니다. 이 문자열을 수신하는 웹 노드는 채널과 관련된 모든 WebSocket을 반복하고 WebSocket 연결을 통해 문자열을 클라이언트에 보냅니다. 이 문자열을 받는 클라이언트는 "GET /api/uploads/{id}/messages"를 사용하여 서버에서 새 메시지를 가져오기 시작합니다.

마찬가지로 이미지에서 클릭 이벤트를 전파하기 위해 클라이언트는 "touch upload:{uploadID} {coordX} {coordY}"와 같은 메시지를 WebSocket을 통해 직접 보냅니다. 이 메시지는 동일한 채널 "upload:{uploadID}"에 게시되는 허브 패키지에서 끝납니다. 결과적으로 문자열은 업로드된 이미지를 보고 있는 모든 사용자에게 배포됩니다. 클라이언트는 이 문자열을 수신하면 이를 구문 분석하여 좌표를 추출하고 클릭 위치를 순간적으로 강조 표시하기 위해 점점 희미해지는 원을 렌더링합니다.

마무리

이 기사에서는 게시-구독 패턴이 실시간 웹 앱을 비교적 쉽게 확장하는 문제를 해결하는 데 어떻게 도움이 되는지 살펴보았습니다.

샘플 앱은 Redis Pub/Sub를 실험하기 위한 놀이터 역할을 하기 위해 존재합니다. 그러나 앞서 언급했듯이 아이디어는 거의 모든 다른 인기 있는 프로그래밍 언어로 구현할 수 있습니다.