محمد حسینی راد
محمد حسینی راد
خواندن ۵ دقیقه·۲ سال پیش

آشنایی با Long polling و پیاده‌سازی آن در Go

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

هی ریکوئست بزنیم!

یک راه حل ساده ایی که به ذهنمون میاد اینه هی ریکوئست بزنیم! چطور؟ یک endpoint پیاده سازی کنیم که اطلاعات تخته رو میده. وقتی حرکتی رو انجام میدیم هر ۲ ثانیه یه بار ریکوئست میزنیم به این اندپوینت تا اگر حریفمون حرکتی انجام داد متوجه بشیم. بیاید کدش رو بزنیم:

کد ما یک منطق خیلی ساده رو پیاده سازی میکنه. یک متغیر Boolean داریم به اسم didTheMove که مشخص میکنه حریف ما حرکتش رو انجام داده یا نه. حریفمون با صدا زدن اندپوینت /move میتونه حرکتش رو ثبت کنه و وقتی این کارو میکنه متغیر ما true میشه. این وسط سیستم من هر ۱ دقیقه یک بار داشته /status رو صدا میزده و بلاخره وقتی حریف حرکتشو انجام بده با true شدن متغیر من متوجه این اتفاق میوفتم.

امااا! این حرکت یک سری مشکل داره

  • اگر ما هر یک ثانیه یک بار ریکوئست بزنیم داریم برای هر بازی فعال ۲ ریکوئست برثانیه به سرور میزنه که هم سمت موبایل کاربر باز کردن I/O برای زدن ریکوئست هرثانیه عملیات سنگینیه و اگه گوشیش قدیمی باشه فشار میاد روش هم برای سرور ما سنگین میشه چون اگه ۲۰۰ بازی همزمان داشته باشیم ۴۰۰ بار در ثانیه باید I/O فقط برای هندل کردن HTTP باز کنیم.
  • مشکل اساسی دوم کد ما اینه که real-time نیست. ما داریم هر یک ثانیه یک بار ریکوئست میزنیم و با یک دیلی یک ثانیه ایی اتفاق ها رو نشون کاربر میدیم. حالا اگه فشار بیاد روی سرور بخاطر مشکل قبلی میگیم عیب نداره ۲ش کنیم و این اتفاق هی میوفته و ریسپانسیو بودن بازی کمتر و کمتر میشه.

یک شبیه سازی کنیم

فرض کنید یک سشن بازی اینطور داریم که:

  • بازی ۵ دقیقه طول میکشه.
  • هر ۲۰ ثانیه یک بار بازیکن ها حرکتشون رو انجام میدن.

یعنی توی این ۵ دقیقه ما ۱۵ بار تختمون آپدیت میشه. اما با این حرکت داریم ۶۰۰ بار به بک‌اند ریکوئست میزنیم. عدد ۶۰۰ یادمون باشه به عنوان بنچ مارک که جلوتر بتونیم راه حل های دیگه رو با هم مقایسه کنیم.

استفاده از سوکت - یک کانکشن یک بار برای همیشه

با استفاده از سوکت ما یک کانکشن TCP با سرور باز میکنیم و هر اتفاقی که بیوفته سرور از طریق همون کانکشن بهمون اطلاع میده. این یعنی توی شبیه سازیمون ۲ تا کانکشن باز میکنیم و اکشن ها از طریق همین کانکشن ها بین کلاینت و بک‌اند رد و بدن میشن. این خیلی خوبه! اما مشکلاش چیه؟!

  • سوکت پروگرمینگ تجربه میخواد، باز نگه داشتن کانکشن‌ها، پینگ کردن و ... یک دنیای جدیدیه که تجربه کردنش نیاز به تجربه داره
  • داریم یک تکنولوژی جدید رو به کلاینت و فرانت و بک‌اند فورس میکنیم.
  • اگر کدمون زده شده نیاز به یک ریفکتور اساسی توی لایه presentation داریم که بتونیم سوکت رو ارائه بدیم.

خلاصه بگم. سوکت سخته! آدم ترجیح میده سمتش نره مگه اینکه واقعا نیازی باشه که استفاده ازش به دردسرهاش بچربه!

لانگ پولینگ! همون کد HTTP ولی هوشمندانه تر

