دانستنی‌ها

مقایسه Java NIO و IO

در آموزش‌های جاوا دیدیم که API جدید java.NIO به جاوا اضافه شده و با قابلیت‌هایش می‌تواند به‌ جای API قدیمی java.IO استفاده شود. حالا سوالی که پیش می‌آید این است که چه زمانی باید از java.NIO استفاده شود و چه زمانی بهتر است همچنان از API قدیمی java.IO استفاده کنیم؟

در این متن، تلاش می‌کنم تفاوت‌های بین این دو API را شفاف‌سازی کنم و در آخر بتوانیم نتیجه بگیریم که از هر کدام کجا استفاده کنیم و اینکه این انتخاب چگونه طراحی کد ما را دست‌خوش تغییر می‌کند.

تفاوت‌های اصلی بین NIO و IO

جدول زیر تفاوت‌های اصلی بین این دو API را مشخص می‌کند. در ادامه به صورت دقیق‌تر این تفاوت‌ها را بررسی می‌کنیم.

NIO IO
بافرمحور جویبارمحور
غیر مسدودکننده  مسدودکننده
Selectorها  

جویبارمحور در مقابل بافرمحور

اولین تفاوت عمده بین java NIO و IO این است که IO جویبارمحور است ولی NIO بافرمحور است. اما این به چه معناست؟

زمانی که می‌گوییم Java IO جویبارمحور است، منظورمان این است که شما می‌توانید در یک بار خواندن، یک یا چند بایت از جویبار ورودی را بخوانید. اینکه چگونه از این بایت‌ها استفاده کنید بستگی به خودتان دارد. اما باید در نظر داشته باشید که این بایت‌ها جایی ذخیره نشده‌اند. شما نمی‌توانید روی داده‌های این جویبار جلو و عقب بروید و برای این منظور، باید ابتدا داده‌ها را ذخیره (کش) کنید.

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

مسدودکننده در مقابل غیرمسدودکننده

جویبارها در Java IO به صورت مسدودکننده هستند. به این معنی که وقتی نخ پردازشی‌ای متدهای ()read یا ()write را صدا می‌زند، نخ باید منتظر بماند تا داده مورد نظر به صورت کامل خوانده (یا نوشته) شود و آن نخ در آن زمان هیچ کار دیگری نمی‌تواند انجام دهد.

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

همین وضعیت برای نوشتن در حالت غیرمسدودکننده نیز برقرار است. یک نخ پردازشی می‌تواند درخواست دهد که داده‌ای در کانال نوشته شود اما منتظر نماند تا عمل نوشتن تمام شود، بلکه می‌تواند در این زمان سراغ پردازش دیگری برود.

در اکثر اوقات، در حینی که یک نخ‌ پردازشی درخواستی برای IO داده ولی نتیجه هنوز انجام نشده، از این زمان برای درخواست دادن روی کانال‌های دیگر استفاده می‌کند. به این وسیله تنها یک نخ پردازشی قابلیت مدیریت چندین کانال ورودی/خروجی را پیدا می‌کند.

انتخاب‌کننده (Selector)ها

انتخاب‌کننده‌ها اجازه می‌دهند یک نخ پردازشی چندین کانال ورودی/خروجی را مدیریت کند. شما می‌توانید کانال‌های مختلف را روی یک انتخاب‌کننده تنظیم کنید و از یک نخ پردازشی برای «انتخاب» بین کانال‌های ورودی‌ای که داده‌ آماده برای پردازش دارند یا آماده نوشتن هستند استفاده کنید. این سازوکار انتخاب‌کننده، کار مدیریت چند کانال را تسهیل می‌کند.

چگونه انتخاب بین IO و NIO طراحی برنامه ما را دستخوش تغییر می‌کند

هر کدام از IO یا NIO را که به عنوان ابزار ورودی/خروجی خود انتخاب کنید ممکن است این جوانب از طراحی برنامه شما را تحت تاثیر قرار دهد:

  1. فراخوانی API های کلاس‌های NIO یا IO.
  2. پردازش داده.
  3. تعداد نخ‌های پردازشی استفاده‌شده برای پردازش داده.
فراخوانی API

تعجبی ندارد که فراخوانی APIها برای استفاده از کلاس‌های NIO با فرخوانی API برای استفاده از کلاس‌های IO متفاوت است. مثلا به جای اینکه بایت‌ به بایت از InputStream داده بخوانیم، ابتدا داده در بافر نوشته می‌شود و سپس از بافر پردازش می‌شود.

پردازش داده

انتخاب بین IO و NIO پردازش داده را نیز تحت تاثیر قرار می‌دهد.

در طراحی مبتنی بر IO، داده به صورت بایت‌ به بایت از InputStream یا یک Reader خوانده می‌شود. فرض کنید که در حال پردازش جویباری از داده متنی به صورت زیر هستیم.

Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890

این جویبار متنی باید به شکل زیر پردازش شود:

InputStream input = ... ; // get the InputStream from the client socket

