تصویر مقاله
عکس از Karmishth Tandel در Unsplash

پیشنهاد می‌کنیم پیش از شروع، ابتدا بخش 8. Elementary UDP Sockets را مطالعه کنید.

در این فصل، قابلیت‌ها و امکاناتی که هنگام توسعهٔ اپلیکیشن با UDP باید در سطح برنامه پیاده‌سازی شوند، توضیح داده شده است. البته نویسندهٔ کتاب تلویحاً اشاره می‌کند که مگر در شرایطی که واقعاً به UDP نیاز دارید (مثل multicast)، بهتر است به سراغ TCP بروید.

از زمان انتشار این کتاب (ویرایش سوم، سال ۲۰۰۳) تا کنون، پروتکل‌های شبکه‌ای فراوانی بر پایهٔ UDP متولد شده‌اند؛ از جمله پروتکل WebRTC در سال ۲۰۱۱ برای ارتباطات P2P و بلادرنگ، و همچنین پروتکل QUIC که در سال ۲۰۱۲ توسط Google معرفی شد. به‌ویژه پروتکل QUIC پس از انتشار سند RFC 9914 توسط IETF در سال ۲۰۲۲، رسماً به عنوان پایه و پیاده‌سازی اصلی HTTP/3 شناخته شد.

امروزه که UDP تا این حد وارد جریان اصلی شده است، به جای اینکه بگوییم «هیچ‌وقت به UDP نیاز پیدا نخواهم کرد»، بهتر است این‌طور به موضوع نگاه کنیم: «اگر بخواهیم با UDP یک پروتکل شبکه بسازیم، چه قابلیت‌هایی را باید خودمان پیاده‌سازی کنیم؟»؛ مطالعهٔ این فصل با چنین دیدگاهی بسیار راهگشا خواهد بود.

نکته

در این فصل بررسی می‌شود که یک اپلیکیشن چگونه پورت مقصد (destination port) را از بسته دریافت می‌کند. در دنیای امروز که NAT همه‌گیر شده، اگر برنامه‌ای در یک شبکهٔ private صرفاً پورت مقصد را از بسته بخواند، در واقع همان IP و پورت شبکهٔ private را که توسط NAT ترجمه شده دریافت می‌کند. در صورتی که پروتکل عیناً از همین اطلاعات استفاده کند، به دلیل مغایرت با آدرس public قابل‌مشاهده از بیرون، تداخل و مشکل به وجود خواهد آمد.

برای حل این چالش، راهکارهایی نظیر STUN، TURN و ICE وجود دارند. اگر به این مبحث علاقه‌مندید، پیشنهاد می‌کنم دربارهٔ آن‌ها تحقیق کنید یا به فصل ۳ کتاب High Performance Browser Networking مراجعه فرمایید.

مقدمه

بیایید تفاوت‌های TCP و UDP را از دیدگاه اندازهٔ بسته‌ها (packet size) مرور کنیم:

  • پروتکل TCP: یک پروتکل byte-stream است که با بهره‌گیری از مکانیزم sliding window تضمین می‌کند فرستنده (sender) هرگز بیش از ظرفیت گیرنده (receiver) داده ارسال نکند.
  • پروتکل UDP: در اینجا هر ورودی (input) یک دیتگرام مستقل UDP است و همین موضوع می‌تواند باعث شود اندازهٔ دیتگرام دریافتی از بافر ورودی اپلیکیشن بزرگ‌تر باشد.

از آنجا که UDP بدون اتصال (connectionless) است، آدرس IP مقصد برای هر دیتگرامی که سوکت UDP دریافت می‌کند، می‌تواند متفاوت باشد. برای استخراج آدرس IP مقصد از یک دیتگرام UDP، باید از قبل گزینهٔ IP_RECVDSTADDR را روی سوکت تنظیم کرده باشید.

اگر سیستم از IP_RECVDSTADDR پشتیبانی نکند، برای دانستن آدرس IP مقصدِ دیتگرام UDP، باید سوکت را به تک‌تک آدرس‌های اینترفیس bind کرده و با استفاده از دستور select آن‌ها را مدیریت کنید.

