اپلیکیشن‌های Real-Time، از HTTP تا WebSocket


اپلیکشین‌های تحت وب، در ابتدا، مطابق با مدل client-server طراحی و پیاده سازی می‌شدند. این اپلیکیشن‌ها، نیاز ارتباطی خودشون رو با استفاده از HTTP که یک پروتکل client-server هست، برطرف می‌کردند. هر زمان که نیاز داشتند، درخواستی برای سرور ارسال می‌کردند و دیتای مورد نظر رو از سرور می‌گرفتند.

پروتکل HTTP برای این نوع از وب اپلیکشین‌ها کاملا مناسب بود. اما با گذشت زمان، وب اپلیکیشن‌هایی با نیازمندی‌ جدید تحت عنوان وب اپلیکیشن‌های Real-Time شروع به گسترش کردند.

منظور از real-time بودن اینه که وقتی رویدادی (event) توی سیستم اتفاق می‌افته، قبل از اینکه اهمیتش رو از دست بده، بتونیم نسبت بهش واکنش نشون بدیم. مثلا در اپلیکیشن تاکسی اسنپ، زمانی که راننده درخواست سفر رو قبول می‌کنه، در واقع یک event توی سیستم رخ داده. اپلیکیشن مسافر باید این قابلیت رو داشته باشه که بتونه در سریع‌ترین زمان ممکن، رخ دادن این اتفاق رو متوجه بشه و کاربر رو ازش مطلع کنه.

درنتیجه، مدل ارتباطی client-server جوابگوی نیاز این نوع از وب اپلیکیشن‌ها نیست.

توی این نوشته، Polling رو به عنوان اولین و ساده‌ترین راه ارتباطی در اپلیکیشن‌های real-time بررسی می‌کنیم و باهم می‌بینیم که چرا بهتره از WebSocket استفاده کنیم و در حین پیاده سازیش، به چه نکاتی باید دقت کنیم.


پیش‌نیاز

برای خوندن این نوشته نیازه که با مفاهیم WebSocket ، HTTP و MQTT تا حدی آشنایی داشته باشید. اگر نیاز داشتید بیشتر راجع به این موضوع‌ها بدونید، می‌تونید از لینک هایی که براتون گذاشتم استفاده کنید.


استفاده از Polling

اولین و ساده‌ترین راه از نظر پیاده‌سازی، استفاده از تکنیک پولینگه. پولینگ یه تکنیک بر پایه HTTP ست.

عملکرد Polling به این صورته که به طور مداوم و در بازه‌های زمانی مساوی، درخواست‌هایی از سمت کلاینت برای سرور ارسال می‌شه و سرور event های جدید رو به کلاینت برمی‌گردونه.

در واقع polling، یه راهیه برای تطبیق دادن مدل ارتباطی client-server با نیازمندی اپلیکیشن‌های real-time.


دو روش پیاده‌سازی Polling

۱- روش Short Polling: در بازه‌های زمانی مساوی، کلاینت درخواستش رو برای سرور ارسال می‌کنه و سرور پاسخ این درخواست رو بلافاصله به کلاینت برمی‌گردونه. اگرچه ممکنه در بیشتر مواقع event جدید رخ نداده باشه و کلاینت ریسپانس خالی دریافت کنه.


به طور خلاصه، short polling به صورت زیر عمل می‌کنه:

15:30:00: اسنپ من رسید؟
15:30:01: نه!
15:30:10: اسنپ من رسید؟
15:30:11: نه!
15:30:20: اسنپ من رسید؟
15:30:21: بله ^^


۲- روش Long Polling: بعد از اینکه کلاینت درخواستش رو برای سرور ارسال می‌کنه،‌ سرور سعی می‌کنه که تا جای ممکن connection کلاینت رو باز نگه داره و فقط زمانی که event جدیدی اتفاق افتاده بود و یا از نظر زمانی، به یک threshold ای رسیده بود، ریسپانس رو به کلاینت برگردونه.

به طور خلاصه، long polling به صورت زیر عمل می‌کنه:

15:30:00: اسنپ من رسید؟
15:30:17: بله ^^



چرا به جای Polling از WebSocket استفاده کنیم؟

روش Polling بر پایه HTTP ست و به همین علت، هم پیاده‌سازی راحتی داره و هم در بیشتر دستگاه‌ها پشتیبانی می‌شه. اما این روش، معایبی هم داره که باعث می‌شه نتونیم به عنوان یه روش کارآمد، ازش استفاده کنیم.


