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

همچنین با مهم‌ترین و پرکاربردترین گزینه‌های سوکت و موارد استفاده از هر یک در سناریوهای واقعی آشنا خواهیم شد.

۱. توابع getsockopt و setsockopt

تصویر مقاله

این توابع برای خواندن (get) و اعمال (set) گزینه‌های مختلف سوکت استفاده می‌شوند.

  • sockfd: همان Socket Descriptor بازشده است.
  • level: مشخص می‌کند که این تنظیم در سطح عمومی سوکت (General Socket) اعمال می‌شود یا مختص به یک پروتکل خاص (مانند IPv4، IPv6، TCP یا UDP) است.
  • optval: اشاره‌گری به مقدار گزینه؛ در تابع setsockopt به مقدار جدید موردنظر و در getsockopt به بافری اشاره دارد که مقدار فعلی در آن ذخیره می‌شود.

پارامتری که در اینجا باید توجه ویژه‌ای به آن داشته باشید، پارامتر level است. برای مثال، جهت اعمال تنظیمات در سطح Socket API، باید مقدار آن را برابر با SOL_SOCKET قرار دهید.

در ادامه، به بررسی بخشی از گزینه‌های سطح SOL_SOCKET (تحت عنوان Generic Socket Options) و گزینه‌های سطح IPPROTO_TCP (تحت عنوان TCP Socket Options) خواهیم پرداخت.

برای مشاهده فهرست کامل و جزئیات بیشتر این گزینه‌ها، پیشنهاد می‌کنم به socket man page و صفحات راهنمای مرتبط مراجعه کنید.

۲. Socket State

در پروتکل TCP، سوکت متصل‌شده (Connected Socket) برخی از گزینه‌های سوکت را از سوکت شنوا (Listening Socket) به ارث می‌برد. از جمله این گزینه‌ها می‌توان به SO_DEBUG، SO_DONTROUTE و SO_KEEPALIVE اشاره کرد.

نکته مهم اینجاست که وقتی سرور با فراخوانی تابع accept سوکت متصل را تحویل می‌گیرد، فرآیند Three-way Handshake در TCP قبلاً به پایان رسیده است؛ بنابراین اگر بخواهید این گزینه‌ها روی سوکت متصل اعمال شوند، باید آن‌ها را از قبل روی Listening Socket تنظیم کرده باشید.

۳. Generic Socket Options

منظور از Generic Socket Options گزینه‌هایی است که با تنظیم SOL_SOCKET برای پارامتر level فعال می‌شوند. این گزینه‌ها مستقل از پروتکل هستند، اما برخی از آن‌ها فقط برای انواع مشخصی از سوکت‌ها کاربرد دارند؛ به عنوان نمونه، گزینهٔ SO_BROADCAST تنها مختص سوکت‌های Datagram است.

به جای فهرست کردن تمامی گزینه‌های موجود در کتاب، در اینجا مهم‌ترین و کاربردی‌ترین موارد را گلچین کرده و توضیح می‌دهیم.

SO_BROADCAST

  • این گزینه به پروسس اجازه می‌دهد تا پیام‌های Broadcast ارسال کند.
  • استفاده از این قابلیت فقط روی سوکت‌های Datagram و در شبکه‌هایی که از پیام‌های Broadcast پشتیبانی می‌کنند (مانند اترنت) امکان‌پذیر است.
  • پروتکل‌های TCP یا SCTP از این قابلیت پشتیبانی نمی‌کنند. جزئیات بیشتر درباره Broadcasting در فصل ۲۰ بررسی خواهد شد.
  • اگر این گزینه روی سوکت تنظیم نشده باشد اما آدرس مقصد یک آدرس Broadcast باشد، خطای EACCES برگردانده می‌شود.

SO_DEBUG

  • این گزینه تنها توسط TCP پشتیبانی می‌شود.
  • با فعال‌سازی آن، کرنل اطلاعات دقیق و کاملی را از تمامی بسته‌های TCP ثبت و ضبط می‌کند.
  • این داده‌ها در یک بافر چرخشی (Circular Buffer) درون حافظه کرنل ذخیره می‌شوند.