بیاید حرکتی که قبلا زدیم رو مرور کنیم. ما یک اندپوینت داشتیم که کلاینت هر ثانیه بهش ریکوئست میزد تا بفهمه وضعیت تخته چطوره. بیاید یکم منطقش رو تغییر بدیم. وقتی کلاینت ریکوئست زد و تخته آپدیت نشده بود کاری نکنیم! کانکشنش رو باز نگه داریم. برای این کار باید به کلاینت بگیم تایم اوت رو مثلا بذار روی ۱ دقیقه! ریکوئستی که زده همینطوری بازه تا وقتی که حریف حرکتی انجام بده.

حالا وقتی حریف حرکتشو انجام داد از طریق سیگنال یا ایونتی به ریکوئست status ایی که زده بودیم میگیم حریف حرکتشو انجام داده و اونجا آپدیت جدید تخته رو نشون میدیم.

یعنی اگر من یک حرکتی انجام بدم و ۱۵ ثانیه بعد حریفم حرکتشو انجام بده اینطور میشه که بعد از انجام حرکتم یک ریکوئست میزنم به /status و این ریکوئست باز میمونه و منتظر جوابه و ثانیه ۱۵ که حریفم حرکتشو انجام میده بعد از ۱۵ ثانیه ریکوئستم جواب میگیره که تخته‌ی اپدیت شده هست.

ببریم توی شبیه‌سازی ایی که کردیم

همون طوری که گفتیم ۱۵ اکشن توی ۵ دقیقه داریم. یعنی توی این ۵ دقیقه ما در کل ۳۰ کانکشن جدید باز میکنیم (که I/O سرور و کلاینت نفس راحت میکشن). اما بازم مثل اگه ۲۰۰ بازی همزمان داشته باشیم ۴۰۰ کانکشن باز داریم در ثانیه. ولی این عدد اصلا زیاد نیست! توجه کنید توی سوکت هم عدد همینه. ما یک کانکشن باز داریم نه اینکه کانکشن جدید باز کنیم!حالا برای همون ۲۰۰ بازی همزمان چند کانکشن جدید (ریکوئست) برثانیه داریم؟ 15/300*400 که میشه ۲۰ ریکوئست برثانیه جای ۴۰۰ ریکوئست برثانیه حالت اولیه. I/O ما نفس راحت میکشه! برای سوکت این عدد میشه ۲.۵.

درسته که ۲.۵ خیلی کمتر از ۲۰ هست ولی این عدد ها برای ۲۰۰ بازی همزمان برثانیه خیلی عدد خوبیه. کد ما اسکیل پذیره و خیلی راحت با یک سری تغییر ساده توی کدبیس قبلیمون میتونیم بهش برسیم جای اینکه کلی دردسر سر سوکت بکشیم! حالا چطور کدشو بزنیم؟ بریم مثال قبلی رو ادیت کنیم:

خلاصه بخوام بگم ما یک چنل توی Go ساختم که منتظر میمونیم تا وقتی حرکت انجام بشه یک دیتای خالی توش ریخته میشه و بعد به کاربر اطلاع میدیم. نکته ایی که هست توی select case که منتظر میمونیم دیتایی توی channel ریخته شه ۲ تا حالت دیگه رو هم چک میکنیم. اگه بیشتر از یک دقیقه گذشته بود یا اگر کاربر کانکشن رو کنسل کرد در هردوحالت دیگه گوش دادن به چنل رو متوقف میکنیم چون دیگه کاربر روی اون goroutine نیست و اگه این کارو نکنیم goroutine leak میدیم.

مشاهده کد در گیت

حرف آخر

لانگ پولینگ حرکت جالبیه. تا جایی که من میدونم یک سری از استارتاپ های بزرگ ایرانی برای چتشون و حتی تلگرام برای دادن آپدیت‌های ربات به وب سرورهاشون از این روش استفاده میکنه که نه تنها ساده هست مشکل Real-time بودن رو حل میکنه و پیاده سازی راحتش توی Go هم یک دلیل دیگه برای عاشق این زبان بودنه :)

golangلانگ پولینگبازی سازیback endسرویس چت
یک مهندس نرم‌افزار در دیوار.
شاید از این پست‌ها خوشتان بیاید