به همین دلیل، هنگام برنامه‌نویسی با UDP باید سازوکارهای پایداری و قابلیت اطمینان (reliability) را در سطح اپلیکیشن پیاده‌سازی کنید؛ مواردی مانند مدیریت timeout، ارسال مجدد (retransmission)، مدیریت دیتگرام‌های گم‌شده، شماره‌گذاری ترتیبی (sequence numbers) و تطبیق پاسخ‌ها با درخواست‌ها.

در این فصل بررسی خواهیم کرد که هنگام برنامه‌نویسی با UDP، از چه سوکت آپشن‌هایی (socket options) می‌توان بهره برد.

فلگ‌های دریافتی، آدرس IP مقصد و ایندکس اینترفیس

پیش از پرداختن به جزئیات، ابتدا تابعی سفارشی به نام recvfrom_flags تعریف می‌کنیم.

این تابع ساختاری شبیه به recvfrom دارد. همان‌طور که پیش‌تر مرور کردیم، تابع recvfrom وظیفهٔ خواندن پیام‌ها از سوکت را بر عهده دارد.

دلیل تعریف تابع recvfrom_flags این است که بتوانیم مقدار msg_flags را دریافت کنیم، آدرس مقصد دیتگرام دریافتی را به دست آوریم (از طریق آپشن IP_RECVDSTADDR) و ایندکس اینترفیسی را که دیتگرام را دریافت کرده است استخراج نماییم (از طریق آپشن IP_RECVIF).

ابتدا یک ساختار (struct) برای نگهداری آدرس مقصد و ایندکس اینترفیس تعریف می‌کنیم:

C

پیاده‌سازی تابع recvfrom_flags به صورت زیر خواهد بود:

C

پس از فراخوانی recvfrom و دریافت پیام، با بررسی cmsg_type، آدرس IP مقصد یا ایندکس اینترفیس را از پیام دریافتی استخراج می‌کنیم. تصویر زیر هدر cmsghdr را در حالتی نشان می‌دهد که نوع پیام برابر با IP_RECVIF باشد.

تصویر مقاله

برش یا ناقص شدن دیتگرام (Datagram Truncation)

تصویر مقاله
تصویر مقاله

همان‌گونه که در فصل ۸ دیدیم، در TCP برای هر کلاینت یک سوکت متصل مجزا همراه با بافر اختصاصی وجود دارد (concurrent server)؛ اما در UDP، تنها یک سوکت و یک بافر واحد وجود دارد که دیتگرام‌های تمام کلاینت‌های مختلف را دریافت می‌کند.

همچنین اندازهٔ این بافر را می‌توان با استفاده از آپشن SO_RCVBUF که در فصل ۷ بررسی کردیم، تنظیم و مدیریت نمود.

حال اگر اندازهٔ دیتگرام UDP ورودی از حجم بافر بیشتر باشد چه اتفاقی می‌افتد؟ در سیستم‌های مبتنی بر BSD، تابع recvmsg فلگ MSG_TRUNC را فعال می‌کند؛ اما همهٔ پیاده‌سازی‌ها رفتار یکسانی ندارند. در چنین شرایطی معمولاً یکی از سناریوهای زیر رخ می‌دهد:

  1. بایت‌های اضافی دور ریخته می‌شوند و فلگ MSG_TRUNC به برنامه بازگردانده می‌شود.
  2. بایت‌های اضافی دور ریخته می‌شوند، اما هیچ خطایی یا اطلاعی به برنامه داده نمی‌شود.
  3. بایت‌های اضافی در صف باقی می‌مانند و در خواندن‌های بعدی برگردانده می‌شوند.

برای اینکه فارغ از نوع سیستم‌عامل بتوانید این خطا را مدیریت کنید، کافی است اندازهٔ بافر را ۱ بایت بزرگ‌تر از حداکثر اندازهٔ دیتگرامی که برنامه می‌تواند دریافت کند در نظر بگیرید. در این حالت، اگر اندازهٔ دیتگرام دریافتی با اندازهٔ بافر برابر شد، متوجه می‌شویم که خطا رخ داده است.