SO_KEEPALIVE

اگر گزینه Keep-Alive برای یک سوکت TCP فعال باشد و به مدت دو ساعت هیچ داده‌ای در دو جهت مبادله نشود، TCP به طور خودکار یک Keep-Alive Probe برای طرف مقابل (Peer) ارسال می‌کند.

طرف مقابل پس از دریافت Keep-Alive Probe موظف به پاسخگویی است و در این حالت ۳ سناریوی ممکن رخ می‌دهد:

  1. طرف مقابل با یک بستهٔ ACK پاسخ می‌دهد: این حالت نشان‌دهنده عملکرد عادی است و برنامه متوجه هیچ رخداد خاصی نخواهد شد.
  2. طرف مقابل با یک بستهٔ RST پاسخ می‌دهد: این وضعیت نشان می‌دهد که هاست مقصد کرش کرده یا ری‌بوت شده است؛ در نتیجه، خطای معلقِ سوکت روی ECONNRESET تنظیم شده و سوکت بسته می‌شود.
  3. طرف مقابل هیچ پاسخی نمی‌دهد: رفتار در این سناریو به پیاده‌سازی بستگی دارد، اما در نسخه‌های مشتق از Berkeley TCP، چندین Probe دیگر ارسال شده و سیستم منتظر می‌ماند. پس از سپری شدن زمان مقرر، خطای سوکت روی ETIMEDOUT تنظیم و ارتباط سوکت خاتمه می‌یابد.

اگر سوکت در پاسخ به Probe یک خطای ICMP دریافت کند، همان خطا را برمی‌گرداند. رایج‌ترین خطای ICMP پیام «Host Unreachable» است که بر اثر قطعی شبکه یا کرش کردن هاست راه دور رخ می‌دهد.

یکی از مباحث پربحث پیرامون گزینهٔ SO_KEEPALIVE، مدت‌زمان انتظار است (که مقدار پیش‌فرض آن معمولاً دو ساعت در نظر گرفته می‌شود).

  • طبق پیوست کتاب TCPv1، این بازه بسته به پیاده‌سازی کرنل متغیر است و می‌تواند از ۲ ساعت تا ۱۵ دقیقه باشد.
  • این مقدار در سطح کرنل تعیین می‌شود و امکان تغییر آن به صورت مجزا برای هر سوکت وجود ندارد.

هدف از به کارگیری SO_KEEPALIVE، تشخیص کرش کردن یا عدم دسترسی‌پذیری هاست مقصد است. اگر تنها پروسس طرف مقابل کرش کند، لایه TCP پیام FIN ارسال کرده و در نتیجه تابع select می‌تواند آن را تشخیص دهد؛ اما اگر کل هاست فیزیکی کرش کند، بدون این سازوکار نمی‌توان متوجه وضعیت شد.

البته بی‌پاسخ ماندن Keep-Alive Probe لزوماً به معنای کرش قطعی هاست نیست، چرا که ممکن است دلایل دیگری مانند قطعی موقت یکی از روترهای میانی در مسیر وجود داشته باشد.

معمولاً سرورها بیشترین استفاده را از این گزینه دارند؛ زیرا خاموش شدن کلاینت یا کرش سیستم آن باعث می‌شود سرور بی‌دلیل منتظر داده‌هایی از یک اتصال از دست‌رفته بماند. به چنین وضعیتی اتصال نیمه‌باز (Half-Open Connection) می‌گویند و Keep-Alive ابزاری عالی برای شناسایی و بستن این اتصالات است.

تصویر زیر روش‌های مختلف تشخیص بروز مشکل در اتصالات TCP را خلاصه کرده است:

تصویر مقاله

SO_LINGER

این گزینه رفتار تابع close را مدیریت می‌کند. در حالت پیش‌فرض، تابع close بلافاصله خروجی را برمی‌گرداند و اگر داده‌ای در بافر ارسال (Send Buffer) سوکت باقی مانده باشد، سیستم در پس‌زمینه تلاش می‌کند آن را به طرف مقابل ارسال کند.

