اگر تا به حال کنجکاو بودهاید که برنامههای چتی مانند Slack، Discord یا WhatsApp در پشت صحنه چگونه کار میکنند، این آموزش به شما نشان خواهد داد. یک سرور چت بلادرنگ را از پایه و با استفاده از زبان Go خواهید ساخت و مفاهیم بنیادینی که سیستمهای ارتباطی مدرن را قدرتمند میکنند، یاد خواهید گرفت. در پایان این راهنما، شما یک چت روم عملی ساختهاید که از کاربران همزمان نامحدودی پشتیبانی میکند، پیامها را حتی پس از crash سرور حفظ میکند، مدیریت session را برای اتصال مجدد کاربران پس از قطعی شبکه فراهم میکند و به طور ظریفی با کلاینتهای کند یا قطع شده برخورد میکند.
یک چت روم، سروری است که به چندین کاربر اجازه میدهد به طور همزمان متصل شده و پیامها را به صورت بلادرنگ مبادله کنند. وقتی از عبارت "در سطح production" استفاده میکنیم، منظورمان این است که شامل ویژگیهایی است که در یک برنامه واقعی انتظار دارید: دادهها را به گونهای ذخیره میکند که پیامها پس از راهاندازی مجدد سرور از بین نروند، قطعیهای شبکه را به صورت ظریف مدیریت میکند و میتواند از کاربران همزمان بسیاری پشتیبانی کند بدون آنکه سرعت آن کاهش یابد. جنبه "توزیع شده" به نحوه مدیریت سیستم برای اتصال چندین کلاینت از مکانهای مختلف اشاره دارد که همگی سعی دارند به طور همزمان پیام ارسال و دریافت کنند.
این معماری چالشهای جالبی را معرفی میکند: چگونه اطمینان حاصل میکنید که همه پیامها را به یک ترتیب مشاهده میکنند؟ چگونه با کلاینتهایی که اتصال اینترنتی کندی دارند برخورد میکنید؟ اگر کسی به طور غیرمنتظره قطع ارتباط کند چه اتفاقی میافتد؟ اینها فقط مشکلات تئوری نیستند. هر برنامه شبکهای با همزمانی، مدیریت state و مدیریت خطا سر و کار دارد. چه در حال ساخت یک برنامه چت باشید، چه یک بازی چندنفره، یک ویرایشگر مشارکتی یا یک پلتفرم معاملاتی، با چالشهای مشابهی روبرو خواهید شد. الگوهایی که در اینجا یاد میگیرید به طور گسترده در سیستمهای توزیع شده کاربرد دارند.
برنامههای چت پروژههای یادگیری عالی هستند زیرا چندین مشکل چالشبرانگیز را در یک مکان ترکیب میکنند. شما نیاز دارید اتصالات همزمان را به صورت ایمن مدیریت کنید، پیامها را به چندین کلاینت بدون مسدود کردن broadcast کنید، با شبکههای غیرقابل اعتماد برخورد کنید، دادهها را به صورت پایدار ذخیره کنید و اطمینان حاصل کنید که سیستم به طور ظریفی از crash بازیابی میشود. هر یک از این موضوعات میتواند آموزش جداگانهای باشد، اما در اینجا خواهید دید که چگونه در یک برنامه واقعی با هم کار میکنند.
این آموزش چندین مفهوم مهم را نشان میدهد که برای ساخت سیستمهای توزیع شده اساسی هستند:
این سیستم از یک معماری client-server پیروی میکند که در آن اجزای داخلی با هم کار میکنند تا یک تجربه چت robust ارائه دهند. قلب سیستم یک حلقه رویداد (Event Loop) است - یک goroutine که تمام تغییرات state را به صورت ترتیبی مدیریت میکند. این حلقه از چندین کانال برای هماهنگی استفاده میکند: یک کانال برای اتصال کلاینتهای جدید، کانالی برای broadcast پیامها، و کانالهایی برای مدیریت خروج و پیامهای خصوصی. این طراحی تضمین میکند که هیچ شرایط رقابتی (Race Condition) روی state وجود ندارد و ترتیب کلی رویدادها حفظ میشود.
برای پایداری، از یک رویکرد دو مرحلهای استفاده میشود: یک Write-Ahead Log (WAL) که هر پیام را بلافاصله در یک فایل append-only ذخیره میکند، و Snapshots دورهای که state کامل چت را در یک فایل جداگانه ذخیره میکنند. این سیستم بازیابی سریع پس از crash را ممکن میسازد. در نهایت، یک سیستم مدیریت session مبتنی بر token به کاربران اجازه میدهد پس از قطع ارتباط، با حفظ هویت و تاریخچه چت خود، دوباره متصل شوند.
با تکمیل این پروژه، نه تنها یک چت روم کاملاً عملی خواهید داشت، بلکه درک عمیقی از نحوه عملکرد سیستمهای توزیع شده، مدیریت همزمانی، persistence و بازیابی از خطا به دست خواهید آورد.
ساخت یک سرور چت توزیعشده با قابلیت استفاده در محیطهای عملیاتی، مستلزم درک عمیق معماری و مفاهیم بنیادین سیستمهای توزیعشده است. این سیستمها باید قادر به مدیریت اتصالات همزمان نامحدود، حفظ پیامها در صورت بروز خرابی سرور، مدیریت نشست برای اتصال مجدد کاربران پس از قطعی شبکه و رسیدگی به مشتریان کند یا قطعشده باشند. معماری چنین سیستمی بر پایه مدل کلاینت-سرور بنا شده و با استفاده از الگوهای همزمانی و پایداری داده، تجربهی چتی روان و قابل اعتماد را فراهم میکند.
یک چتروم توزیعشده سروری است که به چندین کاربر اجازه میدهد به صورت همزمان متصل شده و پیامها را به صورت بلادرنگ مبادله کنند. ویژگی "قابل استفاده در محیط عملیاتی" به این معنی است که سیستم شامل قابلیتهایی است که در یک برنامه واقعی انتظار میرود: داده را به گونهای حفظ میکند که پیامها پس از راهاندازی مجدد سرور از بین نروند، خرابیهای شبکه را به شکلی مناسب مدیریت میکند و میتواند از تعداد زیادی کاربر همزمان بدون کاهش سرعت پشتیبانی کند. جنبه "توزیعشده" به نحوه مدیریت سیستم از سوی چندین کلاینت متصل شده از مکانهای مختلف اشاره دارد که همگی سعی در ارسال و دریافت پیام در یک زمان دارند.
معماری این سیستم از اجزای داخلی تشکیل شده که برای ارائه یک تجربه چت قوی با یکدیگر همکاری میکنند. در هسته سیستم، یک حلقه رویداد قرار دارد که یک گوروتین تنها است و تمام تغییرات حالت را به صورت ترتیبی پردازش میکند. این حلقه از طریق کانالهای مختلفی با دیگر بخشهای سیستم ارتباط برقرار میکند. برای مدیریت اتصالات، یک شنونده TCP روی پورت 9000 به درخواستهای اتصال ورودی گوش میدهد. برای هر اتصال کلاینت جدید، دو گوروتین مجزا ایجاد میشود: یکی برای خواندن پیامهای ورودی از کلاینت و دیگری برای نوشتن پیامهای خروجی به کلاینت. ساختارهای داده اصلی که حالت سیستم را نگهداری میکنند و توسط موتکسها محافظت میشوند، شامل یک نگاشت از کلاینتهای فعال، یک نگاشت از نشستهای کاربر برای اتصال مجدد و یک برش از تاریخچه پیامها هستند.
این چتروم از مدل CSP زبان گو استفاده میکند که رویکردی اساساً متفاوت نسبت به برنامهنویسی همزمان در سایر زبانها ارائه میدهد. به جای محافظت از حافظه مشترک با قفلها، گو رویکرد "به اشتراک گذاری حافظه با برقراری ارتباط" را ترویج میکند. در این مدل، دادهها از طریق کانالها بین گوروتینها منتقل میشوند و در هر زمان تنها یک گوروتین مالک داده است. این طراحی بسیاری از باگهای همزمانی را از بین میبرد. کانالها مزایای متعددی دارند: حذف شرایط رقابت، ارائه کنترل جریان طبیعی، تسهیل در ردیابی جریان پیام و ترکیبپذیری بهتر. در این پروژه از پنج کانال مجزا برای انواع رویدادها استفاده شده است. حلقه رویداد اصلی با استفاده از یک عبارت select از همه این کانالها دریافت میکند، به این معنی که تمام تغییرات حالت به صورت ترتیبی و در یک مکان اتفاق میافتند که درک سیستم را بسیار آسانتر میکند.
برای اطمینان از بقای تاریخچه چت پس از خرابی سرور، از یک رویکرد دو مرحلهای مشابه پایگاههای داده واقعی استفاده میشود: ثبت پیشنویس و اسنپشات. Wال مکانیزم اصلی پایداری است. هر پیام بلافاصله به یک فایل append-only به نام messages.wal اضافه میشود. پس از نوشتن هر پیام، فراخوانی fsync تضمین میکند که داده بلافاصله روی دیسک فیزیکی نوشته شود. مشکل ثبت پیشنویس این است که برای همیشه رشد میکند. اسنپشاتها این مشکل را حل میکنند. هر 5 دقیقه، اگر بیش از 100 پیام جدید وجود داشته باشد، کل تاریخچه پیام در یک فایل جداگانه به نام snapshot.json ذخیره میشود. پس از ایجاد اسنپشات، WAL خالی میشود. هنگام راهاندازی سرور، ابتدا فایل اسنپشات بارگیری میشود و سپس مدخلهای WAL که پس از آخرین اسنپشات نوشته شدهاند، بازپخش میشوند. این سیستم دو مرحلهای بهترین هر دو جهان را ارائه میدهد: نوشتن سریع در حین عملکرد عادی با WAL، بازیابی سریع پس از خرابی با اسنپشات به علاوه بازپخش WAL کوچک، پایداری تضمینشده از طریق fsync و زمان بازیابی محدود.
شبکهها غیرقابل اعتماد هستند. کاربران قطع ارتباط میکنند، وایفای قطع میشود و اتصالات موبایل بین دکلها جابجا میشوند. یک سیستم مدیریت نشست مبتنی بر توکن به کاربران اجازه میدهد پس از قطعیهای شبکه به طور بیدرنگ به سرور متصل شوند، بدون اینکه نیاز به ایجاد حساب کاربری جدید یا وارد کردن مجدد اطلاعات داشته باشند. این سیستم یک توکن منحصربهفرد برای هر کاربر تولید میکند که زماندار بوده و معمولاً پس از یک ساعت منقضی میشود. این توکن تاریخچه چت و هویت کاربر را بدون نیاز به رمزهای عبور یا احراز هویت پیچیده حفظ میکند.
برای درک نحوه تعامل این اجزا، مسیر یک پیام را در سیستم دنبال میکنیم. هنگامی که یک کاربر پیامی را ارسال میکند، ابتدا توسط گوروتین خواندن کلاینت دریافت شده و به کانال broadcast ارسال میشود. حلقه رویداد اصلی این پیام را از کانال دریافت کرده و آن را به Wال مینویسد تا پایداری آن تضمین شود. سپس پیام به تاریخچه پیام در حافظه اضافه میشود. در نهایت، حلقه رویداد پیام را به کانالهای outgoing هر یک از کلاینتهای فعال میفرستد. هر گوروتین نوشتن کلاینت مسئولیت ارسال پیام از طریق اتصال TCP مربوطه را بر عهده دارد. کانال broadcast به عنوان یک نقطه همگامسازی عمل میکند و ترتیب کامل پیام را تضمین میکند.
این معماری با ترکیب حلقه رویداد تکنخی برای هماهنگی، گوروتینهای متعدد برای مدیریت اتصالات، کانالها برای انتقال ایمن داده و موتکسها برای محافظت از دادههای حالت مشترک، تعادلی بین سادگی، عملکرد و قابلیت اطمینان برقرار میکند. استراتژی پایداری دوگانه اطمینان حاصل میکند که داده از دست نرود، در حالی که مدیریت نشست انعطافپذیری لازم در برابر مشکلات شبکه را فراهم میآورد. الگوها و مفاهیم به کار رفته در این معماری، از برنامهنویسی همزمان گرفته تا مدیریت حالت و تحمل خطا، به طور گسترده در سیستمهای توزیعشده از پایگاههای داده و صفهای پیام تا سرورهای وب قابل اعمال هستند.
پیادهسازی سرور و مدیریت اتصالات کلاینتها، هسته اصلی یک چتروم توزیعشده را تشکیل میدهد. این فرآیند شامل ایجاد یک سرور TCP که بتواند به صورت همزمان به تعداد نامحدودی از کاربران سرویس دهد، مدیریت جریان پیامها بین کلاینتها، و تضمین پایداری دادهها حتی در صورت بروز خرابی میشود. در این بخش، مراحل کلیدی راهاندازی سرور و نحوه برقراری ارتباط ایمن و کارآمد کلاینتها را بررسی میکنیم.
سرور چت بر اساس معماری client-server و با استفاده از قابلیتهای همزمانی زبان Go ساخته میشود. یک listener TCP روی پورت 9000 برای پذیرش اتصالات ورودی تنظیم میشود. زمانی که یک کلاینت متصل میشود، سرور برای مدیریت آن دو goroutine مجزا ایجاد میکند: یکی برای خواندن پیامهای ارسالی از سمت کلاینت و دیگری برای ارسال پیامها به کلاینت. قلب این سیستم، یک حلقه رویداد (Event Loop) است که در یک goroutine واحد اجرا میشود. این حلقه از طریق کانالهای مختلفی مانند join، leave، و broadcast، تمامی رویدادهای سیستم (اتصال جدید، خروج کاربر، پیام جدید و ...) را به ترتیب دریافت و پردازش میکند. این طراحی تضمین میکند که تمام تغییرات حالت به صورت ترتیبی و بدون شرایط رقابت (Race Condition) انجام میشود، زیرا تنها یک goroutine مجاز به تغییر دادههای اشتراکی مانند نگاشت کلاینتهای فعال و تاریخچه پیامها است.
فرآیند مدیریت هر اتصال کلاینت با تابع handleClient آغاز میشود. این تابع ابتدا یک مهلت 30 ثانیهای برای وارد کردن نام کاربری توسط کلاینت تعیین میکند تا از اشغال بیجهت منابع توسط اتصالات غیرفعال جلوگیری شود. پس از تأیید نام کاربری، یک ساختار داده Client ایجاد میشود که حاوی اتصال TCP و یک کانال خروجی بافر شده با ظرفیت 10 پیام است. وجود این بافر بسیار حیاتی است؛ زیرا امکان میدهد تا در صورت کند بودن سرعت ارسال پیام به یک کلاینت خاص (مثلاً به دلیل مشکل شبکه)، پیامهای بعدی در صف قرار گیرند و از مسدود شدن فرآیند broadcast برای سایر کلاینتهای سریع جلوگیری شود. سپس، دو گوروتین مجزا برای مدیریت خواندن و نوشتن به طور همزمان راهاندازی میگردند. گوروتین خواننده به طور مداوم پیامهای ورودی از کلاینت را میخواند و به کانال broadcast سرور ارسال میکند. گوروتین نویسنده نیز به طور پیوسته پیامهای موجود در کانال خروجی کلاینت را از بافر خوانده و از طریق اتصال TCP برای کلاینت ارسال مینماید. این جداسازی تضمین میکند که عمل ارسال پیام به یک کلاینت کند، مانع از دریافت پیامهای جدید از همان کلاینت نشود.
هنگامی که یک پیام از طریق کانال broadcast به حلقه رویداد سرور میرسد، چندین عمل مهم به صورت ترتیبی انجام میشود. ابتدا پیام برای ماندگاری بلندمدت در فایل Write-Ahead Log یا WAL نوشته میشود. این فایل به صورت append-only است و پس از هر نوشتن، با فراخوانی fsync، داده بلافاصله روی دیسک فیزیکی ذخیره میشود تا در صورت crash کردن سرور، پیام از دست نرود. سپس، پیام به لیست تاریخچه پیامهای موجود در حافظه اضافه میشود. در نهایت، پیام به تمام کلاینتهای متصل ارسال میگردد. برای انجام این ارسال، سرور از یک الگوی non-blocking استفاده میکند. به این صورت که برای هر کلاینت، پیام جدید را به کانال خروجی آن کلاینت میفرستد. اگر این کانال به دلیل پر بودن بافر (به علت کندی کلاینت) قادر به پذیرش پیام جدید نباشد، عمل ارسال برای آن کلاینت خاص نادیده گرفته میشود تا فرآیند برای دیگران متوقف نشود. این رویکرد که "تخریب graceful" نامیده میشود، باعث میشود سیستم حتی در صورت وجود مشکلات جزئی در برخی اجزا، به کار خود ادامه دهد.
در نهایت، با پیادهسازی این مراحل، یک سرور چتروم پایدار و scalable خواهید داشت که میتواند پایهای برای سیستمهای توزیعشده پیچیدهتر باشد. درک این مفاهیم نه تنها برای ساخت چتروم، بلکه برای توسعه هرگونه سرویس شبکهای که با چالشهای همزمانی، persistence و تحمل خطا روبرو است، ضروری میباشد.
در هسته مرکزی سرور چت، یک حلقه رویداد (Event Loop) قرار دارد که تمامی تغییرات حالت سیستم را به صورت ترتیبی و در یک مکان مدیریت میکند. این حلقه، که در یک گوروتین (Goroutine) واحد اجرا میشود، بر روی پنج کانال مختلف نظارت میکند: پیوستن کاربر جدید (join)، ترک کردن (leave)، پخش پیام (broadcast)، درخواست لیست کاربران (listUsers) و پیام خصوصی (directMessage). زمانی که یک پیام از طرف یک کلاینت ارسال میشود، این پیام به کانال broadcast ارسال شده و توسط حلقه رویداد دریافت میشود. سپس تابع handleBroadcast فراخوانی میشود. این تابع ابتدا پیام را در Write-Ahead Log (WAL) برای ماندگاری داده مینویسد و سپس آن را به کانالهای outgoing تمام کلاینتهای متصل فعال ارسال میکند. استفاده از این معماری مبتنی بر کانالها تضمین میکند که تمامی پیامها به ترتیب رسیدنشان به همه کاربران ارسال میشوند و از شرایط رقابت (Race Condition) جلوگیری میشود.
برای غنیتر کردن تجربه کاربری، یک سیستم دستورات پیادهسازی شده است. دستورات، پیامهایی هستند که با یک اسلش (/) شروع میشوند و به جای پخش عمومی، یک عمل خاص را انجام میدهند. این سیستم در تابع handleClient و در گوروتین readMessages پردازش میشود. هنگامی که یک خط ورودی از کلاینت با '/' شروع شود، به عنوان یک دستور تفسیر شده و به جای ارسال به کانال broadcast، به صورت منطقی پردازش میشود. این الگو مشابه چیزی است که در برنامههایی مانند Slack و Discord مشاهده میکنید و امکان عملکردهای پیشرفته را فراهم میکند.
در دنیای واقعی، سرعت اتصال کلاینتها یکسان نیست. یک چالش اساسی در هر سیستم چندکاربره، مدیریت کلاینتهای کند بدون تأثیرگذاری بر تجربه دیگر کاربران است. برای حل این مشکل از دو تکنیک اصلی استفاده شده است. اولاً، هر کلاینت یک کانال outgoing بافر شده با ظرفیت ۱۰ پیام دارد. این بافر امکان صفبندی پیامها را فراهم میکند و اگر یک کلاینت موقتاً کند شود، سیستم میتواند تا ۱۰ پیام را برای او ذخیره کند بدون اینکه ارسال پیام برای دیگران مسدود شود. ثانیاً، در الگوریتم پخش پیام، از ارسال non-blocking استفاده میشود. اگر بافر یک کلاینت به دلیل کندی مداوم پر باشد، سیستم به جای مسدود کردن و انتظار، پیام را برای آن کلاینت نادیده گرفته و به ارسال برای دیگران ادامه میدهد. این به معنای از دست رفتن برخی پیامها برای کلاینت کند است، اما از توقف کامل سیستم جلوگیری میکند. این رویکرد "تخریب graceful" نامیده میشود: سیستم حتی زمانی که بخشی از آن با مشکل مواجه میشود به کار خود ادامه میدهد.
برای درک بهتر همکاری این مؤلفهها، مسیر یک پیام نمونه را دنبال میکنیم. فرض کنید کاربر "Alice" پیام "Hello everyone!" را ارسال میکند. ۱) پیام از طریق اتصال TCP کلاینت Alice به سرور میرسد. ۲) گوروتین readMessages مربوط به Alice این خط را میخواند. ۳) از آنجا که با '/' شروع نمیشود، به عنوان یک پیام عادی شناسایی شده و به کانال broadcast سرور ارسال میشود. ۴) حلقه رویداد اصلی سرور این پیام را از کانال broadcast دریافت میکند. ۵) حلقه رویداد تابع handleBroadcast را فراخوانی میکند. ۶) این تابع ابتدا پیام را در فایل Wال مینویسد و fsync میکند تا از ماندگاری آن روی دیسک اطمینان حاصل شود. ۷) سپس پیام را به کانال outgoing هر یک از کلاینتهای فعال موجود در نقشه clients ارسال میکند. ۸) گوروتین writeMessages هر کلاینت (مثلاً Bob و John)، پیام را از کانال outgoing خود خوانده و از طریق اتصال شبکه به کلاینت مربوطه ارسال میکند. تمامی این مراحل به لطف هماهنگی через کانالها و موتکسها، به صورت همزمان اما بدون تداخل انجام میشود.
تستکردن یک سیستم همزمان مانند چتروم نیازمند رویکردی متفاوت نسبت به کدهای ترتیبی معمولی است. شما باید مطمئن شوید که گوروتینها بهدرستی هماهنگ میشوند، پیامها به ترتیب صحیح میرسند و سیستم موارد حاشیهای مانند قطعاتصال را به خوبی مدیریت میکند. تستهای واحد، کامپوننتهای مجزا را در انزوا تأیید میکنند. برای چتروم شما، مهمترین تست، تأیید صحت ارسال پیام به تمام کلاینتهای متصل است.
تستهای یکپارچهسازی، عملکرد کل سیستم در کنار هم را تأیید میکنند - سرور واقعی، کلاینتهای واقعی و اتصالات شبکه واقعی. برخلاف تستهای واحد که کامپوننتها را mock میکنند، تستهای یکپارچهسازی تمام stack را تمرین میدهند. این تستها مشکلاتی را شناسایی میکنند که تستهای واحد از قلم میاندازند، مانند timeoutهای شبکه، مسائل ترتیب پیام در بین چندین کلاینت، یا مشکلات مربوط به ایجاد و قفلشدن فایل WAL.
استقرار چتروم شما به معنای اجرای آن روی سروری است که 24/7 فعال میماند، در صورت crash بهطور خودکار restart میشود و هنگام boot سرور شروع به کار میکند. Systemd سیستم init استاندارد در اکثر توزیعهای لینوکس است. این سرویسها را مدیریت میکند، restartها را کنترل میکند و مطمئن میشود که چتروم شما هنگام boot شروع به کار کند. Docker برنامه شما را با تمام وابستگیهای آن بستهبندی میکند و استقرار آن را در هر جایی که Docker اجرا میشود، آسان میسازد. این از یک build چندمرحلهای استفاده میکند که image نهایی را کوچک و بهینه نگه میدارد.
شما اکنون یک چتروم توزیعشده آماده production را از ابتدا ساختهاید. این پروژه مفاهیم مهم سیستمهای توزیعشده از جمله الگوهای همزمانی، برنامهنویسی شبکه، مدیریت state، persistence و تحمل خطا را نشان میدهد. برای توسعه بیشتر، میتوانید قابلیتهایی مانند کانالهای چندگانه، احراز هویت پیشرفته، آپلود فایل و پشتیبانی از WebSocket را اضافه کنید. برای مقیاسپذیری عظیم، میتوانید چتروم را بین چندین سرور shard کنید. کد کامل این پروژه در GitHub در دسترس است. این راهنما پایهای محکم برای درک و ساخت سیستمهای توزیعشده واقعی با استفاده از قدرت همزمانی Go فراهم میکند.