با یک مثال ساده شروع کنم. فرض کنید میخوایم بکاند یک بازی رو پیاده سازی کنیم که نوبتی باشه. مثلا منچ! شطرنج یا حتی دوز! خلاصش اینه من وقتی یک حرکتی انجام میدم باید منتظر بمونم که حریفم حرکت انجام بده تا تخته رو آپدیت کنم. اما چطور بفهمم حریفم حرکتش رو انجام داده؟ بیاید راه حل های مختلف رو بررسی کنیم.
یک راه حل ساده ایی که به ذهنمون میاد اینه هی ریکوئست بزنیم! چطور؟ یک endpoint پیاده سازی کنیم که اطلاعات تخته رو میده. وقتی حرکتی رو انجام میدیم هر ۲ ثانیه یه بار ریکوئست میزنیم به این اندپوینت تا اگر حریفمون حرکتی انجام داد متوجه بشیم. بیاید کدش رو بزنیم:
کد ما یک منطق خیلی ساده رو پیاده سازی میکنه. یک متغیر Boolean داریم به اسم didTheMove که مشخص میکنه حریف ما حرکتش رو انجام داده یا نه. حریفمون با صدا زدن اندپوینت /move میتونه حرکتش رو ثبت کنه و وقتی این کارو میکنه متغیر ما true میشه. این وسط سیستم من هر ۱ دقیقه یک بار داشته /status رو صدا میزده و بلاخره وقتی حریف حرکتشو انجام بده با true شدن متغیر من متوجه این اتفاق میوفتم.
فرض کنید یک سشن بازی اینطور داریم که:
یعنی توی این ۵ دقیقه ما ۱۵ بار تختمون آپدیت میشه. اما با این حرکت داریم ۶۰۰ بار به بکاند ریکوئست میزنیم. عدد ۶۰۰ یادمون باشه به عنوان بنچ مارک که جلوتر بتونیم راه حل های دیگه رو با هم مقایسه کنیم.
با استفاده از سوکت ما یک کانکشن TCP با سرور باز میکنیم و هر اتفاقی که بیوفته سرور از طریق همون کانکشن بهمون اطلاع میده. این یعنی توی شبیه سازیمون ۲ تا کانکشن باز میکنیم و اکشن ها از طریق همین کانکشن ها بین کلاینت و بکاند رد و بدن میشن. این خیلی خوبه! اما مشکلاش چیه؟!
خلاصه بگم. سوکت سخته! آدم ترجیح میده سمتش نره مگه اینکه واقعا نیازی باشه که استفاده ازش به دردسرهاش بچربه!
بیاید حرکتی که قبلا زدیم رو مرور کنیم. ما یک اندپوینت داشتیم که کلاینت هر ثانیه بهش ریکوئست میزد تا بفهمه وضعیت تخته چطوره. بیاید یکم منطقش رو تغییر بدیم. وقتی کلاینت ریکوئست زد و تخته آپدیت نشده بود کاری نکنیم! کانکشنش رو باز نگه داریم. برای این کار باید به کلاینت بگیم تایم اوت رو مثلا بذار روی ۱ دقیقه! ریکوئستی که زده همینطوری بازه تا وقتی که حریف حرکتی انجام بده.
حالا وقتی حریف حرکتشو انجام داد از طریق سیگنال یا ایونتی به ریکوئست status ایی که زده بودیم میگیم حریف حرکتشو انجام داده و اونجا آپدیت جدید تخته رو نشون میدیم.
یعنی اگر من یک حرکتی انجام بدم و ۱۵ ثانیه بعد حریفم حرکتشو انجام بده اینطور میشه که بعد از انجام حرکتم یک ریکوئست میزنم به /status و این ریکوئست باز میمونه و منتظر جوابه و ثانیه ۱۵ که حریفم حرکتشو انجام میده بعد از ۱۵ ثانیه ریکوئستم جواب میگیره که تختهی اپدیت شده هست.
همون طوری که گفتیم ۱۵ اکشن توی ۵ دقیقه داریم. یعنی توی این ۵ دقیقه ما در کل ۳۰ کانکشن جدید باز میکنیم (که I/O سرور و کلاینت نفس راحت میکشن). اما بازم مثل اگه ۲۰۰ بازی همزمان داشته باشیم ۴۰۰ کانکشن باز داریم در ثانیه. ولی این عدد اصلا زیاد نیست! توجه کنید توی سوکت هم عدد همینه. ما یک کانکشن باز داریم نه اینکه کانکشن جدید باز کنیم!حالا برای همون ۲۰۰ بازی همزمان چند کانکشن جدید (ریکوئست) برثانیه داریم؟ 15/300*400 که میشه ۲۰ ریکوئست برثانیه جای ۴۰۰ ریکوئست برثانیه حالت اولیه. I/O ما نفس راحت میکشه! برای سوکت این عدد میشه ۲.۵.
درسته که ۲.۵ خیلی کمتر از ۲۰ هست ولی این عدد ها برای ۲۰۰ بازی همزمان برثانیه خیلی عدد خوبیه. کد ما اسکیل پذیره و خیلی راحت با یک سری تغییر ساده توی کدبیس قبلیمون میتونیم بهش برسیم جای اینکه کلی دردسر سر سوکت بکشیم! حالا چطور کدشو بزنیم؟ بریم مثال قبلی رو ادیت کنیم:
خلاصه بخوام بگم ما یک چنل توی Go ساختم که منتظر میمونیم تا وقتی حرکت انجام بشه یک دیتای خالی توش ریخته میشه و بعد به کاربر اطلاع میدیم. نکته ایی که هست توی select case که منتظر میمونیم دیتایی توی channel ریخته شه ۲ تا حالت دیگه رو هم چک میکنیم. اگه بیشتر از یک دقیقه گذشته بود یا اگر کاربر کانکشن رو کنسل کرد در هردوحالت دیگه گوش دادن به چنل رو متوقف میکنیم چون دیگه کاربر روی اون goroutine نیست و اگه این کارو نکنیم goroutine leak میدیم.
لانگ پولینگ حرکت جالبیه. تا جایی که من میدونم یک سری از استارتاپ های بزرگ ایرانی برای چتشون و حتی تلگرام برای دادن آپدیتهای ربات به وب سرورهاشون از این روش استفاده میکنه که نه تنها ساده هست مشکل Real-time بودن رو حل میکنه و پیاده سازی راحتش توی Go هم یک دلیل دیگه برای عاشق این زبان بودنه :)