چه زمانی باید به جای TCP از UDP استفاده کنیم؟

در چه سناریوهایی استفاده از UDP بر TCP ارجحیت دارد؟

۱. هنگام استفاده از broadcasting و multicasting به سراغ UDP بروید

تصویر مقاله

همان‌طور که در فصل ۲۰ دیدیم، UDP برخلاف TCP از قابلیت‌های broadcasting و multicasting پشتیبانی می‌کند.

الف) در شرایطی که گم شدن چند بسته برای برنامه اهمیتی ندارد (مانند استریم ویدیو یا صوت به‌صورت زنده)، از broadcasting یا multicasting استفاده می‌شود.

ب) در سناریوهایی مثل انتقال فایل که به قابلیت اطمینان بالا نیاز دارند، باید حتماً بررسی کنید که آیا استفاده از multicasting مزیت عملکردی دارد یا خیر (ارسال یک بسته به N مقصد در مقایسه با برقراری N اتصال TCP و ارسال N بسته).

۲. برای برنامه‌های ساده از UDP استفاده کنید

به دلیل بدون اتصال بودن UDP، در یک چرخهٔ سادهٔ درخواست-پاسخ تنها به ۲ بسته نیاز است؛ در حالی که TCP برای همین کار به ۱۰ بسته نیاز دارد.

الف) هنگام ساخت برنامه‌های سادهٔ مبتنی بر درخواست-پاسخ با UDP، باید مکانیزم تشخیص خطا را خودتان پیاده کنید؛ این مکانیزم حداقل باید شامل تأییدیه (ack)، تایم‌اوت و ارسال مجدد باشد.

ب) برای ارسال حجم بالایی از داده‌ها (bulk data) مانند انتقال فایل، بهتر است از UDP استفاده نکنید؛ چرا که UDP فاقد قابلیت‌هایی چون window flow control، کنترل ازدحام و slow start است و در صورت استفاده، مجبور خواهید شد تمام این سازوکارها را مجدداً در لایهٔ اپلیکیشن پیاده‌سازی کنید.

البته اگر از یک اتصال TCP برای چندین رفت‌وبرگشت درخواست-پاسخ استفاده شود، تفاوت تعداد بسته‌های TCP و UDP دیگر چندان چشمگیر نخواهد بود. اما در صورتی که برنامه برای هر درخواست یک اتصال مجزا ایجاد کند (مانند نسخه‌های اولیهٔ HTTP) یا در فواصل زمانی طولانی تنها یک بار تبادل درخواست-پاسخ انجام دهد (مانند DNS)، استفاده از UDP برتری قابل توجهی خواهد داشت.

حال بیایید موارد استثنای بند (۲-ب) — یعنی عدم استفاده از UDP در انتقال داده‌های حجیم — را بررسی کنیم:

پروتکل TFTP نمونه‌ای است که برای انتقال داده‌های حجیم از UDP استفاده می‌کند. دلیل این انتخاب، حجم بسیار کمتر و سادگی کد bootstrap در مقایسه با TCP بود (حدود ۸۰۰ خط کد برای UDP در برابر ۴۵۰۰ خط برای TCP) و اینکه TFTP اصالتاً برای راه‌اندازی سیستم‌ها در بستر LAN طراحی شده بود، نه WAN. به همین خاطر، TFTP خود سازوکارهایی نظیر sequence number برای ack، تایم‌اوت و ارسال مجدد را پیاده‌سازی کرده است.

پروتکل NFS نیز برای انتقال داده‌های حجیم از UDP استفاده می‌کرد؛ زیرا در زمان طراحی اولیه‌اش در دههٔ ۱۹۸۰، تنها در شبکه‌های محلی (LAN) به کار می‌رفت. با ورود NFS به شبکهٔ گسترده (WAN) در دههٔ ۱۹۹۰، پیاده‌سازی‌های مبتنی بر TCP از نظر کارایی بر UDP پیشی گرفتند و امروزه بسیاری از پیاده‌سازی‌های NFS از هر دو پروتکل TCP و UDP پشتیبانی می‌کنند.

