تصویر مقاله
عکس از Mike Winkler در Unsplash
  1. مقدمه
  2. توابع recvfrom و sendto
  3. نمونه پیاده‌سازی کلاینت/سرور UDP
  4. از دست رفتن دیتاگرام‌ها
  5. اعتبارسنجی پاسخ دریافتی
  6. تشخیص در حال اجرا نبودن سرور
  7. تابع connect در پروتکل UDP
  8. جمع‌بندی
نکته: در این کتاب، واحد داده‌های مورد استفاده توسط پروتکل TCP در Layer 3 را «پکت» (packet) و واحد داده‌های UDP را «دیتاگرام» (datagram) می‌نامیم. مفهوم دیتاگرام کلی‌تر است و پکت در واقع گونه‌ای از دیتاگرام به شمار می‌رود.

۱. مقدمه

برخلاف پروتکل TCP که یک اتصال پایدار ایجاد کرده و جریان داده مطمئنی از بایت‌ها (reliable byte stream) فراهم می‌کند، پروتکل UDP کاملاً بدون اتصال (connectionless) است، تضمینی برای تحویل داده نمی‌دهد و بر پایه دیتاگرام کار می‌کند.

تصویر مقاله
توابع سوکت مورد استفاده در کلاینت/سرور UDP

کلاینت بدون نیاز به برقراری اتصال با سرور، مستقیماً با فراخوانی تابع sendto دیتاگرام خود را می‌فرستد. در سمت دیگر، سرور نیز با اجرای تابع recvfrom منتظر دریافت داده از سوی کلاینت می‌ماند.

این رویکرد با پروتکل TCP که همیشه پیش از هر کاری یک اتصال اختصاصی می‌سازد، کاملاً متفاوت است.

تصویر مقاله
توابع سوکت مورد استفاده در کلاینت/سرور TCP

۲. توابع recvfrom و sendto

تصویر مقاله
بررسی توابع recvfrom و sendto

آرگومان flag را در فصل ۱۴ بررسی خواهیم کرد؛ چرا که در یک کلاینت/سرور ساده UDP نیازی به استفاده از آن نداریم.

تابع recvfrom شباهت زیادی به تابع accept در پروتکل TCP دارد و تابع sendto نیز یادآور تابع connect است؛ با این تفاوت اساسی که در اینجا ساختار آدرس سوکت به جای برقراری اتصال، صرفاً مشخص‌کننده فرستنده و گیرنده دیتاگرام است.

هنگام کار با UDP، ارسال یک دیتاگرام با طول صفر نیز کاملاً مجاز است. در چنین وضعیتی، اگرچه حجم داده صفر است، اما هدرهای IP (در IPv4 معادل ۲۰ بایت و در IPv6 معادل ۴۰ بایت) و هدر UDP (معادل ۸ بایت) همچنان ارسال می‌شوند. در پروتکل TCP دریافت داده با طول صفر به معنای بستن اتصال تلقی می‌شود، اما از آنجا که در UDP اتصالی وجود ندارد، این کار معنای خاصی به همراه نخواهد داشت.

تصویر مقاله
در TCP اگر تابع read مقدار صفر برگرداند، به معنای بسته شدن غیرفعال اتصال (passive close) است.

توابع recvfrom و sendto از نظر فنی در TCP نیز قابل استفاده‌اند، اما واقعیت این است که هیچ دلیل منطقی برای استفاده از آن‌ها در TCP وجود ندارد.

۳. نمونه پیاده‌سازی کلاینت/سرور UDP

در این مقاله کدهای برنامه به طور مستقیم آورده نشده‌اند؛ در صورت تمایل به مشاهده جزئیات کدها می‌توانید به متن اصلی کتاب مراجعه کنید.

همان‌طور که در فصل ۵ دیدیم، در اینجا نیز قصد داریم یک اکو سرور (echo server) ساده با پروتکل UDP پیاده‌سازی کنیم.

تصویر مقاله

برای ساخت سوکت در پروتکل UDP از نوع SOCK_DGRAM استفاده می‌شود (در حالی که در TCP از SOCK_STREAM استفاده می‌کردیم).

سرور UDP

پیاده‌سازی اکو سرور با استفاده از UDP ویژگی‌های منحصربه‌فرد زیر را دارد:

  1. از آنجا که در UDP اتصالی ساخته نمی‌شود، برخلاف TCP پدیده‌ای مثل EOF (پایان داده یا فایل) وجود نخواهد داشت.
  2. به طور معمول، به جای ساخت یک سرور هم‌روند (concurrent) مثل TCP، یک سرور تکرارکننده (iterative) پیاده‌سازی می‌شود که نیازی به fork ندارد.

