دانستنی‌ها

ورودی/خروجی مسدودکننده و غیر مسدودکننده در جاوا

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

در برنامه‌های client و server، وقتی کاربر یک درخواست به سرور می‌فرستد، ابتدا سرور آن درخواست را پردازش می‌کند و سپس پاسخی مناسب برای کاربر می‌فرستد. برای اینکه این «درخواست و پاسخ» اتفاق بیافتد، باید در ابتدا یک راه ارتباطی بین کاربر و سرور برقرار شود. در اینجا از سوکت‌ها استفاده می‌شود. برای برقراری ارتباط، کاربر و سرور هر دو به دو سر یک سوکت وصل می‌شوند. سرور گوش به زنگ سوکت می‌نشیند که اگر کاربر درخواستی فرستاد، به آن پاسخ بدهد.

ارتباط بین کلاینت و سرور

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

I/O در دنیای کامپیوتر به معنای ورودی/خروجی (Input/Output) بوده و مربوط به نحوه برقراری ارتباط بین سیستم‌ها است.

ورودی/خروجی مسدودکننده (Blocking I/O)

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

در اینجا یک شی سرور‌سوکت ساخته شده است که به درخواست اتصالات در یک پورت مشخص گوش می‌دهد. در این حالت این پورت سرور به برنامه ما مرتبط شده است.

باز کردن سرور سوکت

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

اتصال کاربر به سرور سوکت و سپس ساخت یک سوکت جدید برای ارتباط با این کاربرسرور سوکت آزاد می‌شود و یک سوکت مخصوص ارتباط با این کاربر ساخته می‌شود.

حالا می‌توان جریان ورودی و خروجی را از سوکت گرفت.

یک خط از جریانِ داده‌ِ ورودی که فرستنده برای ما فرستاده، خوانده و پردازش می‌شود. سپس پاسخ به کاربر در جریان خروجی‌ای که به سوکت وصل است نوشته می‌شود. این کار تا زمانی ادامه پیدا می‌کند که کاربر به سرور، “Done” را بفرستد یا Cntrl-C را فشار دهد و کاراکتر «اتمام ورودی» را وارد کند.

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

سورس‌کد کامل برنامه کلاینت و سرور در حالت ورودی/خروجی مسدودکننده در این لینک قابل دسترس است.

چندین کلاینت با یک سرور در ارتباط هستند

در این رویکرد اشکالاتی وجود دارد:

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

۲- در حالاتی ممکن است چند نخ در حالت انتظار برای درخواست از سمت کاربر باشند و از آن‌ها استفاده‌ای نشود که نوعی اسراف منابع به حساب می‌آید. 

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

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

ورودی/خروجی غیر مسدودکننده (Non-blocking I/O)

ورودی خروجی غیرمسدود کننده

با ورودی/خروجی غیر مسدودکننده، می‌توان چند اتصال را به طور همزمان با استفاده از یک نخ پردازشی، مدیریت کرد. قبل از اینکه به جزئیات بپردازیم، ابتدا بیایید چند مفهوم را مرور کنیم:

۱- در سیستم‌هایی که بر پایه ورودی/خروجی غیرمسدودکننده (مثلا پکیج NIO جاوا) هستند، به جای داشتن جریان داده‌های ورودی و خروجی،‌ داده‌ها در بافر (Buffer) خوانده و نوشته می‌شوند. می‌توان به بافر به عنوان یک فضای ذخیره موقت نگاه کرد. کلاس‌های مختلفی برای پیاده‌سازی بافر در NIO جاوا وجود دارد (برای مثال  ByteBuffer , CharBuffer , ShortBuffer اما معمولا از ByteBuffer در برنامه‌های تحت شبکه استفاده می‌شود.)

۲- کانال (channel) واسطه‌ای است که داده‌ها را به داخل و خارج از بافر منتقل می‌کند و می‌توان به آن، به عنوان نقطه‌ای پایانی برای ارتباط نگاه کرد. (برای مثال کلاس «SocketChannel» را در نظر بگیرید. این کلاس از سوکت‌های TCP می‌خواند و داخشان می‌نویسد اما داده‌ها باید به شکل اشیایی از کلاس بایت‌بافر (ByteBuffer) تبدیل شوند.)

۳-سپس باید با مفهومی به نام «انتخاب براساس آمادگی (Readiness Selection)» آشنا شویم که به معنای انتخاب سوکتی است که هنگام خواندن و نوشتن داده‌ها مسدود نمی‌شود. 

