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

در چهار فراخوانی اصلی سوکت، عملکرد حالت Blocking به صورت زیر است:

  • عملیات ورودی: اگر داده‌ای در بافر دریافت سوکت نباشد، متوقف شده و منتظر می‌ماند.
  • عملیات خروجی: در صورتی که بافر ارسال سوکت فضای خالی نداشته باشد، به حالت انتظار می‌رود.
  • پذیرش اتصال ورودی: اگر اتصال جدیدی آماده پذیرش نباشد، منتظر می‌ماند.
  • برقراری اتصال خروجی: تا زمان دریافت پاسخ ACK برای بسته SYN، در حالت انتظار باقی می‌ماند.

در مقابل، حالت Nonblocking به صورت زیر عمل می‌کند:

  • عملیات ورودی: اگر داده‌ای در بافر دریافت سوکت نباشد، بلافاصله خطای EWOULDBLOCK را برمی‌گرداند.
  • عملیات خروجی: اگر بافر ارسال فضای خالی نداشته باشد، فوراً خطای EWOULDBLOCK را برمی‌گرداند.
  • پذیرش اتصال ورودی: در صورت نبود اتصال جدید، بلافاصله خطای EWOULDBLOCK بازگردانده می‌شود.
  • برقراری اتصال خروجی: اگر امکان برقراری فوری اتصال وجود نداشته باشد، فرآیند اتصال را آغاز کرده و فوراً خطای EINPROGRESS را برمی‌گرداند.
از آنجا که در گذشته سیستم‌های System V هنگام مواجهه با Nonblocking I/O خطای EAGAIN را برمی‌گرداندند، در سیستم‌عامل‌های امروزی از جمله OpenBSD، هر دو خطای EAGAIN و EWOULDBLOCK در <sys/errno.h> به صورت یکسان تعریف شده‌اند.

در این فصل، با بررسی نمونه‌هایی از هر چهار عملیات و پیاده‌سازی کلاینتی جدید که با استفاده از Nonblocking connect چندین اتصال TCP را به شکل هم‌زمان برقرار می‌کند، با سازوکار Nonblocking I/O آشنا می‌شویم.

فهرست مطالب

Nonblocking Reads and Writes: تابع str_cli Nonblocking connect Nonblocking connect: Daytime Client Nonblocking connect: Web Client Nonblocking accept

Nonblocking Reads and Writes

بیایید تابع str_cli را که در فصل‌های ۵ و ۶ بررسی کردیم، این بار در حالت Nonblocking بازنویسی کنیم.

نسخه Blocking برای Multiplexing I/O در فصل ۶ (جهت مقایسه)

C

نسخه Nonblocking

C

با به تصویر کشیدن ارتباط با سرور از طریق str_cli روی یک خط زمانی (Timeline)، نکات زیر به وضوح نمایان می‌شود:

  • کلاینت هر زمان که امکان خواندن یا نوشتن فراهم بود، بدون هیچ وقفه‌ای به اجرای عملیات ادامه داد.
  • عملیات با سرعتی بالاتر نسبت به نسخه Blocking ساختار Multiplexing I/O در فصل ۶ به انجام رسید.
تصویر مقاله

با اینکه کد Nonblocking I/O سریع‌تر از کد Blocking اجرا شد، اما به شکل چشمگیری پیچیده‌تر بود.

از آنجا که با استفاده از دو فرآیند یا دو Thread می‌توان کدی بسیار ساده‌تر نوشت و در عین حال به سرعتی نزدیک به کد Nonblocking دست یافت، استفاده از کد Nonblocking I/O در اینجا مزیت چندانی نخواهد داشت (روش مبتنی بر Thread در فصل ۲۶ بررسی می‌شود).

  • در این حالت، فرآیند فرزند داده‌ها را از سرور به stdout منتقل می‌کند و فرآیند والد کار انتقال داده‌ها از stdin به سرور را انجام می‌دهد.
  • در نسخه مبتنی بر fork نیازی به استفاده از Nonblocking I/O نیست؛ زیرا وقتی داده‌ای برای خواندن نباشد، داده‌ای هم برای نوشتن وجود نخواهد داشت.
تصویر مقاله
C

Nonblocking connect

استفاده از Nonblocking connect مزایای زیر را به همراه دارد:

  • می‌توان هم‌زمان با اجرای فرآیند 3-way handshake، کارهای دیگر را به پیش برد.
  • امکان برقراری چندین اتصال به صورت هم‌زمان فراهم می‌شود.
  • می‌توان با کمک select، مدت‌زمان مهلت اتصال (Timeout) را تنظیم و مدیریت کرد.