در پایان، نگاهی بیندازیم به قابلیت‌هایی که در TCP وجود دارند اما UDP فاقد آن‌هاست:

  1. تأییدیهٔ دریافت (acknowledgement)، ارسال مجدد بسته‌های مفقود، تشخیص بسته‌های تکراری و مرتب‌سازی بسته‌ها بر اساس شمارهٔ ترتیب (sequence numbers)
  2. کنترل جریان پنجره‌ای (window flow control) — گیرنده میزان فضای خالی بافر خود را به فرستنده اطلاع می‌دهد تا از ارسال دادهٔ بیش از حد و سرریز بافر جلوگیری شود.
  3. مکانیزم slow start و جلوگیری از تراکم شبکه (Network Congestion Avoidance)

اگر به هر یک از این سه ویژگی نیاز مبرم دارید، قطعاً باید به سراغ TCP بروید.

افزودن قابلیت اطمینان به برنامه‌های UDP — شماره ترتیب و تایم‌اوت

اگر قصد دارید از UDP در برنامه‌های مبتنی بر «درخواست-پاسخ» (Request-Response) استفاده کنید، حتماً باید این دو قابلیت را پیاده‌سازی نمایید:

  • تایم‌اوت و ارسال مجدد دیتاگرام
  • شماره ترتیب (Sequence Number) برای تشخیص این‌که هر دیتاگرام پاسخِ کدام درخواست است

شماره ترتیب (Sequence Number)

اضافه کردن Sequence Number کار بسیار ساده‌ای است؛ کلاینت به هر درخواست یک شماره ترتیب اختصاص می‌دهد و سرور هنگام ارسال پاسخ، همان شماره را برمی‌گرداند. بدین ترتیب کلاینت دقیقاً متوجه می‌شود که هر پاسخ مربوط به کدام درخواست بوده است.

تایم‌اوت (Timeout)

برای پیاده‌سازی تایم‌اوت، می‌توان پس از ارسال درخواست، N ثانیه منتظر ماند و در صورت عدم دریافت پاسخ، درخواست را مجدداً ارسال کرده و باز هم N ثانیه منتظر ماند. با این حال، این روش یک نقطه‌ضعف اساسی دارد؛ زیرا زمان رفت‌وبرگشت (Round Trip) ممکن است از کسری از میلی‌ثانیه در شبکه‌های محلی (LAN) تا چند ثانیه در شبکه‌های گسترده (WAN) متغیر باشد. در واقع، مقدار RTT تا حد زیادی تحت تأثیر مسافت فیزیکی و شرایط لحظه‌ای شبکه قرار دارد.

بنابراین، بهترین رویکرد این است که با ارسال هر پکت، مقدار RTT را به‌طور پویا محاسبه کرده و آن را در نظر بگیریم. در ادامه، نحوه محاسبه RTO (مخفف Retransmission Timeout) را با استفاده از srtt (تخمین‌گر هموارشده RTT) و rttvar (تخمین‌گر هموارشده میانگین انحراف) مشاهده می‌کنید.

تصویر مقاله

علاوه بر این، در محاسبه RTO می‌توان از استراتژی exponential backoff نیز بهره برد؛ برای مثال، اگر RTO اولیه ۲ ثانیه باشد و پاسخی دریافت نشود، تایم‌اوت بعدی به ۴ ثانیه، سپس به ۸ ثانیه و به همین ترتیب به‌صورت نمایی افزایش می‌یابد.

نکته دیگری که باید در محاسبه RTO مد نظر قرار داد این است که هنگام اندازه‌گیری RTT، به سختی می‌توان تشخیص داد که آیا پاسخ دریافتی از سرور مربوط به درخواست اصلی است یا پاسخی برای درخواست بازترسالی‌شده.

تصویر مقاله

در نمودار سمت راست، پاسخِ درخواست اصلی دریافت می‌شود؛ در حالی که در بخش سمت چپ و وسط، پاسخ مربوط به درخواست مجدد است. در الگوریتم Karn پاسخ‌های مربوط به درخواست‌های بازترسالی‌شده به هیچ وجه در اندازه‌گیری RTT لحاظ نمی‌شوند.

راهکار بهتر این است که سرور هنگام پاسخ دادن، همان برچسب زمانی (Timestamp) ارسال‌شده از سوی کلاینت را بازگرداند. در این حالت، کافی است کلاینت موقع ارسال، زمان فعلی را ثبت کند و پس از دریافت پاسخ، با کم کردن Timestamp از زمان فعلی، به راحتی RTT دقیق را به دست آورد.

در فصل ۲۲.۵ کتاب، پیاده‌سازی و کدهای مربوط به محاسبه RTT با جزئیات کامل شرح داده شده است. اگر می‌خواهید یک پیاده‌سازی شبکه در لایه کاربرد با UDP ایجاد کنید، این مبحث کمک شایانی به شما خواهد کرد.

برای خوانندگانی که زمان کافی ندارند، خلاصه ماجرا این است: یک ساختار هدر مجزا تعریف می‌کنیم و آن را به دیتای موجود در دیتاگرام UDP ضمیمه می‌نماییم. این ساختار هدر شامل Sequence Number و Timestamp خواهد بود.

اگر به فصل ۱۵ برگردیم، به خاطر داریم که قبلاً با ساختار msghdr آشنا شده بودیم.

تصویر مقاله
ساختار msghdr

در ساختار msghdr، بافرهای ورودی و خروجی با عنوان msg_iov وجود دارند.

تصویر مقاله
ارتباط msghdr و iovec

برای پیاده‌سازی این بخش، کافی است هدری شامل فیلدهای Sequence Number و Timestamp تعریف کرده و آن را ارسال کنیم.

تصویر مقاله
بخشی از کد پیاده‌سازی ارائه‌شده در کتاب

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

متصل کردن آدرس به رابط‌ها (Interface Address Binding)

در فصل ۱۷، تابع get_ifi_info را بررسی کردیم؛ تابعی که با تکمیل ساختار ifi_info (حاوی اطلاعات اینترفیس‌های شبکه)، این اطلاعات را برمی‌گرداند.

تصویر مقاله
ساختار ifi_info که توسط تابع get_ifi_info بازگردانده می‌شود
تصویر مقاله
با استفاده از تابع get_ifi_info می‌توان وضعیت رابط‌ها را بررسی کرد؛ توجه داشته باشید که آدرس‌های unicast مختلف می‌توانند دارای یک آدرس broadcast مشترک باشند.

این تابع در میزبان‌هایی که از گزینه سوکت IP_RECVDSTADDR پشتیبانی نمی‌کنند، بسیار کاربردی است و برای تشخیص آدرس IP مقصد در دیتاگرام‌های دریافتی UDP به کار می‌رود.

C++

این تابع تمام اینترفیس‌های IPv4 را دریافت کرده و آن‌ها را به سوکت UDP متصل (Bind) می‌کند. در اینجا می‌توان با استفاده از گزینه SO_REUSEADDR، پورت یکسانی را برای تمامی IPها تنظیم کرد. سپس به ازای هر سوکت، پروسه‌های فرزند با دستور fork ایجاد شده و با اجرای تابع mydg_echo، به درخواست‌های ورودی هر سوکت پاسخ (echo) داده می‌شود.

در صورتی که اینترفیس از برودکست پشتیبانی کند، خطای EADDRINUSE بررسی می‌شود. از آنجا که آدرس‌های unicast مختلف در یک ساب‌نت، آدرس broadcast یکسانی دارند (همان‌طور که در تصویر قبل دیدیم)، برای اینکه به ازای هر آدرس broadcast فقط یک سوکت متصل شود، صرفاً آدرس‌های برودکست مورد بررسی تکراری نبودن قرار می‌گیرند.