معایب Polling

۱- وقتی از روش polling استفاده می‌کنیم، هربار که قراره درخواست جدیدی ارسال بشه،‌ باید یک connection برقرار بشه، هدرهای HTTP پارس بشه، در سمت سرور برای دیتای جدید query زده بشه و ریسپانس ساخته و به کلاینت تحویل داده بشه. در نهایت هم resource cleanup انجام بشه. تکرار این روند، سربار زیادی داره.

۲- تو روش Polling، ممکنه در بیشتر مواقع در اپلیکیشن event خاصی رخ نداده باشه و در نتیجه، سرور دیتای خاصی برای فرستادن نداشته باشه و مجبور باشه که ریسپانس خالی برای کلاینت بفرسته.

مثلا فرض کنید در اپلیکیشن تاکسی اسنپ، در حین سفر قراره ۵ تا ایونت اتفاق بی‌افته که اپلیکیشن کلاینت باید از اون‌ها با خبر بشه. اگر فرض کنیم مدت زمان کل سفر ۳۰ دقیقه و فاصله بین هر درخواست Polling با درخواست بعدی ۱۰ ثانیه باشه، در کل زمان سفر، ۱۸۰ تا درخواست HTTP از سمت کلاینت برای سرور فرستاده می‌شه که از بین این تعداد، فقط ۵ تای اون‌ها ایونتی رو به دست کلاینت می‌رسونن و برای ما اهمیت دارن.

در نتیجه، Polling روش خوب و ساده‌ایه ولی سربار زیادی داره. پس بهتره به دنبال یه روش دیگه باشیم.

اگه دوست داشتید که بیشتر راجع به Long Polling بدونید، می‌تونید از این لینک استفاده کنید.


استفاده از  WebSocket


وب سوکت یه پروتکل ارتباطیه که به کمکش می‌تونیم بین کلاینت و سرور، یه ارتباط دوطرفه و پایدار داشته باشیم.

زمانی که کلاینت می‌خواد یه کانکشن از نوع WebSocket داشته باشه، فقط کافیه که یک درخواست HTTP با تعدادی هدر استاندارد برای سرور ارسال کنه و سرور و کلاینت توافق کنن که TCP connection فعلی رو upgrade کنن، اون رو باز نگه دارن و ازش برای انتقال پیام‌های بعدی استفاده کنن.

هدر‌هایی که کلاینت باید ارسال کنه:

Connection: Upgrade
Upgrade: websocket


در WebSocket پیام‌ها باید چه شکلی باشن؟

وب سوکت یه پروتکل سطح پایینه که بعد از برقراری ارتباط بین کلاینت و سرور، امکان ارسال دیتا رو به ما میده اما، مشخص نمی‌کنه که دیتا باید به چه شکلی باشه و این موضوع رو بر عهده کلاینت و سرور قرار میده. (دیتا باید از نوع binary یا text باشه اما ساختارش محدودیتی نداره).
در نتیجه، برای استفاده بهتر از وب سوکت، کلاینت و سرور بهتره که یک پروتکل در سطح اپلیکیشن (application-level) انتخاب کنند که ساختار دیتا رو مشخص کنه. به بیان دیگه، ما نیاز به یک subprotocol داریم.


چطوری باید SubProtocol رو مشخص کنیم؟


در زمان ارسال HTTP Request اولیه، کلاینت لیستی از subprotocol‌های دلخواهش رو توی هدر Sec-WebSocket- Protocol می‌فرسته و سرور باید یکی از اون‌هارو انتخاب کنه و در ریسپانس همین درخواست، کلاینت رو از انتخاب خودش مطلع کنه.

مثلا هدر request که از سمت کلاینت ارسال می‌شه به صورت زیره:

Sec-WebSocket-Protocol: wamp, mqtt

و اگر به فرض مثال سرور mqtt رو به عنوان subprotocol انتخاب کنه، هدر زیر توی ریسپانس برای کلاینت ارسال می‌شه:

Sec-WebSocket-Protocol: mqtt


زمانی که از HTTP استفاده می‌کنیم، با هر درخواست هدر‌ها و کوکی‌هایی هم به سمت سرور ارسال می‌شه که به کمک اون‌ها می‌تونیم احراز هویت رو انجام بدیم. اگه قرار باشه از webSocket استفاده کنیم که هیچ header و کوکی‌ای نداره، احراز هویت رو به چه صورت می‌تونیم انجام بدیم؟