جاوا یک کلاس یه نام «Selector» دارد که این کلاس به یک نخ پردازشی اجازه می‌دهد رویدادهای ورودی/خروجی را در چند کانال بررسی کند. یعنی این کلاس می‌تواند آمادگی یک کانال را برای خواندن و نوشتن بررسی کند. کانال‌های مختلفی را می‌توان به یک شی Selector وصل کرد. همچنین باید به یاد داشته باشیم که به هر یک از این کانال‌ها یک کلید انتخاب (SelectionKey) نسبت داده می‌شود که مانند یک اشاره‌گر به آن کانال عمل می‌کند.

معماری استفاده از selector

حالا وقت آن است که کمی پیاده‌سازی انجام دهیم. ابتدا کد بخش سرور را می‌نویسیم:

ابتدا باید یک شی انتخاب‌کننده (selector) ساخته شود که چندین کانال را مدیریت کند. در این صورت سرور می‌تواند همه اتصالاتی که آماده دریافت خروجی یا ارسال ورودی هستند را پیدا کند. 

سپس باید یک کانال سرور‌سوکت با رویکرد غیر مسدودشونده ایجاد کرد. کلاس «ServerSocketChannel» مسئول پذیرش اتصالات ورودی جدید است.

سپس یک هاست و پورت به کانال سرور‌سوکت اختصاص می‌یابد.

در ادامه، کانال سرورسوکت باید توسط انتخاب‌کننده ثبت شود. پارامتر SelectionKey.OP_ACCEPT، به انتخاب‌کننده می‌گوید که فقط به اتصالات ورودی گوش کند. پارامتر دوم، نشان می‌دهد که چه دسته از رویدادهایی در کانالِ تحتِ نظارت برای ما مطلوب است. در اینجا ‘‘OP ACCEPT” به این معنا است که کانال سرورسوکت آماده پذیرش اتصال جدید از سمت کاربر است.

سپس متد ()select صدا زده می‌شود تا بررسی شود که آیا چیزی برای انجام کار آماده است یا خیر. اگر بخواهیم که به طور نامحدود منتظر فعالیت جدید باشیم، می‌توان این تابع را درون یک حلقه بی‌نهایت صدا زد. «ReadyCount» تعداد کانال‌های آماده را نشان می‌دهد. اگر تعداد کانال‌های آماده صفر باشد، به انتظار ادامه داده می‌شود. 

زمانی که انتخاب‌کننده یک کانال آماده پیدا می‌کند، متد ()selectedKeys یک مجموعه از کلیدهای آماده برمی‌گرداند که هر عضو آن نماینده یک کانال آماده است. می‌توان روی هر کانال یک حلقه زد و عملیات مورد نیاز را انجام داد. 

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

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

اگر کلید «قابل قبول (acceptable)» باشد به این معناست که کاربری می‌خواهد اتصال جدید ایجاد کند.

اگر کلید «قابل خواندن (readable)» باشد، به این معناست که سرور آماده است که داده را از کاربر بخواند.

اگر کلید «قابل نوشتن (writable)» باشد، به این معناست که سرور می‌تواند داده‌ای را برای کاربر بنویسد. 

حال در ادامه برنامه‌ای ساده برای سمت کاربر (‌‌client) نشان داده می‌شود. 

در ابتدا یک سوکت‌کانال (socket channel) برای ارتباط با سرور ساخته می‌شود. 

سپس به جای استفاده از جریان ورودی و خروجی سوکت، داده‌ها در خود کانال نوشته می‌شوند. همانطور که می‌دانیم داده‌ها برای نوشته شدن در کانال، ابتدا باید به شکل شی بایت‌بافر (ByteBuffer)، تحویل شوند. در اینجا یک بایت‌بافر با ظرفیت ۷۴ بایت ساخته شده است.

در نهایت، پیام کاربر در بافر نوشته شده و بافر آن را در کانال می‌نویسد.

سورس‌کد کامل ورودی/خروجی غیر مسدودکننده (NIO)، را به طور کامل در اینجا مطالعه کنید.

این مقاله برای کمک به درک مفاهیم ورودی و خروجی مسدودکننده و غیر مسدودکننده است و در این راستا با استفاده از جاوا NIO یک برنامه کلاینت-سروری ساده نوشته شد. اما در نظر داشته باشید به جای اینکه برنامه خود را مستقیما با استفاده از جاوا NIO پیاده‌سازی کنید، می‌توانید از چارچوب‌های نرم‌افزاری قابل اعتماد و پربازده‌ای مثل netty استفاده کنید.

منبع: وبلاگ Rukshani Athapathu در مدیوم

.

.

.

.

با ما همراه باشید


آدرس کانال تلگرام: JavaCupIR@

آدرس اکانت توییتر: JavaCupIR@

آدرس صفحه اینستاگرام: javacup.ir

صفحه ویرگول: javcup

آدرس گروه لینکدین: Iranian Java Developers

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا