دانستنی‌ها

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

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

در برنامه های 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

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

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

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

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