دلیل نکته دوم به این برمی‌گردد که لایه UDP از مکانیزمی شبیه به صف (queue) بهره می‌برد. هر سوکت UDP دارای یک بافر دریافت (receive buffer) اختصاصی است و تمام دیتاگرام‌های ورودی در آن قرار می‌گیرند. هر بار که برنامه تابع recvfrom را صدا می‌زند، دیتاگرام‌ها بر اساس اولویت ورود (FIFO) تحویل داده می‌شوند. از آنجا که ظرفیت این بافر محدود است، مسلماً نمی‌تواند بی‌نهایت دیتاگرام را در صف نگه دارد.

حجم بافر دریافت سوکت UDP را می‌توان با تنظیم سوکت‌آپشن SO_RECVBUF تغییر داد و شخصی‌سازی کرد.

تصویر مقاله

در یک سرور TCP، برای رسیدگی به دو کلاینت متفاوت از fork استفاده می‌شود تا برای هر کدام یک اتصال جداگانه برقرار شود؛ در نتیجه دو سوکت متصل مجزا ایجاد می‌شوند که هر کدام بافر دریافت مستقل خود را دارند.

تصویر مقاله

در نقطه مقابل، سرور UDP تنها و تنها یک سوکت دارد که تمام دیتاگرام‌های ورودی را دریافت کرده و تمامی پاسخ‌ها را نیز از طریق همان یک سوکت ارسال می‌کند.

تصویر مقاله
دیدگاه سرور UDP
تصویر مقاله
نحوه دستیابی به IP و پورت‌های مبدا و مقصد در سرورهای TCP و UDP

سرورهای TCP به سادگی به آدرس IP و پورت دسترسی دارند، اما در سوکت‌های UDP برای دریافت آدرس IP مقصد، حتماً باید آپشن IP_RECVDSTADDR (در IPv4) تنظیم شده و از تابع recvmsg استفاده شود؛ چرا که به دلیل ماهیت بدون اتصال UDP، آدرس IP مقصد می‌تواند در هر دیتاگرام متفاوت باشد.

کلاینت UDP

کلاینت‌های TCP با فراخوانی تابع connect از کرنل می‌خواهند که یک پورت موقت (ephemeral port) به سوکت اختصاص دهد. اما کلاینت UDP پس از ساخت سوکت بلافاصله تابع sendto را صدا می‌زند. در این حالت، اگر هنوز پورت محلی برای سوکت تعیین نشده باشد، کرنل در همان حین اجرای sendto یک ephemeral port به آن اختصاص خواهد داد.

تصویر مقاله
دیدگاه کلاینت UDP
  • پورت موقتی که در حین اجرای تابع sendto توسط کرنل تعیین می‌شود، دیگر تغییر نخواهد کرد.
  • اگر کلاینت از تابع bind استفاده نکند، آدرس IP آن ممکن است با هر بار ارسال دیتاگرام UDP تغییر کند.

علت تغییر آدرس IP این است که اگر کلاینت از نوع multihomed باشد (چند کارت شبکه داشته باشد)، بسته به مقصد نهایی ممکن است مسیر خروجی (datalink) متفاوتی انتخاب شود و در نتیجه کرنل نیز آدرس IP سازگار با همان مسیر را انتخاب خواهد کرد.

اگر کلاینت با استفاده از bind آدرس IP سوکت را تثبیت کند، این احتمال وجود دارد که دیتاگرام خروجی با آدرس مبدایی فرستاده شود که با آدرس اینترفیس خروجی (outgoing datalink) همخوانی ندارد.

در ادامه بررسی خواهیم کرد که هنگام کار با کلاینت/سرور UDP چه مشکلاتی ممکن است رخ دهد.

۴. از دست رفتن دیتاگرام‌ها

از آنجا که پروتکل UDP هیچ تضمینی برای تحویل داده ارائه نمی‌دهد، ممکن است دیتاگرام ارسالی از کلاینت یا پاسخ برگشتی از سرور در مسیر شبکه گم شود. در چنین شرایطی، تابع recvfrom در سمت کلاینت تا ابد منتظر پاسخی می‌ماند که هرگز نخواهد رسید! برای جلوگیری از بروز این مشکل، کلاینت معمولاً هنگام فراخوانی recvfrom اقدام به تنظیم یک بازه زمانی مشخص (timeout) می‌کند.