BufferedReader reader = new BufferedReader(new InputStreamReader(input));

String nameLine   = reader.readLine();
String ageLine    = reader.readLine();
String emailLine  = reader.readLine();
String phoneLine  = reader.readLine();

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

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

شمای کار java IO
خواندن از یک جویبار به صورت مسدودکننده به کمک java IO

 

اما پیاده‌سازی با کمک Java NIO ظاهر متفاوتی دارد. یک مثال ساده‌شده مشابه زیر است:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

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

فرض کنید زمانی که کار متد (read(buffer تمام می‌شود، به جای تمام خط، تنها نصف خط در بافر موجود باشد، مثلا تنها رشته‌ “Name: an” وجود داشته باشد. آیا می‌توانید آن را پردازش کنید؟ نه واقعا. شما ناچارید صبر کنید که حداقل یک خط تمام داده دریافت شود تا بتوانید چیزی پردازش کنید.

خب از کجا باید بفهمیم که بافر به اندازه کافی داده دارد که پردازش را شروع کنیم؟ تنها راه فهمیدنش این است که داخل بافر را نگاه کنیم. نتیجه این می‌شود که ناچاریم چندین‌بار داخل بافر را نگاه کنیم تا ببینیم داده‌ مورد نظر را دارد یا خیر. اینکار هم غیربهینه است و هم ظاهر کد را کثیف می‌کند. برای مثال:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

while(! bufferFull(bytesRead) ) {
    bytesRead = inChannel.read(buffer);
}

متد ()bufferFull وظیفه بررسی این را دارد که چه مقدار داده داخل بافر خوانده شده ‌است و در صورت پر بودن بافر مقدار true برمی‌گرداند. به بیان دیگر اگر بافر به اندازه‌ای داده داشته باشد که قابل پردازش باشد، پر درنظر گرفته می‌شود.

باید دقت داشت که متد bufferFull باید بافر را همانگونه که تحویل گرفته تحویل دهد و هیچگونه تغییری در آن ندهد، چرا که اگر تغییری بدهد موقع خواندن از بافر ممکن است از محل صحیحی داده خوانده نشود. البته که اینکار غیرممکن نیست ولی باید آن را در نظر داشته‌ باشیم.

اگر بافر پر باشد، می‌تواند پردازش شود، اما اگر پر نباشد، شما ممکن است بسته به نیاز خود بتوانید همین مقدار داده را نیز پردازش کنید. اینکار در برخی کاربردها منطقی است و در بسیاری دیگر نه!

سازوکار حلقه‌ چک‌کننده‌ پر بودن بافر در شکل زیر نشان داده‌ شده است.

سازوکار حلقه‌ی چک کننده بافر
جاوا NIO: خواندن از کانال ورودی تا زمانی که مقدار مورد نیاز داده در بافر قرار بگیرد.

خلاصه

پکیج NIO اجازه مدیریت چندین کانال (فایل یا اتصال شبکه) را با تنها یک (یا تعداد اندکی) نخ پردازشی می‌دهد. اما استفاده از آن هزینه‌ای هم دارد. در ازای استفاده از تعداد نخ‌های کم‌تر، عملیات پردازش داده، نسبت به «حالت جویبار مسدودکننده در IO» پیچیده‌تر می‌شود.

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

شمای NIO: مدیریت اتصالات مختلف با یک نخ پردازشی
مدیریت اتصالات مختلف با یک نخ پردازشی با کمک NIO

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

نمای پیاده سازی یک سرور با استفاده از کلاس‌های سنتی IO
نمای پیاده‌سازی یک سرور با استفاده از کلاس‌های سنتی IO

 

منبع: tutorials.jenkov.com

.

.


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

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

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

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

آدرس صفحه ویرگول: javcup

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

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

‫2 دیدگاه ها

  1. سلام

    ممنون از برگردان مقاله. پیشنهاد می کنم واژه های تخصصی همچون Thread و Stream را به صورت انگلیسی استفاده کنید. به این دلیل که واژه های تخصصی کلمات شناخته شده ای هستند که در بخصوص کد های برنامه و مستندات API ها به همان صورت (انگلیسی) بکار برده می شوند.

    با احترام

    1. سلام
      کاملا درست می‌فرمایید. اما واژه‌های «نخ» و «جویبار» برای Thread و Stream چندان غیرمصطلح و کم‌کاربرد نیستند و در فیلم‌های آموزشی جاواکاپ و درس‌های دانشگاهی، از این معادل‌های فارسی کم‌و‌بیش استفاده می‌شه و به گوش شنوندگان آشنا هستند.
      ضمن اینکه وقتی در یک متن فارسی، واژه‌های انگلیسی زیاد تکرار و دیده بشن، خوانایی نوشته کم می‌شه. این دو واژه هم چون زیاد در این مقاله تکرار شدن، ترجیح دادیم از معادل فارسی‌شون استفاده کنیم تا یکدست بودن نوشته حفظ بشه.
      خیلی ممنون که نظرتون رو با ما در میون گذاشتید.

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

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

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