در نهایت، یک سوکت برای آدرس wildcard ساخته می‌شود تا شرایطی مدیریت شود که آدرس مقصد دیتاگرام، از نوع unicast یا broadcast معمولی نیست؛ این حالت فقط زمانی رخ می‌دهد که دیتاگرام دارای آدرس برودکست محدود (Limited Broadcast Address) یعنی 255.255.255.255 باشد.

پس از این مراحل، تابع main به کار خود پایان می‌دهد و فرآیندهای فرزند (Child Processes) به اجرای سرور و مدیریت اتصالات ادامه می‌دهند.

شایان ذکر است که به جای fork کردن پروسه‌ها برای هر سوکت، می‌توان از select نیز استفاده کرد، اما در این مثال به دلیل سادگی پیاده‌سازی، از fork استفاده شده است.

سرورهای همروند UDP (Concurrent UDP Servers)

در پروتکل TCP با ایجاد فرآیند جدید (fork) به ازای هر درخواست کلاینت، پیاده‌سازی یک سرور همروند (Concurrent) به سادگی میسر بود؛ زیرا در TCP اتصال وجود دارد و برای هر ارتباط، یک جفت‌سوکت منحصربه‌فرد تعریف می‌شود.

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

برای حل این چالش، سرور می‌تواند برای هر کلاینت یک سوکت مجزا بسازد و آن را به یک پورت موقت (ephemeral) متصل کرده و پاسخ‌ها را بفرستد. البته این روش یک ایراد دارد: کلاینت باید این پورت را ذخیره کند و دیتاگرام‌های بعدی را دقیقاً به همان پورت ارسال نماید.

پروتکل TFTP نمونه بارز این الگو است؛ کلاینت در ابتدا درخواست را به پورت استاندارد و شناخته‌شده (well known) ۶۹ ارسال می‌کند، اما در ادامه باید دیتاگرام‌ها را به همان پورتی بفرستد که پاسخ را از آن دریافت کرده است.

تصویر مقاله

اگر از inetd (مطابق فصل ۱۳) استفاده شود، یک مرحله دیگر نیز به سناریوی بالا اضافه خواهد شد.

تصویر مقاله

در این ساختار، سرور TFTP ابتدا به عنوان فرزند inetd ایجاد می‌شود، سپس فرآیند فرزند دیگری را برای پردازش درخواست fork کرده و در نهایت خودش خارج (exit) می‌شود.

اطلاعات پکت‌های IPv6

در پروتکل IPv6، می‌توان هنگام ارسال دیتاگرام، اطلاعات تکمیلی زیر را نیز همراه با آن فرستاد:

  1. آدرس مبدأ IPv6
  2. ایندکس اینترفیس خروجی (Outgoing Interface Index)
  3. محدودیت هاپ خروجی (Outgoing Hop Limit)
  4. آدرس هاپ بعدی (Next-hop Address)
  5. کلاس ترافیک خروجی (Outgoing Traffic Class)

به‌طور مشابه، در پکت‌های دریافتی نیز می‌توان اطلاعات تکمیلی زیر را به دست آورد:

  1. آدرس مقصد IPv6
  2. شاخص اینترفیس ورودی (Arriving interface index)
  3. محدودیت هاپ ورودی (Arriving hop limit)
  4. کلاس ترافیک ورودی (Arriving traffic class)
تصویر مقاله

آدرس مبدأ IPv6 معمولاً از طریق bind مشخص می‌شود. ارسال آدرس مبدأ همراه با داده‌ها می‌تواند میزان سربار را کاهش دهد و همچنین تضمین کند که آدرس مقصدِ درخواست، دقیقاً با آدرس مبدأِ پاسخ یکسان است.

محدودیت هاپ (Hop limit) از طریق گزینه‌های سوکت IPV6_UNICAST_HOPS یا IPV6_MULTICAST_HOPS تعیین می‌شود. مشخص کردن دستی این مقدار، مقدار پیش‌فرض کرنل را بازنویسی می‌کند. مقدار Hop limit با عبور بسته از هر روتر یک واحد کاهش می‌یابد؛ به همین دلیل، مقدار Hop limit در دیتاگرام‌های دریافتی برای برنامه‌هایی مانند traceroute یا سرویس‌هایی که باید اطمینان یابند بسته فوروارد نشده است (مثلاً با بررسی ۲۵۵ بودن مقدار Hop limit)، بسیار کاربردی خواهد بود.

