المعرفة الأساسية
متزامن ، غير متزامن ، حظر ، عدم الحظر
بادئ ذي بدء ، من السهل للغاية الخلط بين هذه المفاهيم ، لكنها تشارك في NIO ، لذلك دعونا نلخصها.
المزامنة: عند إرجاع مكالمة API ، يعرف المتصل كيف يتم ناتج العملية (كم عدد البايتات التي يتم قراءة/الكتابة بالفعل).
غير متزامن: بالمقارنة مع المزامنة ، لا يعرف المتصل نتيجة العملية عند إرجاع استدعاء API ، وسيقوم رد الاتصال بإخطار النتيجة لاحقًا.
الحظر: عندما لا توجد بيانات يمكن قراءتها أو لا يمكن كتابة جميع البيانات ، قم بتعليق مؤشر الترابط الحالي.
عدم الحظر: عند القراءة ، يمكنك قراءة بقدر البيانات التي يمكنك قراءتها ثم العودة. عند الكتابة ، يمكنك الكتابة بقدر البيانات التي يمكنك كتابتها ثم العودة.
بالنسبة لعمليات الإدخال/الإخراج ، وفقًا لوثائق موقع Oracle الرسمي ، فإن معيار التزامن والقسمة غير المتزامنة هو "ما إذا كان المتصل يحتاج إلى انتظار عملية الإدخال/الإخراج". لا يعني هذا "انتظار عملية إكمال I/O" أنه يجب قراءة البيانات أو يجب كتابة جميع البيانات ، ولكن ما إذا كان المتصل يحتاج إلى الانتظار عند إجراء عملية I/O بالفعل ، مثل الوقت الذي يتم فيه إرسال البيانات بين المخزن المؤقت لبروتوكول TCP/IP ومؤسسة JVM.
لذلك ، فإن أساليب القراءة () والكتابة () شائعة الاستخدام هي I/O متزامنة. ينقسم I/O المتزامن إلى وضعين: الحظر وغير الحظر. إذا كان وضع عدم الحظر ، فسيتم إرجاعه مباشرة عندما يكتشف أنه لا توجد بيانات يمكن قراءتها ، ولا يتم تنفيذ عملية الإدخال/الإخراج حقًا.
باختصار ، في Java ، لا يوجد في الواقع ثلاث آليات فقط: الحظر المتزامن I/O ، I/O متزامن غير متزامن I/O. ما نتحدث عنه أدناه هو الأولين. بدأت JDK1.7 في تقديم I/O غير المتزامن ، والذي يسمى Nio.2.
IO التقليدية
نحن نعلم أن ظهور تقنية جديدة يرافقه دائمًا التحسينات والتحسينات ، وكذلك ظهور Javanio.
I/O التقليدي يحظر الإدخال/الإخراج ، والمشكلة الرئيسية هي مضيعة لموارد النظام. على سبيل المثال ، من أجل قراءة بيانات اتصال TCP ، نسمي طريقة read () لـ inputStream ، مما سيؤدي إلى تعليق مؤشر الترابط الحالي ولن يتم إيقاظه حتى تصل البيانات. يشغل الخيط موارد الذاكرة (مكدس مؤشر ترابط التخزين) خلال فترة وصول البيانات ، ولكن لا يفعل شيئًا. هذا ما يقوله المثل ، احتلال الحفرة وليس أنبوب. من أجل قراءة بيانات الاتصالات الأخرى ، يتعين علينا أن نبدأ موضوعًا آخر. قد يكون هذا جيدًا عندما لا يكون هناك العديد من الاتصالات المتزامنة ، ولكن عندما يصل عدد الاتصالات إلى مقياس معين ، سيتم استهلاك موارد الذاكرة بواسطة عدد كبير من المواضيع. من ناحية أخرى ، يتطلب تبديل مؤشرات الترابط تغيير حالة المعالج ، مثل قيم عدادات البرنامج والسجلات ، وبالتالي فإن التبديل بين عدد كبير من المواضيع هو أيضًا مضيعة للموارد.
من خلال تطوير التكنولوجيا ، توفر أنظمة التشغيل الحديثة آليات I/O جديدة يمكنها تجنب إهدار الموارد هذه. بناءً على ذلك ، وُلدت Javanio ، والميزة التمثيلية لـ NIO هي I/O غير محظورة. بعد ذلك مباشرة ، وجدنا أن استخدام I/O غير المحظور لا يمكن حل المشكلة ، لأنه في وضع عدم الحظر ، ستعود طريقة Read () فورًا عند عدم قراءة البيانات. لا نعرف متى ستصل البيانات ، لذلك يمكننا الاستمرار فقط في استدعاء طريقة القراءة () للمحاولة مرة أخرى. من الواضح أن هذا مضيعة لموارد وحدة المعالجة المركزية. من ما يلي ، يمكننا أن نعرف أن مكون المحدد قد ولد لحل هذه المشكلة.
مكونات جافانيو الأساسية
1. القناة
مفهوم
تعتمد جميع عمليات الإدخال/الإخراج في Javanio على كائنات القناة ، تمامًا كما تعتمد عمليات الدفق على كائنات الدفق ، لذلك من الضروري أولاً فهم القناة. المحتوى التالي مقتطف من وثائق JDK1.8
يمثل Achannel اتصال الملحق بملحق Suchasahardwaredevice و Afile و Annetworksocket و OPROMPROO يكون يمكن إجراؤه على إدخال/OOPERATIONS مميز واحد ، أو فوركسامليرشيب أو الكتابة.
من المحتوى أعلاه ، يمكننا أن نرى أن القناة تمثل اتصالًا بكيان معين ، والذي يمكن أن يكون ملفًا ، ومقبس الشبكة ، وما إلى ذلك ، وبعبارة أخرى ، القناة هي جسر توفره Javanio لبرامجنا للتفاعل مع خدمات I/O الأساسية لنظام التشغيل.
القنوات هي وصف أساسي وتجريدي للغاية ، تتفاعل مع خدمات الإدخال/الإخراج المختلفة ، وتنفيذ عمليات I/O مختلفة ، وتنفيذ تطبيقات مختلفة. لذلك ، تشمل تلك المحددة filechannel ، socketchannel ، إلخ.
القناة تشبه الدفق عند استخدامه. يمكنه قراءة البيانات في المخزن المؤقت أو كتابة البيانات في المخزن المؤقت إلى القناة.
بالطبع ، هناك أيضًا اختلافات ، تنعكس بشكل أساسي في النقطتين التاليتين:
يمكن قراءة القناة وكتابتها ، بينما يتم تشغيل الدفق في اتجاه واحد (مقسومًا إلى inputstream و outputstream)
تحتوي القناة على وضع I/O غير محظور
ينجز
إن تطبيقات القناة الأكثر استخدامًا في Javanio هي كما يلي ، ويمكن ملاحظة أنها تتوافق مع فصول التشغيل التقليدية I/O واحدة تلو الأخرى.
Filechannel: اقرأ واكتب الملفات
DatagramChannel: اتصال شبكة بروتوكول UDP
Socketchannel: اتصال شبكة بروتوكول TCP
ServersocketchAnnel: استمع إلى اتصالات TCP
2.Buffer
المخزن المؤقت المستخدم في NIO ليس صفيف بايت بسيط ، ولكنه فئة عازلة مغلفة. من خلال واجهة برمجة التطبيقات التي توفرها ، يمكننا معالجة البيانات بمرونة. لنلقي نظرة فاحصة.
المقابلة لأنواع Java Basic ، توفر NIO مجموعة متنوعة من أنواع المخزن المؤقت ، مثل Bytebuffer و Charbuffer و Intbuffer ، وما إلى ذلك. الفرق هو أن طول وحدة المخزن المؤقت يختلف عند القراءة والكتابة (القراءة والكتابة بوحدات من متغيرات النوع المقابل).
هناك 3 متغيرات مهمة للغاية في المخزن المؤقت. هم مفتاح فهم آلية العمل للمخزن المؤقت ، أي
السعة (إجمالي السعة)
الموضع (الموضع الحالي للمؤشر)
الحد (قراءة/كتابة موقف الحدود)
تشبه طريقة عمل المخزن المؤقت إلى حد كبير صفائف الأحرف في C. في القياس ، السعة هي الطول الإجمالي للمصفوفة ، والموضع هو متغير ترجمي بالنسبة لنا لقراءة/كتابة الأحرف ، والحد هو موضع الحرف النهائي. وضع المتغيرات الثلاثة في بداية المخزن المؤقت هو كما يلي
أثناء عملية قراءة/كتابة المخزن المؤقت ، سينتقل الموضع للخلف ، والحد هو حدود حركة الموضع. ليس من الصعب تخيل أنه عند الكتابة إلى المخزن المؤقت ، يجب ضبط الحد على حجم السعة ، وعند القراءة إلى المخزن المؤقت ، يجب ضبط الحد على الموضع النهائي الفعلي للبيانات. (ملاحظة: كتابة بيانات المخزن المؤقت إلى القناة هي عملية قراءة عازلة ، وقراءة البيانات من القناة إلى المخزن المؤقت هي عملية كتابة مخزن مؤقت)
قبل قراءة/عمليات الكتابة على المخزن المؤقت ، يمكننا استدعاء بعض الطرق الإضافية التي توفرها فئة المخزن المؤقت لتعيين قيم الموضع والحد بشكل صحيح ، على النحو التالي
Flip (): حدد الحد لقيمة الموضع ، ثم تعيين الموضع على 0. المكالمات قبل قراءة المخزن المؤقت.
REWIND (): فقط قم بتعيين الموضع 0. يتم استدعاؤه عادةً قبل إعادة قراءة بيانات المخزن المؤقت ، على سبيل المثال ، سيتم استخدامه عند قراءة بيانات نفس المخزن المؤقت وكتابتها إلى قنوات متعددة.
Clear (): العودة إلى الحالة الأولية ، أي أن الحد يساوي السعة ، الموضع المحدد على 0. اتصل بالمخزن المؤقت قبل الكتابة.
Compact (): انقل البيانات غير المقروءة (البيانات بين الموضع والحد) إلى بداية المخزن المؤقت وتعيين الموضع على الموضع التالي في نهاية هذه البيانات. في الواقع ، فإنه يعادل كتابة مثل هذه البيانات إلى المخزن المؤقت مرة أخرى.
بعد ذلك ، انظر إلى مثال ، استخدم filechannel لقراءة الملفات النصية وكتابةها ، واستخدم هذا المثال للتحقق من الخصائص القابلة للقراءة والقابلة للكتابة والاستخدام الأساسي للمخزن المؤقت (لاحظ أنه لا يمكن ضبط Filechannel على وضع عدم الحظر).
قناة FileChannel = new RandomAccessFile ("test.txt" ، "RW"). getChannel () ؛ channel.position (channel.size ()) ؛ // انقل مؤشر الملف إلى النهاية (إلحاق الكتابة) bytebuffer bytebuffer = bytebuffer.allocate (20) World!/n ".getBytes (standardcharsets.utf_8)) ؛ // buffer -> channelbytebuffer.flip () ؛ بينما (bytebuffer.hasremaining ()) {channel.write (bytebuffer) ؛} channel.position (0) charsetdecoder decoder = standardcharsets.utf_8.newdecoder () ؛ // قراءة جميع البيانات bytebuffer.clear () ؛ بينما (channel.read (bytebuffer)! = -1 || bytebuffer.position ()> 0) {bytebuffer.flip () ؛ // decode charbuffer.clear () ؛ decoder.decode (bytebuffer ، Charbuffer ، false) ؛ system.out.print (charbuffer.flip ().في هذا المثال ، يتم استخدام اثنين من المخازن المؤقتة ، حيث يكون Bytebuffer هو المخزن المؤقت للبيانات لقراءة القناة والكتابة ، ويتم استخدام Charbuffer لتخزين الشخصيات التي تم فك تشفيرها. استخدام Clear () و flip () كما هو مذكور أعلاه. تجدر الإشارة إلى أن الطريقة الأخيرة المدمجة () هي ، حتى لو كان حجم شاربافير كافيًا تمامًا لاستيعاب Bytebuffer البيانات التي تم فك تشفيرها ، فإن هذا المضغوط ضروري أيضًا. وذلك لأن ترميز UTF-8 للأحرف الصينية الشائعة الاستخدام يمثل 3 بايت ، لذلك هناك احتمال كبير أن يحدث في الاقتطاع الأوسط. يرجى الاطلاع على الشكل أدناه:
عندما يقرأ وحدة فك الترميز 0xE4 في نهاية المخزن المؤقت ، لا يمكن تعيينه إلى Unicode. يتم استخدام المعلمة الثالثة لطريقة Decode () ، FALSE ، لجعل وحدة فك ترميز تعامل بايت لا يمكن الخوض والبيانات اللاحقة كبيانات إضافية. لذلك ، ستتوقف طريقة Decode () هنا ، وسوف يعود الموضع إلى موضع 0xE4. وبهذه الطريقة ، يتم ترك البايت الأول المشفر بواسطة كلمة "متوسطة" في المخزن المؤقت ، ويجب أن يتم ضغطها في المقدمة والتقسيم مع بيانات التسلسل الصحيحة واللاحقة. فيما يتعلق بترميز الأحرف ، يمكنك الرجوع إلى " شرح ANSI و Unicode و BMP و UTF ومفاهيم الترميز الأخرى "
راجع للشغل ، فإن charsetDecoder في المثال هو أيضًا ميزة جديدة لجافانيو ، لذلك كان يجب أن تكتشف قليلاً. عمليات NIO موجهة نحو المخزن المؤقت (I/O التقليدية موجهة نحو الدفق).
في هذه المرحلة ، تعلمنا عن الاستخدام الأساسي للقناة والمخزن المؤقت. بعد ذلك ، سوف نتحدث عن مكونات مهمة للسماح لخيط بإدارة قنوات متعددة.
3.selector
ما هو المحدد
المحدد هو مكون خاص يستخدم لجمع الحالة (أو الأحداث) لكل قناة. نقوم أولاً بتسجيل القناة على المحدد ونضع الحدث الذي نهتم به ، ثم يمكننا الانتظار بهدوء حتى يحدث الحدث عن طريق استدعاء طريقة SELECT ().
تحتوي القناة على الأحداث الأربعة التالية لكي نستمع إليها:
قبول: هناك اتصال مقبول
الاتصال: الاتصال بنجاح
اقرأ: هناك بيانات للقراءة
اكتب: يمكنك كتابة البيانات
لماذا تستخدم المحدد
كما ذكر أعلاه ، إذا كنت تستخدم حظر الإدخال/الإخراج ، فأنت بحاجة إلى متعدد الخيوط (مضيعة للذاكرة) ، وإذا كنت تستخدم I/O غير المحظورة ، فأنت بحاجة إلى المحاولة باستمرار مرة أخرى (استهلاك وحدة المعالجة المركزية). ظهور المحدد يحل هذه المشكلة المحرجة. في وضع عدم الحظر ، من خلال المحدد ، تعمل مؤشرات الترابط الخاصة بنا فقط للقنوات الجاهزة ، وليس هناك حاجة إلى المحاولة بشكل أعمى. على سبيل المثال ، عندما لا يتم الوصول إلى أي بيانات في جميع القنوات ، لا يحدث أي حدث للقراءة ، وسيتم تعليق موضوعنا بطريقة Select () ، وبالتالي التخلي عن موارد وحدة المعالجة المركزية.
كيفية استخدام
كما هو موضح أدناه ، قم بإنشاء محدد وتسجيل قناة.
ملاحظة: لتسجيل القناة على المحدد ، يجب أولاً تعيين القناة على وضع عدم الحظر ، وإلا سيتم طرح استثناء.
Selector Selector = celector.open () ؛ Channel.ConfigureBlocking (false) ؛ SelecteKey Key = Channel.register (Selector ، SelecteKey.op_Read) ؛
تسمى المعلمة الثانية من طريقة التسجيل () "مجموعة الفائدة" ، وهي مجموعة الحدث التي تشعر بالقلق بشأنها. إذا كنت تهتم بأحداث متعددة ، فافصلها بـ "bital أو المشغل" ، على سبيل المثال
selectekey.op_read | selectekey.op_write
طريقة الكتابة هذه ليست غير مألوفة لها. يتم لعبها في لغات البرمجة التي تدعم عمليات البت. يمكن باستخدام متغير عدد صحيح تحديد حالات متعددة. كيف يتم ذلك؟ إنه في الواقع بسيط للغاية. على سبيل المثال ، حددت بعض الثوابت مسبقًا وقيمها (ثنائية) كما يلي
يمكن العثور على أن البتات ذات قيمتها 1 متداخلة ، وبالتالي فإن القيم التي تم الحصول عليها بعد إجراء bitwise أو الحسابات عليها ليس لها أي غموض ، ويمكن استنتاجها عن عكس المتغيرات التي يتم حسابها من. كيف تحكم ، نعم ، إنها عملية "البتات و". على سبيل المثال ، هناك الآن قيمة متغيرة مجموعة الحالة 0011. نحتاج فقط إلى تحديد ما إذا كانت قيمة "0011 & OP_READ" هي 1 أو 0 لتحديد ما إذا كانت المجموعة تحتوي على حالة OP_READ.
بعد ذلك ، لاحظ أن طريقة التسجيل () تُرجع كائن مفتاح التحديد ، والذي يحتوي على معلومات لهذا التسجيل ، ويمكننا أيضًا تعديل معلومات التسجيل من خلاله. من المثال الكامل أدناه ، يمكننا أن نرى أنه بعد Select () ، نحصل أيضًا على القنوات مع الحالة جاهزة عن طريق الحصول على مجموعة من مفاتيح التحديد.
مثال كامل
تم شرح المفاهيم والأشياء النظرية (في الواقع ، بعد كتابتها هنا ، وجدت أنني لم أكتب الكثير ، وهو أمر محرج للغاية (⊙ˍ⊙)). دعونا نلقي نظرة على مثال كامل.
يستخدم هذا المثال Javanio لتنفيذ خادم واحد. الوظيفة بسيطة جدا. يستمع إلى اتصال العميل. عند إنشاء الاتصال ، يقرأ رسالة العميل ويستجيب لرسالة إلى العميل.
تجدر الإشارة إلى أنني أستخدم الحرف "/0" (بايت بقيمة 0) لتحديد نهاية الرسالة.
خادم واحد الخيوط
الطبقة العامة nioserver {public static void main (string [] args) يلقي ioException {// إنشاء محدد محدد = celector.open () ؛ // تهيئة TCP Connection Channel Cannel ServersOcketchannel listenchannel = serversocketchannel.open () ؛ ListenChannel.bind (New InetSocketaddress (9999)) ؛ listenchannel.configureBlocking (false) ؛ // registrict to Selector (استمع إلى قبول الحدث) listenchannel.register (selector ، selectekey.op_accept) ؛ // إنشاء مخزن مؤقت bytebuffer العازلة = bytebuffer.allocate (100) ؛ بينما (صواب) {celector.select () ؛ // block حتى يتم الاستماع إلى الحدث ليحدث ITerator <SelecteKey> keyiter = selector.selectedkeys (). iterator () ؛ // الوصول إلى حدث القناة المحددة من خلال iterator بدوره في حين (keyiter.hasnext ()) = (((serversocketchannel) key.channel ()). قبول () buffer.clear () ؛ // اقرأ حتى نهاية الدفق ، مما يشير إلى أنه تم فصل اتصال TCP ، // لذلك ، من الضروري إغلاق القناة أو إلغاء الاستماع إلى الحدث القراءة // وإلا ، فسيتم حلها بلا حدود إذا ((socketchannel) key.channel ()). قراءة (مخزن) == -1) {key.channel () buffer.flip () ؛ بينما (buffer.hasRemaining ()) {byte b = buffer.get () ؛ if (b == 0) {// /0system.out.println () في نهاية رسالة العميل ؛ // استجابة العميل buffer.clear () ؛ buffer.put ("hello ، client!/0". {((socketchannel) key.Channel ()). الكتابة (العازلة) ؛}} آخر {system.out.print ((char) b) ؛}}} // للأحداث التي تمت معالجتها ، يجب عليك إزالة keyiter.remove () ؛}}} يدويًا ،عميل
هذا العميل يستخدم بحتة للاختبار. من أجل جعل الأمر أقل صعوبة ، فإنه يستخدم أساليب الكتابة التقليدية ، والرمز قصير جدًا.
إذا كنت بحاجة إلى أن تكون أكثر صرامة في الاختبار ، فيجب عليك تشغيل عدد كبير من العملاء بشكل متزامن لحساب وقت الاستجابة للخادم ، ولا ترسل البيانات فورًا بعد إنشاء الاتصال ، وذلك لتوفير اللعب الكامل لمزايا I/O غير المحظورة على الخادم.
عميل الفئة العامة {public static void main (string [] args) يرمي الاستثناء {socket socket = new Socket ("localhost" ، 9999) ؛ inputStream هو = socket.getInputStream () ؛ OutputStream os = socket.getoutputstream () ؛ // إرسال البيانات إلى الخادم أولاً ("Hello ، Server!/0". بينما ((b = iS.Read ())! = 0) {system.out.print ((char) b) ؛} system.out.println () ؛ socket.close () ؛}}لخص
ما سبق هو كل شيء عن الفهم السريع لمكونات NIO الأساسية في Java. آمل أن يكون ذلك مفيدًا للجميع. يمكن للأصدقاء المهتمين الاستمرار في الرجوع إلى محتويات أخرى ذات صلة بهذا الموقع. إذا كانت هناك أي أوجه قصور ، فيرجى ترك رسالة لإشارةها. شكرا لك يا أصدقائك لدعمكم لهذا الموقع!