احراز هویت در WebSocket

در پروتکل WebSocket، برخلاف HTTP، در هنگام ارسال درخواست، هدر و کوکی‌‌ای از سمت کلاینت ارسال نمی‌شه. در نتیجه، WebSocket نسبت به HTTP سربار کمتری داره، اما از طرفی، نیاز به یک راه برای authenticate کردن کلاینت داره. subprotocol‌ای که برای WebSocket انتخاب می‌کنیم، می‌تونه راه‌حل‌هایی هم برای این موضوع داشته باشه.



استفاده از MQTT به عنوان Subprotocol

پروتکل MQTT، یه پروتکل سبک شبکه است که هدفش تامین نیاز ارتباطی کلاینت‌هایی هست که ظرفیت باطری و bandwidth زیادی ندارند. MQTT طبق مدل publish-subscribe کار می‌کنه و می‌تونیم برای انتقال پیام ازش استفاده کنیم. در این مدل، به کسی که منتظر دریافت پیام هست، subscriber و به کسی که اون پیام رو ارسال می‌کنه، publisher گفته می‌شه. وظیفه انتقال پیام‌ها از publisher به subscriber بر عهده broker هست. broker از مفهومی به اسم topic استفاده می‌کنه تا بتونه پیام‌های دریافتی از publisher ها رو برای subscriber‌ها فیلتر کنه. در نتیجه، یک subscriber فقط پیام‌هایی که نیاز داره رو دریافت کنه.

کاربرد اصلی پروتکل MQTT در زمینه IOT ست و این پروتکل به صورت مستقیم در وب پیاده‌سازی نشده. اما می‌تونیم با کمک WebSocket، از این پروتکل در وب اپلیکیشن‌ها هم استفاده کنیم. در واقع می‌تونیم از MQTT به صورت over webSocket استفاده کنیم.

علت انتخاب MQTT به عنوان subprotocol برای webSocket، قابلیت‌های خوب MQTT از جمله سبک بودن پروتکل، reliable بودن ارسال پیام‌ها و داشتن راه‌حل برای authentication است.


پیاده‌سازی WebSocket به کمک MQTT

برای پیاده‌سازی webSocket و MQTT از پکیج MQTT.js استفاده می‌کنیم. این پکیج پروتکل MQTT  رو بر روی webSocket پیاده‌سازی کرده و تمام نیازمندی‌های مطرح شده در webSocket رو شامل می‌شه.

برای اینکه تمام قابلیت‌های مرتبط با MQTT رو در کنار هم داشته‌باشیم، در پیاده‌سازی از class استفاده می‌کنیم.

MqttClient Class
MqttClient Class

در ابتدا، کلاس MqttClient رو تعریف می‌کنیم. در constructor این کلاس، دو چیز رو مشخص می‌کنیم؛ اول آدرس host ای که باید به اون متصل بشه و سپس topic ای که قراره پیام‌های مربوط به اون رو دریافت کنه.

قبل از subscribe کردن روی یک تاپیک، نیاز داریم که به سرور متصل بشیم. این کار رو به کمک متد connect انجام می‌دیم.

Connect Method
Connect Method

خروجی mqtt.connect ابجکت client هست که با کمک این ابجکت، به تمام قابلیت‌های MQTT دسترسی داریم.

حالا می‌تونیم کلاینتمون رو connect و بعد از اون متد subscribe رو پیاده‌سازی کنیم.

Subscribe method
Subscribe method

برای subscribe کردن، اول به سرور متصل می‌شیم. همچنین برای استفاده از client در زمان unsubsribe، اون رو به عنوان یک property از کلاس MqttClient ذخیره می‌کنیم.

با استفاده از ایونت connect می‌تونیم متوجه بشیم که کلاینتمون چه زمانی به سرور متصل می‌شه.

بعد از اتصال به سرور، می‌تونیم کلاینتمون رو روی تاپیک از قبل تعیین شده subscribe کنیم.

در نهایت هم اگر پیامی از سمت سرور به دستمون برسه، با ایونت message می‌تونیم ازش مطلع بشیم.


دیدیم که پیاده‌سازی MQTT نسبتا ساده‌ست و امکانات زیادی رو به ما میده. اما وقتی از MQTT استفاده می‌کنیم، یک چالش خیلی مهم داریم که در قسمت بعدی قراره با هم بررسیش کنیم و براش راه‌حل پیدا کنیم.


وقتی اپلیکیشن کلاینت به background میره چه اتفاقی می‌افته؟

وقتی از پروتکل webSocket استفاده می‌کنیم و کاربر، اپلیکیشن رو به background می‌فرسته، ارتباط webSocket قطع می‌شه و دیگه قادر به گرفتن مسیج‌های که از سمت سرور میاد، نیست.
خود پروتکل webSocket قابلیت اتصال مجدد خودکار به سرور رو برای ما فراهم نمی‌کنه و این موضوع رو برعهده خودمون قرار داده.

پس از بازگشت اپلیکیشن کلاینت به foreground، برای اتصال مجدد به سرور می‌تونیم از client library های نوشته‌شده برای webSocket مثل socket.io، ws و socktjs استفاده کنیم.


خوشبختانه subprotocol ای که ما برای webSocket انتخاب کردیم، امکان اتصال مجدد رو هم بهمون میده. توی پکیج MQTT.js، برای تنظیم کردن وقفه بین هربار تلاش برای reconnect شدن می‌تونید از reconnectPeroid استفاده کنید. همچنین برای اینکه بدونید چه زمانی MQTT داره سعی می‌کنه reconnect بشه، می‌تونید از ایونت reconnect و یا هوک transformWsUrl استفاده کنید.


آیا ایونت‌هایی که در زمان disconnect بودن اتفاق می‌افتن از دست میرن؟

زمانی که کلاینت disconnect می‌شه (مثلا اینترنت قطع می‌شه و یا اپلیکیشن به حالت background میره)، نمی‌تونه ایونت‌هایی که اتفاق می‌افته رو دریافت کنه؛ در نتیجه ممکنه دیتاها و ایونت‌های مهمی رو از دست بدیم.

مثلا، زمانی که کاربر در اپلیکیشن تاکسی اسنپ منتظر رسیدن راننده‌ست و تا اون زمان صفحه گوشیش رو lock می‌کنه و گوشی رو در جیبش قرار می‌ده، ارتباط webSocket قطع می‌شه. اگر در همین حین، راننده به مبدا برسه و یا ایونت مهمی رخ بده، با توجه به قطع بودن ارتباط، اون ایونت‌هارو از دست می‌دیم. چطوری وجود ایونتی که قبل تر اتفاق افتاده و از دست رفته رو متوجه بشیم و او‌ن‌ رو به کاربر اطلاع بدیم؟


استفاده از QOS

یکی دیگه از امکانات خوبی که MQTT به ما می‌ده، QOS هست. QOS یا quality of service قراردادی‌ست بین گیرنده و ارسال کننده که وظیفه‌اش اطمینان از رسیدن مسیج به دست گیرنده‌ست. QOS سه تا مقدار مختلف می‌تونه بگیره:

⭐️ مقدار ۰: دیتا یک‌بار ارسال می‌شه اما چک نمی‌شه که آیا توسط طرف دیگه دریافت شده یا نه.

⭐️⭐️ مقدار ۱: تضمین می‌شه که گیرنده حداقل یک بار پیام رو دریافت می‌کنه؛ اما ممکنه که پیام تکراری هم دریافت کنه.

نحوه عملکردش به این صورته که فرستنده بعد از ارسال پیام، اون رو ذخیره می‌کنه و اگر در زمان معقولی، سیگنال PUBACK رو از سمت گیرنده دریافت نکنه، پیام مجددا برای گیرنده ارسال می‌شه. درنتیجه در این حالت ممکنه گیرنده پیام تکراری دریافت کنه.

⭐️⭐️⭐️ مقدار۲: این مقدار بالاترین سطح quality of service هست و تضمین می‌کنه که هر پیام دقیقا یک‌بار به دست گیرنده می‌رسه.

نحوه عملکردش به این صورته که فرستنده بعد از ارسال پیام، اون رو ذخیره می‌کنه. گیرنده پس از دریافت پیام، سیگنال PUBREC رو برمی‌گردونه. اگر فرستنده این سیگنال رو دریافت نکنه، پیام مجددا برای گیرنده ارسال می‌شه.

زمانی که فرستنده سیگنال PUBREC رو دریافت کرد، می‌تونه با خیال راحت پیامی که ذخیره کرده بود رو پاک کنه و بعد از انجام این کار، سیگنال PUBREL رو برای گیرنده ارسال می‌کنه. گیرنده هم بعد از دریافت این سیگنال، میتونه تمام state هایی که در سمت خودش ذخیره کرده رو پاک کنه. در نهایت هم سیگنال PUBCOM رو به دست فرستنده می‌رسونه و اون هم میتونه تمام state های مربوط به او پیام رو در سمت خودش پاک کنه.


چنتا نکته مربوط به QOS:

  • ازونجایی که broker وظیفه رسیدگی به QOS بین گیرنده و فرستنده رو به عهده داره، فرستنده و گیرنده میتونن QOS‌های مختلفی نسبت به هم داشته باشند.
  • استفاده از QOS 2 تضمین می‌کنه که تمام پیام ها به دست گیرنده میرسه اما به علت بیشتر بودن مراحل بین فرستنده و گیرنده، این ارسال پیام نسبت به مقادیر 0 و  1 کندتر خواهد بود.
  • زمانی که از QOS با مقادیر 1 و 2 استفاده میکنیم، پیامی که در سمت فرستنده ذخیره می‌شه در واقع داخل یک صف (queue) قرار میگیره.
  • اگر تصمیم بگیریم که از QOS با مقدار 1 استفاده کنیم، اپلیکیشن کلاینت رو باید طوری پیاده‌سازی کنیم که با دریافت پیام‌های تکراری، مشکلی براش به وجود نیاد.



پس برای حل مشکل پیام‌های از دست رفته در زمان disconnect بودن، می‌تونیم از QOS با مقادیر 1 یا 2 استفاده کنیم. در این حالت، زمانی که کلاینت مجددا connect می‌شه، تمام ایونت‌هایی که در زمان نبودنش رخ داده رو دریافت می‌کنه.


اما استفاده از این روش، در همه شرایط ممکن نیست. می‌تونید حدس بزنید در چه شرایطی؟




زمانی که اپلیکیشن ما تعداد زیادی کاربر داشته باشه، ذخیره کردن پیام‌ها در سمت سرور سربار بسیار زیادی خواهد داشت و ممکنه که مشکلات زیادی رو به وجود بیاره. در نتیجه، استفاده از این روش منطقی نخواهد بود.


اگر نتونیم از QOS 2 استفاده کنیم، چیکار باید بکنیم؟

هدف از داشتن event و فرستادن اون‌ها به سمت کلاینت، اینه که کلاینت بتونه بر‌اساس اینکه چه ایونتی رخ داده، یک state ای رو آپدیت کنه و در نتیجه‌ی آپدیت کردن اون state، تغییری در اپلیکیشن کلاینت رخ بده (مثلا پیامی به کاربر نشون داده بشه).

ما با استفاده از QOS 2 این امکان رو داشتیم که کلاینتمون در لحظه connect شدن، بتونه ایونت‌هایی که در نبودنش رخ داده رو بگیره و براساس اون ایونت‌ها، state های خودش رو آپدیت کنه.

پس در نهایت هدف اصلی ما، آپدیت کردن state هایی‌ست که در سمت اپلیکیشن کلاینت ذخیره شده‌اند.


یه راه دیگه برای رسیدن به این هدف، پیاده‌سازی و استفاده از HTTP Endpoint ای‌ست که بتونه در هر لحظه، آپدیت‌ترین دیتاهارو به ما بده. مثلا زمانی که اپلیکیشن کلاینت از background به foreground برمیگرده، می‌تونیم از این endpoint استفاده کنیم و state های آپدیت شده رو بگیریم و در نهایت ذخیره کنیم. در نتیجه هر ایونتی که در زمان background بودن کلاینت رخ داده رو خواهیم داشت.

مثلا برای زمانی که کلاینت از background به foreground می‌ره، می‌تونیم از کد زیر استفاده کنیم:


در انتها از همتون ممنونم که وقت گذاشتید و این نوشته رو خوندید. امیدوارم که بهتون کمک کرده باشه. اگر پیشنهاد و یا ایده‌ای دارید، خوشحال می‌شم از طریق کامنت و یا راه‌های ارتباطی زیر باهام درمیون بگذارید.

توییتر

لینکدین

ریپازیتوری کدهای این نوشته رو هم می‌تونید اینجا ببینید.