در صورت تعیین آدرس هاپ بعدی (Next hop)، این آدرس حتماً باید متعلق به گره همسایه در گرهِ ارسال‌کننده باشد. اگر آدرس Next hop با آدرس مقصد برابر باشد، نتیجه دقیقاً مشابه استفاده از گزینه سوکت SO_DONTROUTE خواهد بود.

در ساختار کلاس ترافیک، شش بیت بالایی به DSCP (Differentiated Service Code Point) و دو بیت پایینی به ECN (Explicit Congestion Notification) اختصاص یافته است. در این کتاب وارد جزئیات DSCP و ECN نمی‌شویم، اما به طور خلاصه: روترها و سوئیچ‌ها از DSCP برای کنترل ترافیک، صف‌بندی و وزن‌دهی به ارسال داده‌ها استفاده می‌کنند و از ECN نیز برای آگاهی نقاط پایانی از وقوع ازدحام در شبکه بهره گرفته می‌شود.

تنظیم Path MTU در IPv6

در فصل دوم با مفهوم MTU (Maximum Transmission Unit) آشنا شدیم. به بیان ساده، MTU حداکثر اندازه مجاز برای یک دیتاگرام IP را مشخص می‌کند.

در پروتکل TCP، علاوه بر MTU، محدودیت اندازه دیگری به نام MSS نیز در لایه TCP اعمال می‌شود.

تصویر مقاله
تصویر مقاله

در خصوص UDP، اندازه بافر سوکت با گزینه SO_SNDBUF کنترل می‌شود؛ اما برعکس TCP، این مقدار یک بافر واقعی در حافظه ایجاد نمی‌کند، بلکه صرفاً به عنوان حداکثر سقف مجاز برای اندازه دیتاگرام UDP به کار می‌رود (در TCP، کرنل ابتدا داده‌ها را از بافر برنامه به بافر سوکت منتقل کرده و سپس ارسال می‌کند).

به کوچک‌ترین MTU در کل مسیر میان دو هاست، اصطلاحاً Path MTU گفته می‌شود.

پروتکل IPv6 اختیارات و امکانات مختلفی را در خصوص فرایند Path MTU discovery در دسترس برنامه‌ها قرار می‌دهد.

ارسال با حداقل MTU

هنگام اجرای فرایند Path MTU discovery، بسته‌ها متناسب با اندازه Path MTU تکه‌تکه می‌شوند. پروتکل IPv6 حداقل مقدار MTU را که تمامی مسیرها ملزم به پشتیبانی از آن هستند، ۱۲۸۰ بایت تعیین کرده است.

استفاده از این حداقل MTU تعریف‌شده در IPv6 (یعنی ۱۲۸۰ بایت)، اگرچه به دلیل کوچک‌تر بودن از Path MTU واقعی ممکن است بازدهی کمتری داشته باشد، اما این مزیت بزرگ را دارد که برنامه را از اجرای فرایند Path MTU discovery کاملاً بی‌نیاز می‌کند.

برنامه‌هایی که از حداقل MTU بهره می‌برند معمولاً در دو دسته جای می‌گیرند:

  • برنامه‌های مبتنی بر Multicast: در این برنامه‌ها به دلیل ارتباط هم‌زمان با میلیون‌ها سرور، مدیریت و بررسی تک‌به‌تک خطاهای مربوط به اندازه بسته بسیار دشوار است.
  • سرویس‌هایی مانند DNS که درخواست‌های کوتاهی را با مقصدهای متعددی تبادل می‌کنند: در این حالت، میزان تعامل با یک سرور خاص آن‌قدر زیاد نیست که پذیرش ریسک اتلاف بسته (Packet Loss) برای کشف MTU توجیه‌پذیر باشد.