گزینهٔ SO_LINGER این رفتار پیش‌فرض را تغییر می‌دهد و برای تنظیم آن از ساختار (struct) زیر استفاده می‌شود:

C
  1. چنانچه مقدار l_onoff برابر ۰ باشد، این گزینه غیرفعال بوده و سیستم طبق روال عادی عمل می‌کند.
  2. اگر l_onoff غیرصفر و l_linger برابر ۰ باشد، هنگام فراخوانی تابع close اتصال TCP بلافاصله متوقف (Abort) می‌شود؛ به این معنا که TCP داده‌های بافر ارسال را دور ریخته، یک بستهٔ RST به مقصد ارسال می‌کند و فرآیند استاندارد خاتمه (4-Way Handshake) دور زده می‌شود. این کار از رفتن سوکت به وضعیت TIME_WAIT جلوگیری می‌کند، اما اگر ظرف مدت 2MSL یک کانکشن جدید با مشخصات کاملاً یکسان ({ip1, port1, ip2, port2}) ایجاد شود (Incarnation)، خطر آن وجود دارد که سگمنت‌های باقی‌مانده از کانکشن قبلی به اشتباه به این کانکشن جدید تحویل داده شوند.
  3. اگر هر دو فیلد l_onoff و l_linger مقادیری غیرصفر داشته باشند، هنگام بسته شدن سوکت، کرنل برای تکمیل انتقال منتظر می‌ماند؛ یعنی اگر داده‌ای در بافر ارسال باقی مانده باشد، پروسس به حالت انتظار (Sleep) می‌رود تا زمانی که کرنل تمام داده‌ها را به طرف مقابل تحویل داده و تاییدیه (ACK) دریافت کند، یا اینکه مهلت زمانی تعیین‌شده (Timeout) به پایان برسد.

برای درک بهتر، سناریویی را تجسم کنید که در آن سرور برای لحظاتی مشغول پردازش است و کلاینت بلافاصله پس از فرستادن داده‌ها، تابع close را فراخوانی می‌کند.

در چنین شرایطی، بسته به تنظیمات گزینهٔ linger، سناریوهای متفاوتی رقم می‌خورد:

  • عدم استفاده از گزینهٔ linger: با فراخوانی تابع close، کنترل بلافاصله برمی‌گردد و سیستم منتظر دریافت پاسخ یا تأییدیه برای داده‌های ارسال‌شده نمی‌ماند (تصویر ۷.۷).
  • استفاده از گزینه linger: این گزینه تا زمان دریافت ACK مربوط به FIN ارسال‌شده از سوی کلاینت، نتیجه‌ای بازنمی‌گرداند (تصویر ۷.۸).
  • در صورت استفاده از shutdown به جای close: عملیات read تا زمان دریافت FIN از طرف peer در حالت انتظار (wait) باقی می‌ماند (تصویر ۷.۱۰).
تصویر مقاله
تصویر مقاله
تصویر مقاله

SO_RCVBUF, SO_SNDBUF

این گزینه‌ها برای تغییر اندازه send buffer و receive buffer سوکت به کار می‌روند.

  • بافر دریافت (receive buffer) وظیفه دارد داده‌ها را تا زمانی که توسط اپلیکیشن خوانده شوند، در خود نگه دارد.
  • در بافر سوکت TCP پدیده overflow رخ نمی‌دهد؛ زیرا طرف مقابل (peer) مجاز نیست داده‌ای فراتر از اندازه window ارسال کند و در صورت ارسال داده اضافه، نادیده گرفته می‌شود. (نکته: به TCP receive window مراجعه کنید. به طور مشابه TCP Congestion window نیز وجود دارد که این مقدار توسط sender مدیریت می‌شود).
  • اندازه بافر سوکت TCP باید حداقل ۴ برابر اندازه MSS کانکشن باشد.
  • همچنین برای جلوگیری از اتلاف فضای بافر، اندازه بافر سوکت TCP باید مضربی از MSS باشد.
تصویر مقاله

از آنجا که TCP یک پروتکل full-duplex است، امکان ارسال و دریافت هم‌زمان داده‌ها در هر دو جهت وجود دارد.

تصویر بالا وضعیتی را نشان می‌دهد که در آن ظرفیت کانکشن TCP برابر با ۸ سگمنت است؛ یعنی ۴ سگمنت ارسالی توسط کلاینت و ۴ سگمنت ACK دریافتی از سرور. در این سناریو، اندازه send buffer کلاینت باید حداقل گنجایش ۸ سگمنت را داشته باشد، زیرا لایه TCP کلاینت باید هر سگمنت را تا زمان دریافت پاسخ ACK مربوطه در بافر نگه‌داری کند.

به طور معمول، ظرفیت کانکشن (pipe) در پروتکل TCP از حاصل‌ضرب bandwidth * RTT محاسبه می‌شود.

SO_REUSEADDR, SO_REUSEPORT

گزینه SO_REUSEADDR زمانی استفاده می‌شود که با وجود کانکشن‌های قبلی، بخواهیم از پورت اشغال‌شده مجدداً استفاده کنیم.

بیایید به این سناریو توجه کنیم:

  1. سرور listening شروع به کار می‌کند.
  2. یک درخواست کانکشن دریافت می‌شود و با ایجاد یک فرایند فرزند (child process)، پردازش آن کانکشن آغاز می‌گردد.
  3. سرور listening متوقف می‌شود، اما فرایند فرزند همچنان مشغول پردازش کانکشن است.
  4. سرور listening مجدداً راه‌اندازی (restart) می‌شود.

در حالت عادی، راه‌اندازی مجدد سرور listening در مرحله bind با خطا مواجه می‌شود؛ اما با تنظیم گزینه SO_REUSEADDR روی سوکت، عملیات bind با موفقیت انجام خواهد شد.

بنابراین در تمامی شرایطی که احتمال راه‌اندازی مجدد سرور وجود دارد، استفاده از این گزینه ضروری است.

این گزینه همچنین زمانی که یک نمونه (instance) چندین آدرس IP محلی دارد نیز کاربرد دارد.

برای مثال، فرض کنید اینستنسی با آدرس IP اصلی 198.69.10.2 و aliasهای 198.69.10.128 و 198.69.10.129 داریم. اگر بخواهیم سه سرور مختلف را روی یک پورت یکسان در این اینستنس اجرا کنیم، بدون تنظیمات لازم عملیات bind با خطا روبه‌رو خواهد شد.

اما با استفاده از گزینه SO_REUSEADDR می‌توان هر سه سرور را راه‌اندازی کرد. سرور HTTP اول با آدرس محلی wildcard و سرورهای دوم و سوم به ترتیب با آدرس‌های محلی 198.69.10.128 و 198.69.10.129 شروع به کار می‌کنند. در این حالت، درخواست‌های ورودی به 198.69.10.128 به سرور دوم، درخواست‌های 198.69.10.129 به سرور سوم و سایر درخواست‌ها به سرور اول ارسال می‌شوند.

در TCP امکان راه‌اندازی چند سرور با IP و پورت کاملاً یکسان (completely duplicated binding) وجود ندارد. با این حال، در صورت پشتیبانی پروتکل transport در UDP این امکان وجود دارد؛ قابلیتی که عمدتاً همراه با Multicast برای اجرای هم‌زمان چند نمونه از یک برنامه روی یک میزبان استفاده می‌شود (جزئیات بیشتر در فصل‌های ۲۰ و ۲۱ بررسی خواهد شد).

گزینه SO_REUSEPORT با اضافه شدن پشتیبانی از Multicast ارائه شد. اگر هر سوکت این گزینه را فعال و سپس bind کند، امکان استفاده از IP و پورت کاملاً یکسان (completely duplicated binding) میسر خواهد شد.

۴. گزینه‌های سوکت TCP

این گزینه‌ها زمانی کاربرد دارند که مقدار level بر روی IPPROTO_TCP تنظیم شده باشد.