اگر این موضوع را با فصل ۵ که در آن سرور TCP را پیاده‌سازی کردیم مقایسه کنید، تفاوت‌ها به‌خوبی مشخص می‌شوند. در پروتکل TCP، قابلیت اطمینان داده‌ها تضمین می‌شود؛ یعنی اگر ارسال بسته‌ای با شکست مواجه شود، مجدداً فرستاده خواهد شد. همان‌طور که در تصویر زیر می‌بینید، برای هر درخواست یک پیام ACK فرستاده می‌شود تا رسیدن بسته تأیید شود. علاوه بر این، هنگام پایان کار فرآیند سرور یک پیام FIN ارسال می‌گردد و اگر پس از راه‌اندازی مجدد (Reboot) هاست، بسته‌ای دریافت شود، با ارسال RST اتصال بلافاصله قطع می‌شود.
تصویر مقاله
در پروتکل TCP، ارسال پیام ACK در پاسخ به درخواست، نشانهٔ دریافت موفقیت‌آمیز بسته است.

با این حال، تنظیم تایم‌اوت (Timeout) دوای همهٔ دردها نیست و تمام مشکلات را حل نمی‌کند؛ چرا که پس از وقوع تایم‌اوت مشخص نیست دیتوگرام کلاینت در مسیر گم شده یا پاسخ سرور موفق به بازگشت نشده است. برای مثال، اگر درخواست کلاینت عملیاتی مانند «انتقال وجه از حساب A به حساب B» باشد، بسیار حیاتی است که بدانیم آیا درخواست اصلاً به سرور رسیده است یا خیر.

در فصل ۲۲ به بررسی روش‌های ایجاد قابلیت اطمینان (Reliability) در ارتباط کلاینت/سرور UDP خواهیم پرداخت.

۵. اعتبارسنجی پاسخ‌های دریافتی

از آنجا که پروتکل UDP اتصالی برقرار نمی‌کند، نمی‌توان به‌سادگی فهمید دیتوگرام ورودی به سوکت کلاینت دقیقاً از طرف کدام پردازش ارسال شده است. به بیان دیگر، باید مشخص شود که آیا این داده واقعاً پاسخ مورد انتظار از سمت سرور است یا دیتوگرامی متفرقه است که پردازش دیگری آن را ارسال کرده است.

ساده‌ترین راهکار این است که ساختار آدرس سوکت برگشتی از recvfrom را بررسی کنیم تا ببینیم آیا IP مبدأ دیتوگرام با IP سرور همخوانی دارد یا خیر.

البته این روش در صورت multihomed بودن سرور (داشتن چندین آدرس IP یا کارت شبکه) ممکن است کارایی نداشته باشد. اگر سرور به جای یک IP مشخص، سوکت را روی wildcard اصطلاحاً bind کند، کرنل برای آدرس مبدأِ دیتوگرام، آدرس IP اصلی (Primary IP) درگاه خروجی را در نظر می‌گیرد. بنابراین اگر کلاینت درخواست را به آدرس IP غیر اصلی فرستاده باشد، تطبیق آدرس‌ها ناموفق خواهد بود.

برای حل این مشکل، کلاینت می‌تواند با استفاده از DNS یک بار دیگر آدرس IP را بررسی کند؛ یا اینکه سرور UDP برای تک‌تک آدرس‌های IP خود سوکت‌های جداگانه‌ای را bind کرده و به کمک select آن‌ها را مدیریت کند.

۶. تشخیص فعال نبودن سرور

اگر یک کلاینت سادهٔ UDP درخواستی را به سروری بفرستد که اجرا نشده است، در تابع recvfrom برای همیشه مسدود (Block) و معلق خواهد ماند.

بیایید سناریویی را در نظر بگیریم که در آن ابزار tcpdump روی هاست کلاینت (macosx) اجرا شده و درخواستی به هاست سرور (freebsd4) ارسال می‌شود.

تصویر مقاله

در خط سوم، کلاینت دیتوگرام را به سمت هاست سرور ارسال می‌کند و در خط چهارم، پروتکل ICMP خطای «port unreachable» را بازمی‌گرداند. اما این خطای ICMP مستقیماً به فرآیند کلاینت تحویل داده نمی‌شود (یادآوری: ARP در خطوط ۱ و ۲، و همچنین ICMP، پروتکل‌های لایه ۳ یا layer 3 هستند).