اگر قصد دارید از حداقل مقدار MTU استفاده کنید، کافی است گزینه سوکت IPV6_USE_MIN_MTU را فعال نمایید.

دریافت اعلان‌های تغییر Path MTU

برای باخبر شدن از تغییرات Path MTU، برنامه باید گزینه سوکت IPV6_RECVPATHMTU را فعال کند. با انتخاب این گزینه، هر زمان که Path MTU تغییر کند، مقدار جدید در قالب داده‌های کمکی (Ancillary data) ارسال می‌شود. ساختار cmsghdr حاوی این اطلاعات جانبی است و چنانچه فیلد cmsg_level برابر با IPPROTO_IPV6 و فیلد cmsg_type برابر با IPV6_PATHMTU باشد، داده‌ها ساختار ip6_mtuinfo را در بر خواهند داشت.

تصویر مقاله

تعیین Path MTU فعلی

چنانچه برنامه‌ای گزینه IPV6_RECVPATHMTU را تنظیم نکرده باشد و تغییرات Path MTU را دنبال نکند، می‌تواند با فراخوانی IPV6_PATHMTU روی یک سوکت متصل‌شده، اندازه فعلی Path MTU را دریافت کند. اگر هنوز مقداری برای Path MTU مشخص نشده باشد، سیستم مقدار MTU مربوط به اینترفیس خروجی (Outgoing interface) را بازمی‌گرداند.

جلوگیری از قطعه‌قطعه‌سازی (Fragmentation)

پروتکل IPv6 به صورت پیش‌فرض بسته‌های ارسالی را بر اساس اندازه Path MTU تکه‌تکه می‌کند. با استفاده از گزینه سوکت IPV6_DONTFRAG می‌توان این رفتار خودکار را غیرفعال کرد؛ قابلیتی که برنامه‌هایی مانند traceroute برای پیدا کردن مقدار Path MTU به آن نیاز دارند.

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

22.10 جمع‌بندی (Summary)

برخی برنامه‌ها نیازمند دسترسی به آدرس IP مقصد و اینترفیس دریافت‌کننده دیتاگرام‌های UDP ورودی هستند. گزینه‌های سوکت IP_RECVDSTADDR و IP_RECVIF امکان دریافت این دست اطلاعات را به عنوان داده‌های کمکی برای هر دیتاگرام فراهم می‌سازند. به همین ترتیب، در IPv6 نیز می‌توان با بهره‌گیری از گزینه سوکت IPV6_RECVPKTINFOP اطلاعات مربوط به Hop limit را دریافت کرد.

اگرچه پروتکل TCP امکانات و ویژگی‌های به مراتب بیشتری ارائه می‌دهد، اما شرایطی وجود دارد که استفاده از UDP در آن‌ها الزامی است. پروتکل UDP برای سناریوهای Broadcasting و Multicasting ضروری بوده و در تعاملات ساده مبتنی بر درخواست-پاسخ بسیار کارآمد است؛ با این حال، برای تضمین قابلیت اطمینان (Reliability)، منطق لازم باید درون خود برنامه پیاده‌سازی شود و استفاده از آن برای انتقال داده‌های حجیم (Bulk data) به هیچ وجه مناسب نیست.

در این فصل با پیاده‌سازی سازوکارهای Timeout و ارسال مجدد (Retransmission) در کلاینت UDP، نحوه مدیریت بسته‌های از دست‌رفته (Lost packets) را بررسی کردیم. ما با افزودن برچسب زمانی (Timestamp) به هر بسته، زمان وقفه (Timeout) را به صورت پویا تنظیم کردیم و با استفاده از شماره توالی (Sequence number)، هر پاسخ را به درخواست متناظرش پیوند دادیم. پیاده‌سازی ما مبتنی بر پروتکل ساده Stop-and-wait بود؛ چراکه دقیقاً همین سناریوهای سبک و سرراست، بهترین بستر برای استفاده از UDP به شمار می‌روند.

مراجع (Reference)

Unix network programming