TCP_MAXSEG

این گزینه برای خواندن یا تنظیم اندازه MSS در کانکشن‌های TCP استفاده می‌شود.

  • مقدار MSS: حداکثر حجم داده‌ای که TCP می‌تواند ارسال کند (به تصویر زیر مراجعه فرمایید).
  • معمولاً مقدار MSS طی تبادل بسته‌های SYN تعیین می‌شود و طرفین مقدار کوچک‌تر را به عنوان مقدار نهایی انتخاب می‌کنند.
تصویر مقاله

TCP_NODELAY

این گزینه الگوریتم Nagle را در TCP غیرفعال می‌کند.

  • هدف از الگوریتم Nagle، کاهش تعداد بسته‌های کوچک در شبکه است.
  • منظور از «بسته کوچک»، بسته‌ای است که اندازه آن از مقدار MSS کمتر باشد.

بیایید سناریوی ارسال رشته "hello!" به یک سرور echo را در نظر بگیریم.

اگر الگوریتم Nagle خاموش باشد، هر کاراکتر در یک بسته جداگانه قرار می‌گیرد و بدون معطلی برای دریافت ACK ارسال می‌شود.

تصویر مقاله

اما در صورت روشن بودن الگوریتم Nagle، کاراکتر اول ارسال شده و برنامه تا رسیدن ACK منتظر می‌ماند؛ سپس کاراکترهای بعدی را به شکل تجمیع‌شده در قالب یک بسته ارسال می‌کند.

تصویر مقاله

الگوریتم Nagle معمولاً همراه با الگوریتم delayed ACK در TCP مورد استفاده قرار می‌گیرد.

در الگوریتم delayed ACK، لایه TCP بسته ACK را بلافاصله نمی‌فرستد؛ بلکه منتظر می‌ماند تا اگر داده‌ای برای ارسال به طرف مقابل وجود داشت، ACK را همراه آن ارسال کند. البته اگر تا مدت مشخصی داده‌ای تولید نشود، ACK به تنهایی فرستاده خواهد شد.

در زمان فعال بودن الگوریتم delayed ACK، اگر سرور پاسخی برای ارسال به کلاینت نداشته باشد، به دلیل نبود داده برای الحاق به ACK، تأخیر شدیدی به وجود می‌آید. در این سناریو باید با استفاده از TCP_NODELAY الگوریتم Nagle را غیرفعال کرد.

یکی دیگر از موقعیت‌های چالش‌برانگیز زمانی است که درخواست ارسالی به سرور به بخش‌های کوچک تقسیم می‌شود.

فرض کنید کلاینت برای ارسال یک درخواست ۴۰۰ بایتی، آن را به یک بخش ۴ بایتی (نوع درخواست) و یک بخش ۳۹۶ بایتی (داده اصلی) تقسیم کند. کلاینت بسته ۴ بایتی را می‌فرستد و منتظر ACK می‌ماند؛ سرور نیز با دریافت ۴ بایت قادر به پردازش درخواست نیست و باید منتظر ۳۹۶ بایت بعدی بماند. در این شرایط با فعال‌سازی گزینه TCP_NODELAY و ارسال متوالی هر دو بخش درخواست، این مشکل به راحتی حل می‌شود.

۵. تابع fcntl

تصویر مقاله

نام تابع fcntl برگرفته از عبارت «file control» است.

تصویر مقاله

استاندارد POSIX توصیه می‌کند که برای انجام ۴ عملیات اول مربوط به سوکت‌ها در جدول فوق، از تابع fcntl استفاده شود.

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

  • قابلیت Nonblocking I/O: در فصل ۱۶ به تفصیل بررسی شده است.
  • قابلیت Signal-driven I/O: در فصل ۲۵ به تفصیل بررسی شده است.
  • دستور F_SETOWN: مالک سوکت را برای دریافت سیگنال‌های SIGIO و SIGURG مشخص می‌کند.
  • دستور F_GETOWN: مالک فعلی سوکت را برمی‌گرداند.

۶. منابع

  1. Unix Network Programming