الذهاب في الوقت الفعلي مع Redis Pub / Sub
نشرت: 2022-03-11دائمًا ما يكون توسيع نطاق تطبيق الويب تحديًا مثيرًا للاهتمام ، بغض النظر عن التعقيد الذي ينطوي عليه ذلك. ومع ذلك ، تطرح تطبيقات الويب في الوقت الفعلي مشكلات فريدة تتعلق بقابلية التوسع. على سبيل المثال ، لتكون قادرًا على توسيع نطاق تطبيق ويب للمراسلة أفقيًا يستخدم WebSockets للتواصل مع عملائه ، فسوف يحتاج إلى مزامنة جميع عُقد الخادم بطريقة أو بأخرى. إذا لم يتم إنشاء التطبيق مع وضع ذلك في الاعتبار ، فقد لا يكون توسيعه أفقيًا خيارًا سهلاً.
في هذه المقالة ، سنتعرف على بنية تطبيق ويب بسيط لمشاركة الصور والمراسلة في الوقت الفعلي. هنا ، سنركز على المكونات المختلفة ، مثل Redis Pub / Sub ، التي تشارك في إنشاء تطبيق في الوقت الفعلي ونرى كيف تلعب جميعها دورها في الهيكل العام.
الوظائف الحكيمة ، التطبيق خفيف جدا. يسمح بتحميل الصور والتعليقات في الوقت الفعلي على تلك الصور. علاوة على ذلك ، يمكن لأي مستخدم النقر على الصورة وسيتمكن المستخدمون الآخرون من رؤية تأثير مضاعف على شاشتهم.
كود المصدر الكامل لهذا التطبيق متاح على GitHub.
الأشياء التي نحتاجها
اذهب
سوف نستخدم لغة البرمجة Go. لا يوجد سبب خاص لاختيارنا Go لهذه المقالة ، بالإضافة إلى أن بناء جملة Go نظيف وأن دلالاته أسهل في المتابعة. ثم هناك بالطبع تحيز المؤلف. ومع ذلك ، يمكن بسهولة ترجمة جميع المفاهيم التي تمت مناقشتها في هذه المقالة إلى اللغة التي تختارها.
بدء استخدام Go سهل. يمكن تنزيل توزيعها الثنائي من الموقع الرسمي. إذا كنت تستخدم نظام التشغيل Windows ، فهناك مُثبِّت MSI لـ Go على صفحة التنزيل الخاصة بهم. أو في حالة ما إذا كان نظام التشغيل لديك (لحسن الحظ) يوفر مدير حزم:
آرتش لينكس:
pacman -S go
أوبونتو:
apt-get install golang
نظام التشغيل Mac OS X:
brew install go
لن يعمل هذا إلا إذا كان لدينا برنامج Homebrew مثبتًا.
MongoDB
لماذا تستخدم MongoDB إذا كان لدينا Redis ، تسأل؟ كما ذكرنا سابقًا ، Redis هو مخزن بيانات داخل الذاكرة. على الرغم من أنه يمكنه الاحتفاظ بالبيانات على القرص ، إلا أن استخدام Redis لهذا الغرض ربما لا يكون أفضل طريقة للذهاب. سنستخدم MongoDB لتخزين البيانات الوصفية للصور التي تم تحميلها والرسائل.
يمكننا تنزيل MongoDB من موقعه الرسمي على الإنترنت. في بعض توزيعات Linux ، هذه هي الطريقة المفضلة لتثبيت MongoDB. ومع ذلك ، يجب أن يظل قابلاً للتثبيت باستخدام مدير الحزم الخاص بمعظم التوزيع.
آرتش لينكس:
pacman -S mongodb
أوبونتو:
apt-get install mongodb
نظام التشغيل Mac OS X:
brew install mongodb
في كود Go الخاص بنا ، سنستخدم الحزمة mgo (تُنطق مانجو). لا يقتصر الأمر على اختبار المعارك فحسب ، بل توفر حزمة برنامج التشغيل واجهة برمجة تطبيقات نظيفة وبسيطة حقًا.
إذا لم تكن خبيرًا في MongoDB ، فلا تقلق على الإطلاق. يعد استخدام خدمة قاعدة البيانات هذه ضئيلًا في نموذج التطبيق الخاص بنا ، ويكاد يكون غير ذي صلة بتركيز هذه المقالة: بنية Pub / Sub.
أمازون S3
سنستخدم Amazon S3 لتخزين الصور التي تم تحميلها بواسطة المستخدم. ليس هناك الكثير للقيام به هنا ، باستثناء التأكد من أن لدينا حسابًا جاهزًا لخدمات الويب من أمازون وأن دلوًا مؤقتًا تم إنشاؤه.
لا يعد تخزين الملفات التي تم تحميلها على القرص المحلي خيارًا لأننا لا نريد الاعتماد على هوية عقد الويب الخاصة بنا بأي شكل من الأشكال. نريد أن يكون المستخدمون قادرين على الاتصال بأي من عقد الويب المتاحة وأن يظلوا قادرين على رؤية نفس المحتوى.
للتفاعل مع حاوية Amazon S3 من كود Go الخاص بنا ، سنستخدم AdRoll / goamz ، وهو شوكة حزمة Goamz من Canonical مع بعض الاختلافات.
ريديس
أخيرًا وليس آخرًا: ريديس. يمكننا تثبيته باستخدام مدير حزم التوزيع لدينا:
آرتش لينكس:
pacman -S redis
أوبونتو:
apt-get install redis-server
نظام التشغيل Mac 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 وتشغيله ، ابدأ تشغيل Terminal وأدخل CLI لـ Redis:
redis-cli
حاول إدخال الأوامر التالية ومعرفة ما إذا كنت تحصل على الإخراج المتوقع:
SET answer 41 INCR answer GET answer
يخزن الأمر الأول "41" مقابل المفتاح "الإجابة" ، والأمر الثاني يزيد القيمة ، ويطبع الأمر الثالث القيمة المخزنة مقابل المفتاح المحدد. يجب أن تكون النتيجة "42".
يمكنك معرفة المزيد حول جميع الأوامر التي يدعمها Redis على موقعه الرسمي على الويب.
سنستخدم redigo package للاتصال بـ Redis من داخل كود التطبيق الخاص بنا.
نظرة خاطفة على Redis Pub / Sub
نمط النشر - الاشتراك هو طريقة لتمرير الرسائل إلى عدد عشوائي من المرسلين. مرسلو هذه الرسائل (الناشرون) لا يحددون صراحة المستلمين المستهدفين. بدلاً من ذلك ، يتم إرسال الرسائل على قناة يمكن أن ينتظرها أي عدد من المستلمين (المشتركين).
في حالتنا ، يمكن أن يكون لدينا أي عدد من عقد الويب تعمل خلف موازن التحميل. في أي لحظة ، قد لا يكون هناك مستخدمان ينظران إلى نفس الصورة متصلين بنفس العقدة. هنا يأتي دور Redis Pub / Sub. عندما تحتاج عقدة الويب إلى ملاحظة تغيير (على سبيل المثال ، يتم إنشاء رسالة جديدة بواسطة المستخدم) ، ستستخدم Redis Pub / Sub لبث هذه المعلومات إلى جميع عقد الويب ذات الصلة. والتي بدورها ستنشر المعلومات للعملاء ذوي الصلة حتى يتمكنوا من جلب القائمة المحدثة من messagesredis.
نظرًا لأن نمط النشر والاشتراك يسمح لنا بإرسال رسائل على القنوات المسماة ، فيمكننا توصيل كل عقدة ويب بـ Redis ، والاشتراك فقط في تلك القنوات التي يهتم بها المستخدمون المتصلون بهم. على سبيل المثال ، إذا كان هناك مستخدمان يشاهدان نفس الصورة ولكنها متصلة بنقطتين ويب مختلفتين من العديد من عقد الويب ، عندئذٍ تحتاج عقدتا الويب فقط للاشتراك في القناة المقابلة. سيتم تسليم أي رسالة يتم نشرها على تلك القناة إلى عقدتي الويب هاتين فقط.
يبدو جيدا جدا ليكون صحيحا؟ يمكننا تجربتها باستخدام CLI من Redis. ابدأ ثلاث حالات من redis-cli
. نفّذ الأمر التالي في المقام الأول:
SUBSCRIBE somechannel
نفّذ الأمر التالي في مثيل Redis CLI الثاني:
SUBSCRIBE someotherchannel
نفّذ الأوامر التالية في المثيل الثالث من Redis CLI:
PUBLISH somechannel lorem PUBLISH someotherchannel ipsum
لاحظ كيف تلقى المثال الأول "lorem" ولكن ليس "ipsum" ، وكيف تلقى المثيل الثاني "ipsum" ولكن ليس "lorem".
من الجدير بالذكر أنه بمجرد دخول عميل 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 and http: // localhost: 9093.
خلف الكواليس
بدلاً من متابعة كل خطوة في تطوير التطبيق ، سنركز على بعض الأجزاء الأكثر أهمية. على الرغم من أن كل هذه العناصر ليست ذات صلة بنسبة 100٪ بـ Redis Pub / Sub وآثاره في الوقت الفعلي ، إلا أنها لا تزال ذات صلة بالهيكل العام للتطبيق وستجعل من السهل متابعتها بمجرد التعمق أكثر.
لتبسيط الأمور ، لن نهتم بمصادقة المستخدم. ستكون عمليات التحميل مجهولة المصدر ومتاحة لكل من يعرف عنوان URL. يمكن لجميع المشاهدين إرسال رسائل ، وسيكون لديهم القدرة على اختيار الاسم المستعار الخاص بهم. يجب أن يكون تكييف آلية المصادقة المناسبة وإمكانيات الخصوصية أمرًا تافهًا ، ويتجاوز نطاق هذه المقالة.
البيانات المستمرة
هذا سهل.
عندما يقوم المستخدم بتحميل صورة ، نقوم بتخزينها في Amazon S3 ثم نقوم بتخزين المسار إليها في MongoDB مقابل معرفين: معرف كائن BSON واحد (المفضل لدى MongoDB) ، ومعرف قصير آخر مكون من 8 أحرف (يرضي بعض الشيء للعينين). يذهب هذا إلى مجموعة "تحميلات" قاعدة البيانات الخاصة بنا وله هيكل مثل هذا:
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"` }
الشيء الوحيد المثير للاهتمام هنا هو حقل معرف التحميل ، والذي يستخدم لربط الرسائل بتحميل معين.
نقاط نهاية API
يحتوي هذا التطبيق بشكل أساسي على ثلاث نقاط نهاية.
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 واستخدامه لتحميل الصورة إلى Amazon S3 قبل الاستمرار في تحميل كيان التحميل إلى MongoDB.
هناك إيجابيات وسلبيات للطريقة التي يتم بها إنشاء معرفات كائن BSON. يتم إنشاؤها في نهاية العميل. ومع ذلك ، فإن الاستراتيجية المستخدمة في إنشاء معرف الكائن تجعل احتمال الاصطدام ضئيلًا للغاية بحيث يكون من الآمن توليدها من جانب العميل. من ناحية أخرى ، عادةً ما تكون قيم معرفات الكائنات المُنشأة متسلسلة وهذا شيء لا تحبه Amazon S3 تمامًا. الحل السهل لهذا هو أن تبدأ اسم الملف بسلسلة عشوائية.
احصل على / api / uploads / {id} / messages
تُستخدم واجهة برمجة التطبيقات هذه لجلب الرسائل الحديثة والرسائل التي تم نشرها بعد وقت معين.
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()) }
هذا المعالج مشابه جدًا للمعالج الآخر لدرجة أنه من الممل تقريبًا تضمينه هنا. أو هو؟ لاحظ كيف يوجد محور استدعاء دالة .Emit () في نهاية الوظيفة. ما هو المحور الذي تقوله؟ هذا هو المكان الذي يحدث فيه كل سحر Pub / Sub.
المحور: حيث تلتقي WebSockets مع Redis
Hub هو المكان الذي نلصق فيه WebSockets بقنوات Redis Pub / Sub. ومن قبيل المصادفة أن الحزمة التي نستخدمها للتعامل مع WebSockets داخل خوادم الويب الخاصة بنا تسمى الغراء.
يحافظ Hub بشكل أساسي على عدد قليل من هياكل البيانات التي تنشئ تعيينًا بين جميع WebSockets المتصلة لجميع القنوات التي يهتمون بها. على سبيل المثال ، يشير WebSocket في علامة تبويب متصفح المستخدم إلى صورة معينة تم تحميلها يجب أن تكون مهتمة بطبيعة الحال بجميع الإخطارات ذات الصلة إليها.
تنفذ حزمة المحور ست وظائف:
- الإشتراك
- إلغاء الاشتراك
- ينبعث
- EmitLocal
- InitHub
- المقبض
الاشتراك وإلغاء الاشتراكالجميع
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 }
هذه الوظيفة ، تمامًا مثل معظم الوظائف الأخرى في هذه الحزمة ، تحمل قفلًا على كائن القراءة / الكتابة أثناء التنفيذ. هذا حتى نتمكن من تعديل المتغيرات والمواضيع لهياكل البيانات البدائية بأمان. المتغير الأول ، المقابس ، يرسم المقابس لأسماء القنوات ، بينما المتغير الثاني ، الموضوعات ، يرسم أسماء القنوات إلى مآخذ التوصيل. في هذه الوظيفة نبني هذه الخرائط. عندما نرى اشتراك المقبس في اسم قناة جديد ، فإننا نجعل اتصال Redis الخاص بنا ، والرقم الفرعي ، والاشتراك في تلك القناة على Redis باستخدام subconn.Subscribe . هذا يجعل 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 }
عندما نزيل المقبس الأخير من بنية البيانات المهتمة بقناة معينة ، فإننا نلغي الاشتراك من القناة في Redis باستخدام subconn .
ينبعث
func Emit(t string, m string) error { _, err := pubconn.Do("PUBLISH", t, m) return err }
تنشر هذه الوظيفة رسالة m على القناة t باستخدام اتصال النشر إلى 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 }
في وظيفة InitHub ، نقوم بإنشاء اتصالين بـ Redis: أحدهما للاشتراك في القنوات التي تهتم بها عقدة الويب هذه ، والآخر لنشر الرسائل. بمجرد إنشاء الاتصالات ، نبدأ روتين Go جديدًا مع حلقة تعمل إلى الأبد في انتظار تلقي الرسائل من خلال اتصال المشترك بـ Redis. في كل مرة يتلقى فيها رسالة ، يرسلها محليًا (أي إلى جميع WebSockets المتصلة بعقدة الويب هذه).
المقبض
وأخيرًا ، فإن HandleSocket هو المكان الذي ننتظر فيه وصول الرسائل عبر WebSockets أو تنظيفها بعد إغلاق الاتصال:
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 للواجهة الأمامية الخاصة به ، فمن الأسهل بكثير التعامل مع WebSockets (أو الرجوع إلى استقصاء XHR عند عدم توفر WebSockets):
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}"
- انقر على الصورة: “المس: {تنسيق} ، {تنسيق}” ، حيث يمثل الإحداثيات والإحداثيات النسبة المئوية للإحداثيات القائمة على موقع نقر المستخدم على الصورة
عندما ينشئ المستخدم رسالة جديدة ، نستخدم واجهة برمجة تطبيقات "POST / api / uploads / {uploadID} / messages" لإنشاء رسالة جديدة. يتم ذلك باستخدام التابع 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())
نظرة عامة
عندما يكتب المستخدم رسالة ويضغط على مفتاح الإدخال ، يستدعي العميل نقطة نهاية واجهة برمجة التطبيقات "POST / api / uploads / {id} / messages". يؤدي هذا بدوره إلى إنشاء كيان رسالة في قاعدة البيانات ونشر سلسلة "message: {messageID}" عبر Redis Pub / Sub على القناة "تحميل: {uploadID}" من خلال حزمة المحور.
يعيد Redis توجيه هذه السلسلة إلى كل عقدة ويب (مشترك) مهتم بالقناة "تحميل: {uploadID}". تتكرر عقد الويب التي تتلقى هذه السلسلة عبر جميع WebSockets ذات الصلة بالقناة وترسل السلسلة إلى العميل عبر اتصالات WebSocket الخاصة بهم. يبدأ العملاء الذين يتلقون هذه السلسلة في جلب الرسائل الجديدة من الخادم باستخدام "GET / api / uploads / {id} / messages".
وبالمثل ، لنشر أحداث النقر على الصورة ، يرسل العميل مباشرةً رسالة عبر WebSocket تبدو مثل "تحميل اللمس: {uploadID} {formatX} {formatY}". تنتهي هذه الرسالة في حزمة الموزع حيث يتم نشرها على القناة نفسها "تحميل: {uploadID}". نتيجة لذلك ، يتم توزيع السلسلة على جميع المستخدمين الذين ينظرون إلى الصورة التي تم تحميلها. عند استلام العميل لهذه السلسلة ، يوزعها لاستخراج الإحداثيات ويعرض دائرة تتلاشى لتبرز موقع النقر للحظات.
يتم إحتوائه
لقد رأينا في هذه المقالة لمحة عن كيف يمكن لنمط النشر والاشتراك أن يساعد في حل مشكلة توسيع نطاق تطبيقات الويب في الوقت الفعلي إلى حد كبير وبسهولة نسبية.
نموذج التطبيق موجود ليكون بمثابة ساحة لعب لتجربة Redis Pub / Sub. ولكن ، كما ذكرنا سابقًا ، يمكن تنفيذ الأفكار في أي لغة برمجة شائعة أخرى تقريبًا.