به این خطای ICMP، خطای ناهمگام (asynchronous error) می‌گویند. با اینکه منشأ این خطا فراخوانی تابع sendto است، اما خود sendto وضعیت موفقیت‌آمیز برمی‌گرداند؛ زیرا به محض وجود فضای کافی در صف خروجی رابط شبکه (Interface output queue) برای ساخت دیتوگرام IP، پاسخ موفقیت ثبت می‌شود. اما خطای ICMP پس از آن اتفاق می‌افتد و دیگر به فرآیند کلاینت بازگردانده نخواهد شد.

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

۷. کاربرد تابع connect در پروتکل UDP

همان‌طور که پیش‌تر اشاره شد، برای مهار خطاهای ناهمگام در سوکت UDP باید از تابع connect استفاده کنیم.

در صورت استفاده از connect، پروتکل UDP برخلاف TCP وارد فرایند مصافحه سه‌مرحله‌ای (three-way handshake) نمی‌شود؛ در عوض، کرنل خطاهایی مثل «unreachable destination» را بررسی کرده و آدرس IP و شماره پورت طرف مقابل (Peer) را ثبت می‌کند و به پردازش تحویل می‌دهد.

سوکت UDP پس از اجرای connect (که از این پس آن را سوکت متصل‌شدهٔ UDP می‌نامیم)، تفاوت‌های مهمی با سوکت‌های عادی UDP پیدا می‌کند:

  • در سوکت متصل‌شدهٔ UDP دیگر نیازی به تعیین آدرس IP یا پورت مقصد نیست؛ زیرا به جای sendto می‌توان مستقیماً توابع write یا send را به کار برد.
  • همچنین دیگر لزومی به فراخوانی recvfrom نیست و می‌توان از read، recv، یا recvmsg استفاده کرد.
  • کرنل در این حالت تنها دیتوگرام‌هایی را به برنامه تحویل می‌دهد که از آدرس ثبت‌شده در connect آمده باشند؛ بنابراین، سوکت متصل‌شدهٔ UDP تضمین می‌کند که تبادل داده فقط با همان یک طرف مشخص (Peer) انجام پذیرد.
  • خطاهای ناهمگام منحصراً در سوکت‌های متصل‌شده به فرآیند بازگردانده می‌شوند.

در یک سوکت متصل‌شدهٔ UDP، تابع connect را می‌توان بارها فراخوانی کرد؛ این قابلیت برای تعیین آدرس IP و پورت جدید یا قطع کامل اتصال سوکت کاربرد دارد.

  • تعیین آدرس IP و پورت جدید: برخلاف TCP، در سوکت UDP می‌توان با فراخوانی‌های مکرر، آدرس IP مقصد را به‌راحتی تغییر داد.
  • قطع اتصال سوکت: کافی است ساختار آدرس سوکت را روی مقدار AF_UNSPEC تنظیم کرده و مجدداً connect را اجرا کنید.

بررسی عملکرد و کارایی

اگر روی یک سوکت معمولی UDP (بدون فراخوانی connect) تابع sendto را اجرا کنید، سه مرحله طی می‌شود:

۱) اتصال سوکت، ۲) ارسال دیتوگرام، ۳) قطع اتصال سوکت.

از آنجا که برای هر بار ارسال، فرایند اتصال و قطع مجدداً تکرار می‌شود، فرستادن پشت‌سر‌همِ چندین دیتوگرام می‌تواند عملکرد سیستم را تا حد محسوسی کاهش دهد.

در نقطهٔ مقابل، اگر از connect استفاده کنید، اتصال سوکت فقط یک‌بار برقرار می‌شود؛ بنابراین ارسال چندین دیتوگرام به یک مقصد ثابت بسیار بهینه‌تر خواهد بود.

۸. جمع‌بندی

پیاده‌سازی یک سرور با پروتکل UDP در مقایسه با سرور TCP به مراتب ساده‌تر و سرراست‌تر است.

با این حال، امکاناتی که TCP به‌صورت پیش‌فرض در اختیارتان می‌گذاشت—نظیر تشخیص گم شدن بسته‌ها، ارسال مجدد، یا تأیید اصالت هویت فرستنده—در UDP وجود ندارد. در این فصل روش‌های پوشش دادن برخی از این نقاط ضعف را مرور کردیم و در فصل ۲۲ به شیوه‌های پیشرفته‌ترِ افزودن قابلیت اطمینان به برنامه‌های UDP خواهیم پرداخت.

فراموش نکنید که برای دریافت خطاهای ناهمگام پس از ارسال بسته، سوکت UDP حتماً باید به کمک تابع connect متصل شود.

۹. مراجع و منابع (Reference)

  1. کتاب Unix Network Programming
  2. Packet vs Datagram