با این وجود، هنگام استفاده از Nonblocking connect باید به استانداردهای POSIX توجه ویژه‌ای داشت؛ چرا که این ساختار در عمل مستعد مشکلات سازگاری و پورتابلیتی است.

  • در پیاده‌سازی سوکت، پس از برقراری موفقیت‌آمیز اتصال، توصیف‌کننده (Descriptor) باید تنها قابلیت نوشتن (Writable) داشته باشد.
  • اما در صورت وقوع خطا حین برقراری اتصال، توصیف‌کننده باید هم برای خواندن و هم برای نوشتن در دسترس قرار گیرد.

Nonblocking connect: Daytime Client

در کدهای زیر می‌توانیم تلاش‌های صورت‌گرفته برای رفع مشکلات سازگاری را مشاهده کنیم.

برقراری فوری اتصال بلافاصله پس از فراخوانی connect

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

  • فراخوانی getsockopt() برای بررسی مقدار بازگشتی و وضعیت error (روشی که در این مثال به کار رفته است)
  • فراخوانی getpeername() جهت بررسی وقوع خطای ENOTCONN
  • فراخوانی دستور read با طول ۰ بایت برای اعتبارسنجی اتصال
  • فراخوانی مجدد connect() برای بررسی خطای EISCONN

تفاوت‌های رفتاری تابع getsockopt()

  • در سیستم‌عامل Solaris، تابع getsockopt() مقدار ۱- را برمی‌گرداند و جزئیات خطا را درون errno قرار می‌دهد.
  • در پیاده‌سازی‌های مبتنی بر Berkeley، تابع getsockopt() مقدار ۰ را برمی‌گرداند و خطا را در متغیر error ثبت می‌کند.

قطع شدن اتصال (Connection Interruption)

  • حتی اگر اتصال قطع شود، بلافاصله نمی‌توان متوجه این موضوع شد.
  • تنها با فراخوانی select است که می‌توان قطع شدن اتصال را شناسایی کرد.
C

Nonblocking connect: Web Client

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

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

این مثال، نحوه برقراری چندین اتصال موازی را به نمایش می‌گذارد.

C

هرچند برقراری اتصالات هم‌زمان با Nonblocking connect مزایای عملکردی دارد، اما می‌تواند مشکلات دیگری را نیز رقم بزند.

  • پروتکل TCP در شرایط ازدحام (یعنی زمانی که تعداد بسته‌ها از ظرفیت پردازش فراتر می‌رود)، بسته‌ها را حذف می‌کند تا ترافیک را کنترل نماید؛ در این زمینه الگوریتم‌های Slow Start و TCP congestion avoidance به شکل گسترده‌ای به کار می‌روند (فصل ۲۱). در وضعیتی مانند این مثال که چندین اتصال به یک سرور برقرار است، اگر در یکی از اتصالات بسته‌ای از دست برود، سایر اتصال‌ها از این واقعه بی‌خبر مانده و به ارسال بسته ادامه می‌دهند که این موضوع ازدحام شبکه را تشدید می‌کند.
  • علاوه بر این، ایجاد تعداد زیادی اتصال بار پردازشی سرور را افزایش می‌دهد.

Nonblocking accept

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

شاید این‌طور به نظر برسد که وقتی select منتظر اتصال ورودی می‌ماند و پس از آماده شدن آن را برای پردازش تحویل می‌دهد، تابع accept هرگز Block نمی‌شود و نیازی به Nonblocking کردن سوکت نیست؛ اما این موضوع استثناهایی دارد.

  • اگر سرور به دلیل مشغله زیاد، بین اجرای select و فراخوانی accept دچار تاخیر شود،
  • و در این بازه کلاینت ارتباط را قطع کند،
  • تابع accept در سمت سرور تا رسیدن درخواست اتصال بعدی در حالت انتظار (Block) باقی خواهد ماند.

برای جلوگیری از بروز این نوع اختلال و حملات محروم‌سازی از سرویس (DoS)، راهکارهای زیر توصیه می‌شود:

  • سوکت شنونده را در حالت Nonblocking قرار دهید.
  • خطاهای احتمالی رخ‌داده هنگام فراخوانی accept را نادیده بگیرید.

جمع‌بندی

  • تابع select در کنار Nonblocking I/O به کار می‌رود تا زمان دقیق آماده شدن توصیف‌کننده برای خواندن یا نوشتن را مشخص کند.
  • قابلیت Nonblocking connect امکان انجام کارهای موازی در طول فرآیند Handshake را فراهم می‌کند و بسیار کارآمد به نظر می‌رسد، اما از نظر سازگاری با چالش‌هایی همراه است.
  • برقراری چندین اتصال با استفاده از Nonblocking connect زمان اجرا را کاهش می‌دهد، اما سازگاری مناسبی با مکانیزم‌های کنترل ازدحام TCP ندارد.