توفر Java NIO طريقة مختلفة لعمل الإدخال/الإخراج عن الإدخال/الإخراج القياسي:
القنوات والمخازن المؤقتة: يعمل الإدخال/الإخراج القياسي على أساس تدفقات البايتات وتدفقات الأحرف، بينما يعمل NIO على أساس القنوات (القناة) والمخازن المؤقتة (المخزن المؤقت). تتم قراءة البيانات دائمًا من القناة إلى منطقة المخزن المؤقت، أو كتابتها من المخزن المؤقت قناة.
الإدخال/الإخراج غير المتزامن: يتيح لك Java NIO استخدام الإدخال/الإخراج بشكل غير متزامن. على سبيل المثال، عندما يقرأ مؤشر ترابط البيانات من قناة إلى مخزن مؤقت، لا يزال بإمكان مؤشر الترابط القيام بأشياء أخرى. عند كتابة البيانات إلى المخزن المؤقت، يمكن للخيط متابعة معالجتها. الكتابة إلى قناة من المخزن المؤقت مشابه.
المحددات: تقدم Java NIO مفهوم المحددات، والتي تُستخدم للاستماع إلى الأحداث على قنوات متعددة (على سبيل المثال: فتح الاتصال، وصول البيانات). لذلك، يمكن لخيط واحد الاستماع إلى قنوات بيانات متعددة.
دعنا نقدم المعرفة ذات الصلة بـ Java NIO بالتفصيل.
نظرة عامة على جافا NIO
يتكون Java NIO من الأجزاء الأساسية التالية:
القنوات
المخازن المؤقتة
محددات
على الرغم من وجود العديد من الفئات والمكونات الأخرى في Java NIO، في رأيي، تشكل القناة والمخزن المؤقت والمحدد واجهة برمجة التطبيقات الأساسية. المكونات الأخرى، مثل Pipe وFileLock، هي مجرد فئات أدوات مساعدة تستخدم مع المكونات الأساسية الثلاثة. ولذلك، في هذه النظرة العامة سوف أركز على هذه المكونات الثلاثة. يتم تناول المكونات الأخرى في فصول منفصلة.
القناة والمخزن المؤقت
في الأساس، كل عمليات الإدخال والإخراج في NIO تبدأ من قناة. القنوات تشبه إلى حد ما التدفقات. يمكن قراءة البيانات من القناة إلى المخزن المؤقت، أو كتابتها من المخزن المؤقت إلى القناة. هنا مثال توضيحي:
هناك عدة أنواع من القنوات والمخازن المؤقتة. فيما يلي تطبيقات بعض القنوات الرئيسية في JAVA NIO:
قناة الملف
قناة مخطط البيانات
قناة المقبس
ServerSocketChannel
كما ترى، تغطي هذه القنوات عمليات الإدخال والإخراج لشبكة UDP وTCP، بالإضافة إلى عمليات الإدخال والإخراج للملفات.
إلى جانب هذه الفئات، هناك بعض الواجهات المثيرة للاهتمام، ولكن من أجل البساطة حاولت عدم ذكرها في النظرة العامة. سأشرحها في فصول أخرى من هذا البرنامج التعليمي حيثما كانت ذات صلة.
فيما يلي تطبيق Buffer الرئيسي في Java NIO:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
تغطي هذه المخازن المؤقتة أنواع البيانات الأساسية التي يمكنك إرسالها عبر الإدخال/الإخراج: byte، وshort، وint، وlong، وfloat، وdouble، وchar.
يحتوي Java NIO أيضًا على Mappyteuffer، والذي يُستخدم لتمثيل الملفات المعينة للذاكرة، ولن أشرح ذلك في النظرة العامة.
محدد
يسمح المحدد لخيط واحد بالتعامل مع قنوات متعددة. إذا فتح تطبيقك اتصالات (قنوات) متعددة، ولكن حركة مرور كل اتصال منخفضة جدًا، فقد يكون استخدام المحدد أمرًا مناسبًا. على سبيل المثال، في خادم الدردشة.
هذا مثال توضيحي لاستخدام المحدد لمعالجة 3 قنوات في موضوع واحد:
لاستخدام محدد، يجب عليك تسجيل قناة باستخدام المحدد ثم استدعاء أسلوب التحديد () الخاص به. سيتم حظر هذه الطريقة حتى يكون لدى القناة المسجلة حدث جاهز. بمجرد عودة هذه الطريقة، يمكن للخيط التعامل مع هذه الأحداث، ومن أمثلة الأحداث الاتصالات الجديدة الواردة، واستقبال البيانات، وما إلى ذلك.
جافا NIO مقابل IO
(العنوان الأصلي لهذا الجزء، المؤلف: جاكوب جينكوف، المترجم: غوه لي، المصحح اللغوي: فانغ تنغفي)
بعد التعرف على Java NIO وIO API، تبادر إلى ذهني سؤال على الفور:
يقتبس
متى يجب أن أستخدم IO ومتى يجب أن أستخدم NIO؟ في هذه المقالة، سأحاول أن أشرح بوضوح الاختلافات بين Java NIO وIO، وسيناريوهات استخدامها، وكيفية تأثيرها على تصميم التعليمات البرمجية الخاصة بك.
الاختلافات الرئيسية بين Java NIO وIO
يلخص الجدول التالي الاختلافات الرئيسية بين Java NIO وIO وسأصف الاختلافات في كل جزء من الجدول بمزيد من التفصيل.
ايو نيو
تيار موجه نحو المخزن المؤقت
حظر الإدخال والإخراج عدم حظر الإدخال والإخراج
محددات
موجه نحو الدفق وموجه نحو المخزن المؤقت
أول فرق كبير بين Java NIO وIO هو أن IO موجه نحو التدفق بينما NIO موجه نحو المخزن المؤقت. Java IO موجه نحو الدفق مما يعني أنه تتم قراءة بايت واحد أو أكثر من الدفق في المرة الواحدة، وحتى تتم قراءة جميع البايتات، لا يتم تخزينها مؤقتًا في أي مكان. بالإضافة إلى ذلك، لا يمكن نقل البيانات في الدفق للأمام أو للخلف. إذا كنت بحاجة إلى نقل البيانات المقروءة من الدفق ذهابًا وإيابًا، فستحتاج إلى تخزينها مؤقتًا في المخزن المؤقت أولاً. يختلف نهج Java NIO الموجه نحو المخزن المؤقت قليلاً. تتم قراءة البيانات في المخزن المؤقت الذي تتم معالجته لاحقًا، مع التحرك ذهابًا وإيابًا في المخزن المؤقت حسب الحاجة. وهذا يزيد من المرونة في المعالجة. ومع ذلك، تحتاج أيضًا إلى التحقق من أن المخزن المؤقت يحتوي على كافة البيانات التي تحتاج إلى معالجتها. تأكد أيضًا من أنه عند قراءة المزيد من البيانات في المخزن المؤقت، لا تتم الكتابة فوق البيانات غير المعالجة في المخزن المؤقت.
حظر وغير حظر IO
يتم حظر التدفقات المختلفة لـ Java IO. هذا يعني أنه عندما يستدعي خيط القراءة () أو الكتابة ()، يتم حظر الخيط حتى تتم قراءة بعض البيانات، أو تتم كتابة البيانات بالكامل. لا يمكن للخيط أن يفعل أي شيء آخر خلال هذه الفترة. يسمح وضع عدم الحظر في Java NIO للخيط بإرسال طلب لقراءة البيانات من قناة معينة، لكنه يمكنه فقط الحصول على البيانات المتاحة حاليًا. إذا لم تكن هناك بيانات متاحة حاليًا، فلن يتم الحصول على أي شيء. بدلاً من إبقاء الخيط محظورًا، يمكن للخيط الاستمرار في القيام بأشياء أخرى حتى تصبح البيانات قابلة للقراءة. الشيء نفسه ينطبق على الكتابة غير المحظورة. يطلب الخيط كتابة بعض البيانات إلى القناة، لكنه لا يحتاج إلى الانتظار حتى تتم كتابتها بالكامل. يمكن للخيط القيام بأشياء أخرى في هذه الأثناء. تستخدم الخيوط عادةً وقت الخمول في عمليات الإدخال والإخراج غير المحظورة لإجراء عمليات الإدخال والإخراج على قنوات أخرى، لذلك يمكن لخيط واحد الآن إدارة قنوات إدخال وإخراج متعددة.
محددات
تسمح محددات Java NIO لخيط واحد بمراقبة قنوات إدخال متعددة. يمكنك تسجيل قنوات متعددة باستخدام محدد، ثم استخدام مؤشر ترابط منفصل "لتحديد" القنوات: تحتوي هذه القنوات بالفعل على مدخلات يمكن معالجتها أو تحديد قناة جاهز للكتابة. تعمل آلية التحديد هذه على تسهيل إدارة سلسلة رسائل واحدة لقنوات متعددة.
كيف يؤثر NIO وIO على تصميم التطبيق
سواء اخترت صندوق أدوات الإدخال/الإخراج أو NIO، فهناك العديد من الجوانب التي قد تؤثر على تصميم التطبيق الخاص بك:
مكالمات API إلى فئات NIO أو IO.
معالجة البيانات.
عدد الخيوط المستخدمة لمعالجة البيانات.
استدعاء واجهة برمجة التطبيقات
بالطبع، تبدو مكالمات واجهة برمجة التطبيقات (API) عند استخدام NIO مختلفة عنها عند استخدام IO، ولكن هذا ليس غير متوقع لأنه بدلاً من مجرد القراءة من InputStream بايت بايت، يجب أولاً قراءة البيانات في المخزن المؤقت ثم معالجتها.
معالجة البيانات
باستخدام تصميم NIO النقي مقارنة بتصميم IO، تتأثر معالجة البيانات أيضًا.
في تصميم الإدخال والإخراج، نقرأ البيانات بايتًا بايت من InputStream أو Reader. لنفترض أنك تقوم بمعالجة دفق بيانات نصي قائم على سطر، على سبيل المثال:
انسخ رمز الكود كما يلي:
الاسم: آنا
العمر: 25
البريد الإلكتروني: [email protected]
الهاتف: 1234567890
يمكن التعامل مع تدفق أسطر النص على النحو التالي:
انسخ رمز الكود كما يلي:
InputStream input = …; // احصل على InputStream من مقبس العميل
BufferedReader Reader = new BufferedReader(new InputStreamReader(input));
String nameLine = Reader.readLine();
String ageLine = Reader.readLine();
String emailLine= Reader.readLine();
String phoneLine= Reader.readLine();
لاحظ أن حالة المعالجة يتم تحديدها حسب مدة تنفيذ البرنامج. بمعنى آخر، بمجرد عودة التابع readline()، ستعرف على وجه اليقين أن سطر النص قد تمت قراءته، ولهذا السبب يحظر readline() حتى تتم قراءة السطر بأكمله. وتعلم أيضًا أن هذا السطر يحتوي على أسماء؛ وبالمثل، عندما يعود استدعاء readline() الثاني، فإنك تعلم أن هذا السطر يحتوي على أعمار، وما إلى ذلك. كما ترى، يعمل هذا المعالج فقط عند قراءة البيانات الجديدة، ويعرف البيانات الموجودة في كل خطوة. بمجرد أن يقوم الخيط قيد التشغيل بمعالجة بعض البيانات التي قرأها، فإنه لن يستعيد البيانات (في الغالب). ويوضح الشكل التالي هذا المبدأ أيضًا:
قراءة البيانات من دفق محظور
على الرغم من أن تنفيذ NIO سيكون مختلفًا، إليك مثال بسيط:
انسخ رمز الكود كما يلي:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
لاحظ السطر الثاني، وهو قراءة البايتات من القناة إلى ByteBuffer. عندما يعود استدعاء هذا الأسلوب، لا تعرف ما إذا كانت كافة البيانات التي تحتاجها موجودة في المخزن المؤقت. كل ما تعرفه هو أن المخزن المؤقت يحتوي على بعض البايتات، مما يجعل المعالجة صعبة بعض الشيء.
لنفترض أنه بعد استدعاء القراءة (المخزن المؤقت) الأول، تكون البيانات المقروءة في المخزن المؤقت نصف سطر فقط، على سبيل المثال، "الاسم: An"، هل يمكنك معالجة البيانات؟ من الواضح أنه لا، فأنت بحاجة إلى الانتظار حتى تتم قراءة صف البيانات بالكامل في ذاكرة التخزين المؤقت، وقبل ذلك، فإن أي معالجة للبيانات لا معنى لها.
إذًا، كيف يمكنك معرفة ما إذا كان المخزن المؤقت يحتوي على بيانات كافية للمعالجة؟ حسنا، أنت لا تعرف. يمكن للطرق المكتشفة عرض البيانات الموجودة في المخزن المؤقت فقط. والنتيجة هي أنه يتعين عليك التحقق من بيانات المخزن المؤقت عدة مرات قبل أن تعرف أن جميع البيانات موجودة في المخزن المؤقت. ليس هذا غير فعال فحسب، بل قد يؤدي أيضًا إلى فوضى في حل البرمجة. على سبيل المثال:
انسخ رمز الكود كما يلي:
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
بينما (! bufferFull(bytesRead)) {
bytesRead = inChannel.read(buffer);
}
يجب أن تتبع طريقة bufferFull() مقدار البيانات التي تمت قراءتها في المخزن المؤقت وإرجاع صواب أو خطأ، اعتمادًا على ما إذا كان المخزن المؤقت ممتلئًا. بمعنى آخر، إذا كان المخزن المؤقت جاهزًا للمعالجة، فهذا يعني أن المخزن المؤقت ممتلئ.
يقوم الأسلوب bufferFull() بفحص المخزن المؤقت، لكن يجب أن يظل في نفس الحالة التي كانت عليها قبل استدعاء الأسلوب bufferFull(). إذا لم يكن الأمر كذلك، فقد لا تتم قراءة البيانات التالية المقروءة في المخزن المؤقت إلى الموقع الصحيح. وهذا مستحيل، ولكن هذه مسألة أخرى يجب أن تكون على علم بها.
إذا كان المخزن المؤقت ممتلئًا، فيمكن معالجته. إذا لم ينجح الأمر، وكان منطقيًا في حالتك الفعلية، فقد تتمكن من التعامل مع بعض منه. ولكن في كثير من الحالات ليس هذا هو الحال. يوضح الشكل التالي "دورة بيانات المخزن المؤقت جاهزة":
قراءة البيانات من القناة حتى تتم قراءة كافة البيانات في المخزن المؤقت
تلخيص
يتيح لك NIO إدارة قنوات متعددة (اتصالات الشبكة أو الملفات) باستخدام مؤشر ترابط واحد فقط (أو عدد قليل)، ولكن المفاضلة هي أن تحليل البيانات يمكن أن يكون أكثر تعقيدًا من قراءتها من تدفق محظور.
إذا كنت بحاجة إلى إدارة آلاف الاتصالات المفتوحة في وقت واحد والتي ترسل فقط كميات صغيرة من البيانات في كل مرة، مثل خادم الدردشة، فقد يكون الخادم الذي ينفذ NIO ميزة. وبالمثل، إذا كنت بحاجة إلى الاحتفاظ بالعديد من الاتصالات المفتوحة بأجهزة كمبيوتر أخرى، كما هو الحال في شبكة P2P، فقد يكون من المفيد استخدام سلسلة منفصلة لإدارة جميع اتصالاتك الصادرة. يظهر الشكل أدناه مخطط تصميم الاتصالات المتعددة في خيط واحد:
يدير الخيط الواحد اتصالات متعددة
إذا كان لديك عدد صغير من الاتصالات التي تستخدم نطاقًا تردديًا عاليًا جدًا، وترسل كميات كبيرة من البيانات مرة واحدة، فربما يكون تنفيذ خادم الإدخال والإخراج النموذجي مناسبًا. يوضح الشكل التالي تصميمًا نموذجيًا لخادم الإدخال والإخراج:
تصميم خادم IO نموذجي:
تتم معالجة الاتصال بواسطة مؤشر ترابط
قناة
قنوات Java NIO تشبه التدفقات، ولكنها مختلفة بعض الشيء:
يمكن قراءة البيانات من القناة ويمكن كتابة البيانات على القناة. لكن تيارات القراءة والكتابة عادة ما تكون في اتجاه واحد.
يمكن قراءة القنوات وكتابتها بشكل غير متزامن.
يجب أولاً قراءة البيانات الموجودة في القناة من المخزن المؤقت، أو كتابتها دائمًا من المخزن المؤقت.
كما ذكر أعلاه، تتم قراءة البيانات من القناة إلى المخزن المؤقت وتتم كتابة البيانات من المخزن المؤقت إلى القناة. كما هو موضح أدناه:
تنفيذ القناة
هذه هي تطبيقات أهم القنوات في Java NIO:
FileChannel: قراءة وكتابة البيانات من الملفات.
DatagramChannel: يمكنه قراءة وكتابة البيانات في الشبكة من خلال UDP.
المقبس: يمكنه قراءة وكتابة البيانات في الشبكة من خلال TCP.
ServerSocketChannel: يمكنه مراقبة اتصالات TCP الواردة، مثل خادم الويب. يتم إنشاء قناة المقبس لكل اتصال وارد جديد.
مثال القناة الأساسية
فيما يلي مثال على استخدام FileChannel لقراءة البيانات في المخزن المؤقت:
انسخ رمز الكود كما يلي:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
بينما (بايت ريد! = -1) {
System.out.println("قراءة" + bytesRead);
buf.flip();
بينما(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.Close();
لاحظ أن استدعاء buf.flip() يقرأ أولاً البيانات الموجودة في المخزن المؤقت، ثم يعكس المخزن المؤقت، ثم يقرأ البيانات من المخزن المؤقت. سيتطرق القسم التالي إلى مزيد من التفاصيل حول Buffer.
المخزن المؤقت
يتم استخدام المخزن المؤقت في Java NIO للتفاعل مع قنوات NIO. كما تعلم، تتم قراءة البيانات من القناة إلى المخزن المؤقت وكتابتها من المخزن المؤقت إلى القناة.
المخزن المؤقت هو في الأساس كتلة من الذاكرة يمكن كتابة البيانات إليها ومن ثم يمكن قراءة البيانات منها. يتم تجميع هذه الذاكرة ككائن NIO Buffer وتوفر مجموعة من الطرق للوصول بسهولة إلى هذه الذاكرة.
الاستخدام الأساسي للمخزن المؤقت
عادةً ما يتبع استخدام Buffer لقراءة البيانات وكتابتها الخطوات الأربع التالية:
كتابة البيانات إلى المخزن المؤقت
استدعاء طريقة الوجه ().
قراءة البيانات من المخزن المؤقت
اتصل بالطريقة الواضحة () أو الطريقة المضغوطة ().
عند كتابة البيانات إلى المخزن المؤقت، يسجل المخزن المؤقت مقدار البيانات التي تمت كتابتها. بمجرد رغبتك في قراءة البيانات، ستحتاج إلى تبديل المخزن المؤقت من وضع الكتابة إلى وضع القراءة من خلال طريقة flip(). في وضع القراءة، يمكن قراءة جميع البيانات المكتوبة مسبقًا في المخزن المؤقت.
بمجرد قراءة كافة البيانات، يجب مسح المخزن المؤقت بحيث يمكن الكتابة إليه مرة أخرى. هناك طريقتان لمسح المخزن المؤقت: استدعاء الطريقة الواضحة () أو المضغوطة (). تقوم الطريقة الواضحة () بمسح المخزن المؤقت بالكامل. ستقوم الطريقة المضغوطة () بمسح البيانات التي تمت قراءتها فقط. يتم نقل أي بيانات غير مقروءة إلى بداية المخزن المؤقت، ويتم وضع البيانات المكتوبة حديثًا بعد البيانات غير المقروءة في المخزن المؤقت.
فيما يلي مثال لاستخدام Buffer:
انسخ رمز الكود كما يلي:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
// إنشاء مخزن مؤقت بسعة 48 بايت
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
بينما (بايت ريد! = -1) {
buf.flip();//جعل المخزن المؤقت جاهزًا للقراءة
بينما(buf.hasRemaining()){
System.out.print((char) buf.get()); // قراءة 1 بايت في المرة الواحدة
}
buf.clear(); // جعل المخزن المؤقت جاهزًا للكتابة
bytesRead = inChannel.read(buf);
}
aFile.Close();
سعة المخزن المؤقت وموقعه وحدوده
المخزن المؤقت هو في الأساس كتلة من الذاكرة يمكن كتابة البيانات إليها ومن ثم يمكن قراءة البيانات منها. يتم تجميع هذه الذاكرة ككائن NIO Buffer وتوفر مجموعة من الطرق للوصول بسهولة إلى هذه الذاكرة.
لكي تفهم كيفية عمل Buffer، عليك أن تكون على دراية بخصائصه الثلاث:
سعة
موضع
حد
يعتمد معنى الموضع والحد على ما إذا كان المخزن المؤقت في وضع القراءة أو وضع الكتابة. بغض النظر عن الوضع الذي يوجد فيه المخزن المؤقت، فإن معنى السعة هو نفسه دائمًا.
فيما يلي شرح للسعة والموضع والحد في وضع القراءة والكتابة، مع شرح تفصيلي يتبع الرسم التوضيحي.
سعة
باعتباره كتلة ذاكرة، يمتلك Buffer قيمة ثابتة الحجم، تسمى أيضًا "السعة". يمكنك فقط كتابة سعة البايت والطول والشار والأنواع الأخرى. بمجرد امتلاء المخزن المؤقت، يجب إفراغه (عن طريق قراءة البيانات أو مسح البيانات) قبل مواصلة كتابة البيانات.
موضع
عند كتابة البيانات إلى المخزن المؤقت، يمثل الموضع الموضع الحالي. قيمة الموضع الأولية هي 0. عند كتابة بايت أو بيانات طويلة وما إلى ذلك إلى المخزن المؤقت، سينتقل الموضع للأمام إلى وحدة المخزن المؤقت التالية حيث يمكن إدراج البيانات. الحد الأقصى للموضع يمكن أن يكون سعة 1.
عند قراءة البيانات، تتم قراءتها أيضًا من موقع محدد. عند تحويل المخزن المؤقت من وضع الكتابة إلى وضع القراءة، سيتم إعادة تعيين الموضع إلى 0. عند قراءة البيانات من موضع المخزن المؤقت، يتحرك الموضع للأمام إلى الموضع التالي القابل للقراءة.
حد
في وضع الكتابة، يشير حد المخزن المؤقت إلى الحد الأقصى لكمية البيانات التي يمكنك كتابتها في المخزن المؤقت. في وضع الكتابة، الحد يساوي سعة المخزن المؤقت.
عند تحويل المخزن المؤقت إلى وضع القراءة، يشير الحد إلى الحد الأقصى لكمية البيانات التي يمكنك قراءتها. لذلك، عند تحويل المخزن المؤقت إلى وضع القراءة، سيتم تعيين الحد على قيمة الموضع في وضع الكتابة. بمعنى آخر، يمكنك قراءة جميع البيانات المكتوبة من قبل (يتم تعيين الحد لعدد البيانات المكتوبة، ويتم وضع هذه القيمة في وضع الكتابة)
نوع المخزن المؤقت
يحتوي Java NIO على أنواع المخزن المؤقت التالية:
ByteBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
كما ترون، تمثل أنواع المخزن المؤقت هذه أنواعًا مختلفة من البيانات. بمعنى آخر، يمكن معالجة البايتات الموجودة في المخزن المؤقت من خلال الأنواع char أو short أو int أو long أو float أو double.
يعد MappedByteBuffer خاصًا بعض الشيء وسيتم مناقشته في الفصل الخاص به.
تخصيص المخزن المؤقت
للحصول على كائن Buffer، يجب عليك أولاً تخصيصه. كل فئة Buffer لديها طريقة تخصيص. فيما يلي مثال لتخصيص ByteBuffer بسعة 48 بايت.
انسخ رمز الكود كما يلي:
ByteBuffer buf = ByteBuffer.allocate(48);
يؤدي هذا إلى تخصيص CharBuffer الذي يمكنه تخزين 1024 حرفًا:
انسخ رمز الكود كما يلي:
CharBuffer buf = CharBuffer.allocate(1024);
كتابة البيانات إلى المخزن المؤقت
هناك طريقتان لكتابة البيانات إلى Buffer:
الكتابة من القناة إلى المخزن المؤقت.
اكتب إلى Buffer من خلال طريقة put() الخاصة بـ Buffer.
مثال للكتابة من القناة إلى المخزن المؤقت
انسخ رمز الكود كما يلي:
int bytesRead = inChannel.read(buf);
مثال على كتابة المخزن المؤقت من خلال طريقة الوضع:
انسخ رمز الكود كما يلي:
buf.put(127);
هناك العديد من الإصدارات من طريقة put، مما يسمح لك بكتابة البيانات إلى المخزن المؤقت بطرق مختلفة. على سبيل المثال، الكتابة إلى موقع محدد، أو كتابة مصفوفة بايت إلى مخزن مؤقت. لمزيد من التفاصيل حول تنفيذ Buffer، راجع JavaDoc.
طريقة الوجه ().
تقوم طريقة الوجه بتحويل المخزن المؤقت من وضع الكتابة إلى وضع القراءة. سيؤدي استدعاء طريقة flip () إلى إعادة الموضع إلى 0 وتعيين الحد إلى قيمة الموضع السابق.
بمعنى آخر، يُستخدم الموضع الآن لتحديد موضع القراءة، ويمثل الحد عدد البايتات والأحرف وما إلى ذلك التي تمت كتابتها من قبل - كم عدد البايتات والأحرف وما إلى ذلك التي يمكن قراءتها الآن.
قراءة البيانات من المخزن المؤقت
هناك طريقتان لقراءة البيانات من Buffer:
قراءة البيانات من المخزن المؤقت إلى القناة.
استخدم طريقة get() لقراءة البيانات من المخزن المؤقت.
مثال لقراءة البيانات من المخزن المؤقت إلى القناة:
انسخ رمز الكود كما يلي:
// القراءة من المخزن المؤقت إلى القناة.
int bytesWritten = inChannel.write(buf);
مثال على استخدام طريقة get() لقراءة البيانات من Buffer
انسخ رمز الكود كما يلي:
بايت aByte = buf.get();
هناك العديد من الإصدارات من طريقة get، مما يسمح لك بقراءة البيانات من المخزن المؤقت بطرق مختلفة. على سبيل المثال، القراءة من موضع محدد، أو قراءة البيانات من المخزن المؤقت إلى مصفوفة بايت. لمزيد من التفاصيل حول تنفيذ Buffer، راجع JavaDoc.
طريقة الترجيع ().
يعيد التابع Buffer.rewind() الموضع إلى 0، حتى تتمكن من إعادة قراءة جميع البيانات الموجودة في المخزن المؤقت. يبقى الحد دون تغيير ولا يزال يشير إلى عدد العناصر (البايت، الحرف، وما إلى ذلك) التي يمكن قراءتها من المخزن المؤقت.
طرق واضحة () وصغيرة الحجم ().
بمجرد قراءة البيانات الموجودة في المخزن المؤقت، يجب أن يكون المخزن المؤقت جاهزًا للكتابة عليه مرة أخرى. يمكن القيام بذلك عبر التابعين Clear() أو Compact().
إذا تم استدعاء الأسلوب Clear()، فسيتم تعيين الموضع مرة أخرى إلى 0 وسيتم تعيين الحد على قيمة السعة. وبعبارة أخرى، يتم مسح المخزن المؤقت. لا يتم مسح البيانات الموجودة في المخزن المؤقت، ولكن هذه العلامات تخبرنا من أين نبدأ في كتابة البيانات في المخزن المؤقت.
إذا كانت هناك بعض البيانات غير المقروءة في المخزن المؤقت وقمت باستدعاء الطريقة Clear()، فسيتم "نسيان" البيانات، مما يعني أنه لن تكون هناك أي علامات لإخبارك بالبيانات التي تمت قراءتها وتلك التي لم تتم قراءتها.
إذا كانت لا تزال هناك بيانات غير مقروءة في المخزن المؤقت وكانت البيانات مطلوبة لاحقًا، ولكنك تريد كتابة بعض البيانات أولاً، فاستخدم الطريقة المضغوطة ().
تقوم الطريقة Compact() بنسخ جميع البيانات غير المقروءة إلى بداية المخزن المؤقت. ثم قم بتعيين الموضع خلف آخر عنصر غير مقروء مباشرةً. لا تزال سمة الحد مضبوطة على السعة مثل طريقة Clear(). المخزن المؤقت جاهز الآن لكتابة البيانات، ولكن لن تتم الكتابة فوق البيانات غير المقروءة.
طرق مارك () وإعادة تعيين ().
من خلال استدعاء الأسلوب Buffer.mark()، يمكنك تحديد موضع معين في Buffer. يمكنك لاحقًا استعادة هذا الوضع عن طريق استدعاء التابع Buffer.reset() . على سبيل المثال:
انسخ رمز الكود كما يلي:
buffer.mark();
// اتصل بـ buffer.get() عدة مرات، على سبيل المثال أثناء التحليل.
buffer.reset();// اضبط الموضع مرة أخرى لوضع علامة عليه.
يساوي () وطرق المقارنة ().
يمكنك استخدام أساليب يساوي () ومقارنة () لمخزنين مؤقتين.
يساوي ()
عند استيفاء الشروط التالية، فهذا يعني أن المخزنين متساويان:
لديك نفس النوع (بايت، شار، إنت، وما إلى ذلك).
عدد البايتات والأحرف وما إلى ذلك المتبقية في المخزن المؤقت متساوي.
جميع وحدات البايت والأحرف وما إلى ذلك المتبقية في المخزن المؤقت هي نفسها.
كما ترون، يساوي يقارن فقط جزءًا من المخزن المؤقت، وليس كل عنصر فيه. في الواقع، فهو يقارن فقط العناصر المتبقية في المخزن المؤقت.
طريقة المقارنة ().
يقارن الأسلوب CompareTo() العناصر المتبقية (البايت، والحرف، وما إلى ذلك) لمخزنين مؤقتين إذا تم استيفاء الشروط التالية، فسيتم اعتبار أحد المخزنين المؤقتين "أقل من" المخزن المؤقت الآخر:
العنصر الأول غير المتكافئ أصغر من العنصر المقابل في المخزن المؤقت الآخر.
جميع العناصر متساوية، لكن المخزن المؤقت الأول يتم استنفاده قبل الآخر (المخزن المؤقت الأول يحتوي على عناصر أقل من الآخر).
(تعليق: العناصر المتبقية هي العناصر من الموضع إلى الحد)
مبعثر / جمع
(العنوان الأصلي لهذا الجزء، المؤلف: جاكوب جينكوف، المترجم: غوه لي)
يبدأ Java NIO في دعم التشتت/التجميع، ويتم استخدام التشتت/التجميع لوصف عملية القراءة من القناة أو الكتابة إليها (ملاحظة المترجم: غالبًا ما تُترجم القناة على أنها قناة باللغة الصينية).
القراءة المبعثرة من القناة تعني كتابة بيانات القراءة في مخازن مؤقتة متعددة أثناء عملية القراءة. لذلك، تقوم القناة "بتوزيع" البيانات المقروءة من القناة إلى مخازن مؤقتة متعددة.
يعني التجميع والكتابة في قناة ما كتابة البيانات من مخازن مؤقتة متعددة إلى نفس القناة أثناء عملية الكتابة، لذلك، تقوم القناة "بتجميع" البيانات في مخازن مؤقتة متعددة وإرسالها إلى القناة.
يتم استخدام التبعثر/التجميع غالبًا في المواقف التي تحتاج فيها البيانات المرسلة إلى المعالجة بشكل منفصل. على سبيل المثال، عند إرسال رسالة تتكون من رأس رسالة ونص الرسالة، يمكنك تشتيت نص الرسالة ورأسها في مخازن مؤقتة مختلفة. يمكنك بسهولة معالجة رؤوس الرسائل ونصوص الرسائل.
قراءات متناثرة
تشير قراءات التشتت إلى قراءة البيانات من قناة واحدة إلى مخازن مؤقتة متعددة. كما هو موضح في الشكل أدناه:
مثال الكود كما يلي:
انسخ رمز الكود كما يلي:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
لاحظ أنه يتم أولاً إدراج المخزن المؤقت في المصفوفة، ثم يتم استخدام المصفوفة كمعلمة إدخال إلىchannel.read(). تكتب طريقة القراءة () البيانات المقروءة من القناة إلى المخزن المؤقت بترتيب المخزن المؤقت في المصفوفة. عندما يمتلئ مخزن مؤقت واحد، تكتب القناة إلى مخزن مؤقت آخر.
يجب أن تملأ القراءات المتناثرة المخزن المؤقت الحالي قبل الانتقال إلى المخزن المؤقت التالي، وهو ما يعني أيضًا أنها غير مناسبة للرسائل الديناميكية (ملاحظة المترجم: حجم الرسالة غير ثابت). بمعنى آخر، إذا كان هناك رأس رسالة ونص الرسالة، يجب ملء رأس الرسالة بالكامل (على سبيل المثال، 128 بايت) لكي تعمل ميزة "القراءات المبعثرة" بشكل صحيح.
جمع يكتب
تجميع عمليات الكتابة يعني أن البيانات تتم كتابتها من مخازن مؤقتة متعددة إلى نفس القناة. كما هو موضح في الشكل أدناه:
مثال الكود كما يلي:
انسخ رمز الكود كما يلي:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
// كتابة البيانات في المخازن المؤقتة
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
مصفوفة المخازن المؤقتة هي معلمة الإدخال لطريقة الكتابة () ستقوم طريقة الكتابة () بكتابة البيانات إلى القناة بترتيب المخازن المؤقتة في المصفوفة. لذلك، إذا كانت سعة المخزن المؤقت 128 بايت ولكنه يحتوي فقط على 58 بايت من البيانات، فسيتم كتابة 58 بايت من البيانات إلى القناة. لذلك، على عكس تشتت القراءات، يمكن لـ Gathering Writes التعامل مع الرسائل الديناميكية بشكل أفضل.
نقل البيانات بين القنوات
(العنوان الأصلي لهذا الجزء، المؤلف: جاكوب جينكوف، المترجم: غوه لي، المصحح اللغوي: تشو تاي)
في Java NIO، إذا كانت إحدى القناتين عبارة عن FileChannel، فيمكنك نقل البيانات مباشرة من قناة واحدة (ملاحظة المترجم: غالبًا ما تتم ترجمة القناة كقناة باللغة الصينية) إلى قناة أخرى.
نقل من ()
يمكن لطريقة TransferFrom() الخاصة بـ FileChannel نقل البيانات من القناة المصدر إلى FileChannel (ملاحظة المترجم: تم شرح هذه الطريقة في وثائق JDK على أنها نقل البايتات من قناة بايت محددة قابلة للقراءة إلى ملف هذه القناة. ). إليك مثال بسيط:
انسخ رمز الكود كما يلي:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
موقف طويل = 0؛
عدد طويل = fromChannel.size();
toChannel.transferFrom(position, count, fromChannel);
يشير موضع معلمة الإدخال للطريقة إلى البدء من الموضع لكتابة البيانات إلى الملف الهدف، ويشير العدد إلى الحد الأقصى لعدد البايتات المنقولة. إذا كانت القناة المصدر تحتوي على مساحة متبقية أقل من عدد البايتات، يكون عدد البايتات المنقولة أقل من عدد البايتات المطلوبة.
بالإضافة إلى ذلك، تجدر الإشارة إلى أنه عند تنفيذ SoketChannel، لن يقوم SwitchChannel إلا بنقل البيانات المعدة في هذه اللحظة (والتي قد تكون أقل من عدد البايتات). ولذلك، قد لا تقوم قناة SwitchChannel بنقل كافة البيانات المطلوبة (عدد البايتات) إلى FileChannel.
نقل إلى()
تقوم طريقة TransferTo() بنقل البيانات من FileChannel إلى قنوات أخرى. إليك مثال بسيط:
انسخ رمز الكود كما يلي:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
موقف طويل = 0؛
عدد طويل = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
هل وجدت أن هذا المثال مشابه بشكل خاص للمثال السابق؟ باستثناء أن كائن FileChannel الذي يستدعي الطريقة مختلف، فكل شيء آخر هو نفسه.
المشاكل المذكورة أعلاه حول SwitchChannel موجودة أيضًا في طريقة النقل إلى (). سوف يستمر SwitchChannel في إرسال البيانات حتى يتم ملء المخزن المؤقت المستهدف.
محدد
(رابط إلى النص الأصلي لهذا القسم، المؤلف: جاكوب جينكوف، المترجم: لانجيف، المراجع: دينغ يي)
المحدد هو أحد مكونات Java NIO يمكنه اكتشاف قناة NIO واحدة أو أكثر ومعرفة ما إذا كانت القناة جاهزة لأحداث مثل القراءة والكتابة. بهذه الطريقة، يمكن لخيط واحد إدارة قنوات متعددة وبالتالي اتصالات شبكة متعددة.
(1) لماذا نستخدم المحدد؟
تتمثل ميزة استخدام مؤشر ترابط واحد فقط للتعامل مع قنوات متعددة في الحاجة إلى عدد أقل من سلاسل الرسائل للتعامل مع القنوات. في الواقع، من الممكن استخدام مؤشر ترابط واحد فقط للتعامل مع جميع القنوات. بالنسبة لنظام التشغيل، يعد تبديل السياق بين سلاسل العمليات مكلفًا للغاية، ويشغل كل مؤشر ترابط بعض موارد النظام (مثل الذاكرة). لذلك، كلما قل عدد الخيوط المستخدمة، كلما كان ذلك أفضل.
ومع ذلك، ضع في اعتبارك أن أنظمة التشغيل ووحدات المعالجة المركزية الحديثة تتحسن أكثر فأكثر في تنفيذ المهام المتعددة، وبالتالي يصبح الحمل الزائد لتعدد العمليات أصغر فأصغر بمرور الوقت. في الواقع، إذا كانت وحدة المعالجة المركزية تحتوي على مراكز متعددة، فإن عدم استخدام المهام المتعددة قد يكون إهدارًا لطاقة وحدة المعالجة المركزية. على أية حال، مناقشة هذا التصميم يجب أن تكون في مقالة مختلفة. ويكفي هنا أن تعرف أنه يمكنك التعامل مع قنوات متعددة باستخدام Selector.
فيما يلي مثال على رسم تخطيطي لسلسلة واحدة تستخدم محددًا لمعالجة ثلاث قنوات:
(2) إنشاء المحدد
قم بإنشاء محدد عن طريق استدعاء الأسلوب Selector.open()، كما يلي:
انسخ رمز الكود كما يلي:
محدد محدد = Selector.open();
(3) قم بتسجيل القناة باستخدام المحدد
لاستخدام القناة والمحدد معًا، يجب تسجيل القناة لدى المحدد. ويتم تحقيق ذلك من خلال طريقة SelectableChannel.register()، كما يلي:
انسخ رمز الكود كما يلي:
channel.configureBlocking(false);
مفتاح التحديد = Channel.register(selector,
Selectionkey.OP_READ);
عند استخدامها مع محدد، يجب أن تكون القناة في وضع عدم الحظر. وهذا يعني أنه لا يمكنك استخدام FileChannel مع محدد لأنه لا يمكن تحويل FileChannel إلى وضع عدم الحظر. قنوات المقبس جيدة.
لاحظ المعلمة الثانية لطريقة التسجيل (). هذا هو "مجموعة الاهتمامات"، وهو ما يعني الأحداث التي تهمك عند الاستماع إلى القناة من خلال أداة التحديد. هناك أربعة أنواع مختلفة من الأحداث التي يمكن الاستماع إليها:
يتصل
يقبل
يقرأ
يكتب
القناة التي تقوم بتشغيل حدث ما تعني أن الحدث جاهز. ولذلك، فإن القناة التي تتصل بنجاح بخادم آخر تسمى "اتصال جاهز". يُقال إن قناة مأخذ توصيل الخادم "جاهزة للاستقبال" عندما تكون جاهزة لاستقبال الاتصالات الواردة. يُقال إن القناة التي تحتوي على بيانات للقراءة هي "جاهزة للقراءة". يمكن القول بأن القناة التي تنتظر كتابة البيانات "جاهزة للكتابة".
يتم تمثيل هذه الأحداث الأربعة بالثوابت الأربعة لـ SelectionKey:
مفتاح التحديد.OP_CONNECT
مفتاح التحديد.OP_ACCEPT
مفتاح التحديد.OP_READ
SelectionKey.OP_WRITE
إذا كنت مهتمًا بأكثر من حدث واحد، فيمكنك استخدام عامل البت OR لتوصيل الثوابت، كما يلي:
انسخ رمز الكود كما يلي:
int InterestSet = SelectionKey.OP_READ |.
سيتم ذكر مجموعات الفائدة أدناه.
(4) مفتاح الاختيار
في القسم السابق، عند تسجيل قناة باستخدام المحدد، تقوم طريقة التسجيل () بإرجاع كائن SelectionKey. يحتوي هذا الكائن على بعض الخصائص التي قد تهمك:
جمع الفائدة
مجموعة جاهزة
قناة
محدد
كائنات إضافية (اختياري)
أدناه أصف هذه الخصائص.
جمع الفائدة
كما هو موضح في قسم تسجيل قناة باستخدام محدد، فإن مجموعة الاهتمامات عبارة عن مجموعة من الأحداث المثيرة للاهتمام التي تحددها. يمكنك قراءة وكتابة مجموعة الاهتمامات من خلال SelectionKey، مثل هذا:
انسخ رمز الكود كما يلي:
int InterestSet = SelectionKey.interess();
boolean isInterestedInAccept= (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = InterestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = InterestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = InterestSet & SelectionKey.OP_WRITE;
يمكن ملاحظة أنه باستخدام "bit AND" لتشغيل مجموعة الاهتمامات وثابت SelectionKey المحدد، يمكنك تحديد ما إذا كان حدث معين موجودًا في مجموعة الاهتمامات.
مجموعة جاهزة
المجموعة الجاهزة هي مجموعة العمليات التي تكون القناة جاهزة لها. بعد التحديد (الاختيار)، ستصل أولاً إلى المجموعة الجاهزة. سيتم شرح الاختيار في القسم التالي. يمكن الوصول إلى المجموعة الجاهزة على النحو التالي:
int ReadySet = SelectionKey.readyOps();
يمكنك استخدام نفس طريقة اكتشاف مجموعة الاهتمام لاكتشاف الأحداث أو العمليات الجاهزة في القناة. ومع ذلك، تتوفر أيضًا الطرق الأربع التالية، وجميعها تُرجع نوعًا منطقيًا:
انسخ رمز الكود كما يلي:
SelectionKey.isAcceptable();
SelectionKey.isConnectable();
SelectionKey.isReadable();
SelectionKey.isWritable();
قناة + محدد
يعد الوصول إلى القناة والمحدد من SelectionKey أمرًا بسيطًا. على النحو التالي:
انسخ رمز الكود كما يلي:
Channelchannel= SelectionKey.channel();
محدد محدد = التحديدKey.selector();
كائنات إضافية
يمكن إرفاق كائن أو مزيد من المعلومات بمفتاح التحديد لتحديد قناة معينة بسهولة. على سبيل المثال، يمكنك إرفاق مخزن مؤقت للاستخدام مع قناة، أو كائن يحتوي على بيانات مجمعة. كيفية استخدامه:
انسخ رمز الكود كما يلي:
SelectionKey.attach(theObject);
Object AttachedObj = SelectionKey.attachment();
يمكنك أيضًا إرفاق كائنات عند تسجيل القناة باستخدام المحدد باستخدام طريقة التسجيل (). يحب:
انسخ رمز الكود كما يلي:
SelectionKey key =channel.register(selector, SelectionKey.OP_READ, theObject);
(5) حدد القناة من خلال المحدد
بمجرد تسجيل قناة واحدة أو أكثر باستخدام المحدد، يمكن استدعاء العديد من أساليب التحديد () المحملة بشكل زائد. تقوم هذه الطرق بإرجاع تلك القنوات الجاهزة للحدث الذي تهتم به (مثل الاتصال أو القبول أو القراءة أو الكتابة). بمعنى آخر، إذا كنت مهتمًا بالقنوات "الجاهزة للقراءة"، فسيقوم التابع Select() بإرجاع تلك القنوات التي تكون أحداث القراءة جاهزة لها.
إليك طريقة التحديد ():
كثافة العمليات حدد ()
int حدد (مهلة طويلة)
إنت حدد الآن ()
يقوم Select() بالحظر حتى تصبح قناة واحدة على الأقل جاهزة للحدث الذي قمت بتسجيله.
تحديد (مهلة طويلة) هو نفس تحديد ()، باستثناء أنه سيتم حظره لمدة تصل إلى مللي ثانية (المعلمة).
لا يتم حظر ()selectNow ويعود فورًا بغض النظر عن القناة الجاهزة (ملاحظة المترجم: تنفذ هذه الطريقة عملية اختيار غير محظورة. إذا لم تصبح أي قناة قابلة للتحديد منذ عملية التحديد السابقة، فإن هذه الطريقة ترجع صفرًا مباشرة.).
تشير قيمة int التي يتم إرجاعها بواسطة طريقة التحديد () إلى عدد القنوات الجاهزة. أي عدد القنوات التي أصبحت جاهزة منذ آخر استدعاء لطريقة التحديد (). إذا تم استدعاء طريقة التحديد ()، فسيتم إرجاع 1 لأن إحدى القنوات تصبح جاهزة. إذا تم استدعاء طريقة التحديد () مرة أخرى، إذا كانت قناة أخرى جاهزة، فسترجع 1 مرة أخرى. إذا لم يتم إجراء أي عمليات على القناة الأولى الجاهزة، فهناك الآن قناتان جاهزتان، ولكن بين كل استدعاء لأسلوب Select()، تكون هناك قناة واحدة فقط جاهزة.
المفاتيح المحددة ()
بمجرد استدعاء طريقة التحديد () وتشير قيمة الإرجاع إلى أن قناة واحدة أو أكثر جاهزة، يمكن بعد ذلك الوصول إلى القنوات الجاهزة في "مجموعة المفاتيح المحددة" عن طريق استدعاء طريقة المفاتيح المحددة () الخاصة بالمحدد. كما هو موضح أدناه:
انسخ رمز الكود كما يلي:
تعيين المفاتيح المحددة = Selector.selectedKeys();
عند تسجيل قناة مثل المحدد، يقوم الأسلوب Channel.register() بإرجاع كائن SelectionKey. يمثل هذا الكائن القناة المسجلة في المحدد. يمكن الوصول إلى هذه الكائنات من خلال طريقة SelectionKey() المحددة في SelectionKey.
يمكن الوصول إلى القنوات الجاهزة عن طريق اجتياز مجموعة المفاتيح المحددة هذه. على النحو التالي:
انسخ رمز الكود كما يلي:
تعيين المفاتيح المحددة = Selector.selectedKeys();
Iterator keyIterator = SelectedKeys.iterator();
بينما (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
إذا (key.isAcceptable()) {
// تم قبول الاتصال بواسطة ServerSocketChannel.
} وإلا إذا (key.isConnectable()) {
// تم إنشاء اتصال مع خادم بعيد.
} وإلا إذا (key.isReadable()) {
// القناة جاهزة للقراءة
} وإلا إذا (key.isWritable()) {
// القناة جاهزة للكتابة
}
keyIterator.<tuihighlight><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;">إزالة</a ></tuihighlight>();
}
تتكرر هذه الحلقة عبر كل مفتاح في مجموعة المفاتيح المحددة وتكتشف الحدث الجاهز للقناة المقابلة لكل مفتاح.
لاحظ استدعاء keyIterator.remove() في نهاية كل تكرار. لا يقوم المحدد بإزالة مثيلات SelectionKey من مجموعة المفاتيح المحددة نفسها. يجب إزالة نفسك عند معالجة القناة. في المرة التالية التي تصبح فيها القناة جاهزة، سيقوم المحدد بوضعها في مجموعة المفاتيح المحددة مرة أخرى.
يجب تحويل القناة التي يتم إرجاعها بواسطة الأسلوب SelectionKey.channel() إلى النوع الذي تريد معالجته، مثل ServerSocketChannel أو SwitchChannel، وما إلى ذلك.
(6) الاستيقاظ()
تم حظر الخيط بعد استدعاء طريقة التحديد () حتى إذا لم تكن هناك قناة جاهزة، فهناك طريقة لإعادتها من طريقة التحديد (). ما عليك سوى السماح للخيوط الأخرى باستدعاء طريقة Selector.wakeup() على الكائن حيث يطلق الخيط الأول على طريقة Select(). سيعود الخيط المحظور في طريقة التحديد () على الفور.
إذا قام مؤشر ترابط آخر باستدعاء طريقة Wakeup ()، ولكن لا يوجد مؤشر ترابط محظور حاليًا على طريقة Select ()، فإن الخيط التالي الذي يستدعي طريقة Select () سوف "ينشط" على الفور.
(7)إغلاق()
سيؤدي استدعاء أسلوب الإغلاق () الخاص به بعد استخدام المحدد إلى إغلاق المحدد وإبطال جميع مثيلات SelectionKey المسجلة في المحدد. القناة نفسها لا تغلق.
(8) مثال كامل
فيما يلي مثال كامل، افتح محددًا، وقم بتسجيل قناة في المحدد (تم حذف عملية تهيئة القناة)، ثم راقب باستمرار ما إذا كانت الأحداث الأربعة للمحدد (القبول، والاتصال، والقراءة، والكتابة) جاهزة أم لا.
انسخ رمز الكود كما يلي:
محدد محدد = Selector.open();
channel.configureBlocking(false);
مفتاح التحديد =channel.register(selector, SelectionKey.OP_READ);
بينما (صحيح) {
int ReadyChannels = Selector.select();
إذا (readyChannels == 0) يستمر؛
تعيين المفاتيح المحددة = Selector.selectedKeys();
Iterator keyIterator = SelectedKeys.iterator();
بينما (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
إذا (key.isAcceptable()) {
// تم قبول الاتصال بواسطة ServerSocketChannel.
} وإلا إذا (key.isConnectable()) {
// تم إنشاء اتصال مع خادم بعيد.
} وإلا إذا (key.isReadable()) {
// القناة جاهزة للقراءة
} وإلا إذا (key.isWritable()) {
// القناة جاهزة للكتابة
}
keyIterator.<tuihighlight><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;">إزالة</a ></tuihighlight>();
}
}
قناة الملف
(رابط إلى النص الأصلي لهذا القسم، المؤلف: جاكوب جينكوف، المترجم: تشو تاي، المصحح: دينغ يي)
FileChannel في Java NIO هي قناة متصلة بملف. يمكن قراءة الملفات وكتابتها من خلال قنوات الملفات.
لا يمكن ضبط FileChannel على وضع عدم الحظر، فهو يعمل دائمًا في وضع الحظر.
OpenFileChannel
قبل استخدام FileChannel، يجب فتحه. ومع ذلك، لا يمكننا فتح FileChannel مباشرةً، بل نحتاج إلى الحصول على مثيل FileChannel باستخدام InputStream أو OutputStream أو RandomAccessFile. فيما يلي مثال لفتح FileChannel عبر RandomAccessFile:
انسخ رمز الكود كما يلي:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
قراءة البيانات من FileChannel
قم باستدعاء إحدى طرق القراءة () المتعددة لقراءة البيانات من FileChannel. يحب:
انسخ رمز الكود كما يلي:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
أولاً، قم بتخصيص المخزن المؤقت. ستتم قراءة البيانات المقروءة من FileChannel في Buffer.
ثم قم باستدعاء الأسلوب FileChannel.read(). تقوم هذه الطريقة بقراءة البيانات من FileChannel إلى Buffer. تشير قيمة int التي يتم إرجاعها بواسطة طريقة القراءة () إلى عدد البايتات التي تمت قراءتها في المخزن المؤقت. إذا أعاد -1، فهذا يعني أنه تم الوصول إلى نهاية الملف.
كتابة البيانات إلى FileChannel
استخدم الأسلوب FileChannel.write() لكتابة البيانات إلى FileChannel. معلمة هذه الطريقة هي المخزن المؤقت. يحب:
انسخ رمز الكود كما يلي:
String newData = "سلسلة جديدة للكتابة إلى الملف..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
بينما (buf.hasRemaining()) {
Channel.write(buf);
}
لاحظ أن FileChannel.write() يتم استدعاؤه في حلقة while. نظرًا لعدم وجود ضمان لعدد البايتات التي يمكن لطريقة write() كتابتها إلى FileChannel في وقت واحد، يجب استدعاء طريقة write() بشكل متكرر حتى لا توجد بايتات في المخزن المؤقت لم تتم كتابتها إلى القناة.
CloseFileChannel
يجب إغلاق FileChannel عند الانتهاء منه. يحب:
انسخ رمز الكود كما يلي:
قناة. إغلاق ()؛
طريقة موضع FileChannel
في بعض الأحيان قد يكون من الضروري قراءة/كتابة البيانات في موقع محدد في FileChannel. يمكنك الحصول على الموضع الحالي لـ FileChannel عن طريق استدعاء طريقة الموضع ().
يمكنك أيضًا ضبط الموضع الحالي لـ FileChannel عن طريق استدعاء طريقة الموضع (long pos).
فيما يلي مثالان:
انسخ رمز الكود كما يلي:
نقاط البيع الطويلة = Channel.position();
Channel.position(pos +123);
إذا قمت بتعيين الموضع بعد نهاية الملف ثم حاولت قراءة البيانات من قناة الملف، فسترجع طريقة القراءة -1 - علامة نهاية الملف.
إذا قمت بتعيين الموضع بعد نهاية الملف ثم قمت بكتابة البيانات إلى القناة، فسيتم توسيع الملف إلى الموضع الحالي وسيتم كتابة البيانات. يمكن أن يؤدي هذا إلى "فجوات الملفات"، وهي فجوات بين البيانات المكتوبة في الملفات الفعلية الموجودة على القرص.
طريقة حجم ملف القناة
ستعيد طريقة size() لمثيل FileChannel حجم الملف المرتبط بالمثيل. يحب:
انسخ رمز الكود كما يلي:
حجم الملف الطويل = Channel.size();
طريقة اقتطاع FileChannel
يمكنك استخدام الأسلوب FileChannel.truncate() لاعتراض ملف. عند اعتراض ملف، سيتم حذف الجزء بعد الطول المحدد للملف. يحب:
انسخ رمز الكود كما يلي:
Channel.truncate(1024);
يعترض هذا المثال أول 1024 بايت من الملف.
طريقة فرض FileChannel
يفرض الأسلوب FileChannel.force() البيانات الموجودة في القناة والتي لم تتم كتابتها بعد من قرص إلى قرص. لأسباب تتعلق بالأداء، يقوم نظام التشغيل بتخزين البيانات مؤقتًا في الذاكرة، لذلك ليس هناك ضمان بأن البيانات المكتوبة إلى FileChannel ستتم كتابتها على القرص على الفور. لضمان ذلك، يجب استدعاء أسلوب force().
تحتوي طريقة force() على معلمة منطقية تشير إلى ما إذا كان سيتم كتابة بيانات تعريف الملف (معلومات الإذن، وما إلى ذلك) على القرص في نفس الوقت.
يفرض المثال التالي بيانات الملف وبيانات التعريف على القرص:
انسخ رمز الكود كما يلي:
قناة. القوة (صحيح)؛
قناة المقبس
(رابط إلى النص الأصلي لهذا القسم، المؤلف: جاكوب جينكوف، المترجم: تشنغ يوتينج، المراجع: دينغ يي)
تعتبر قناة JackChannel في Java NIO قناة متصلة بمقبس شبكة TCP. يمكن إنشاء قناة المقبس بالطريقتين التاليتين:
افتح SwitchChannel واتصل بخادم على الإنترنت.
عندما يصل اتصال جديد إلى ServerSocketChannel، يتم إنشاء SwitchChannel.
افتح قناة المقبس
فيما يلي كيفية فتح SwitchChannel:
انسخ رمز الكود كما يلي:
المقبسChannel المقبس = المقبسChannel.open();
المقبسChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
إغلاق قناة المقبس
عند الانتهاء من استخدام SwitchChannel، اتصل بـ ()SocketChannel لإغلاق SwitchChannel:
انسخ رمز الكود كما يلي:
المقبسChannel.Close();
قراءة البيانات من المقبس
لقراءة البيانات من قناة المقبس، قم باستدعاء إحدى طرق القراءة (). فيما يلي أمثلة:
انسخ رمز الكود كما يلي:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = المقبسChannel.read(buf);
أولاً، قم بتخصيص المخزن المؤقت. سيتم وضع البيانات المقروءة من SwitchChannel في هذا المخزن المؤقت.
ثم اتصل بـSocketChannel.read(). تقوم هذه الطريقة بقراءة البيانات من SwitchChannel إلى Buffer. تشير قيمة int التي يتم إرجاعها بواسطة طريقة القراءة () إلى عدد البايتات التي تمت قراءتها في المخزن المؤقت. إذا تم إرجاع -1، فهذا يعني أنه تمت قراءة نهاية الدفق (تم إغلاق الاتصال).
الكتابة إلى قناة المقبس
تستخدم عملية كتابة البيانات إلى SwitchChannel الأسلوب ()SocketChannel.write، الذي يأخذ المخزن المؤقت كمعلمة. الأمثلة هي كما يلي:
انسخ رمز الكود كما يلي:
String newData = "سلسلة جديدة للكتابة إلى الملف..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
بينما (buf.hasRemaining()) {
Channel.write(buf);
}
لاحظ أنه يتم استدعاء الأسلوب ()SocketChannel.write في حلقة زمنية. لا يمكن للأسلوب Write() ضمان عدد البايتات التي يمكن كتابتها إلى قناة المقبس. لذلك، نستدعي write() بشكل متكرر حتى لا يتبقى في المخزن المؤقت أي بايت للكتابة.
وضع عدم الحظر
يمكنك ضبط SwitchChannel على وضع عدم الحظر. بعد الإعداد، يمكنك الاتصال بـ () وقراءة () والكتابة () في الوضع غير المتزامن.
يتصل()
إذا كان SwitchChannel في وضع عدم الحظر وتم استدعاء الاتصال () في هذا الوقت، فقد تعود الطريقة قبل إنشاء الاتصال. لتحديد ما إذا كان الاتصال قد تم تأسيسه، يمكنك استدعاء الأسلوب FinishConnect(). مثله:
انسخ رمز الكود كما يلي:
المقبسChannel.configureBlocking(false);
المقبسChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
بينما(! المقبسChannel.finishConnect() ){
//انتظر، أو افعل شيئًا آخر...
}
يكتب()
في وضع عدم الحظر، قد يعود التابع write() قبل كتابة أي شيء. لذلك يجب استدعاء write() في الحلقة. لقد كانت هناك أمثلة من قبل، لذلك لن أخوض في التفاصيل هنا.
يقرأ()
في وضع عدم الحظر، قد يعود الأسلوب read() قبل قراءة أي بيانات. لذلك عليك الانتباه إلى قيمة الإرجاع int، والتي ستخبرك بعدد البايتات التي تمت قراءتها.
وضع عدم الحظر والمحددات
يعمل وضع عدم الحظر بشكل أفضل مع المحددات. من خلال تسجيل قناة مقبس واحدة أو أكثر مع المحدد، يمكنك سؤال المحدد عن القناة الجاهزة للقراءة والكتابة وما إلى ذلك. ستتم مناقشة مجموعة Selector وSocketChannel بالتفصيل لاحقًا.
قناة ServerSocket
(رابط إلى النص الأصلي لهذا القسم، المؤلف: جاكوب جينكوف، المترجم: تشنغ يوتينج، المراجع: دينغ يي)
ServerSocketChannel في Java NIO هي قناة يمكنها الاستماع إلى اتصالات TCP الواردة الجديدة، تمامًا مثل ServerSocket في IO القياسي. فئة ServerSocketChannel موجودة في الحزمة java.nio.channels.
هنا مثال:
انسخ رمز الكود كما يلي:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
بينما (صحيح) {
قناة المقبس - قناة المقبس =
serverSocketChannel.accept();
// افعل شيئًا باستخدام قناة المقبس...
}
افتح ServerSocketChannel
افتح ServerSocketChannel عن طريق استدعاء الأسلوب ServerSocketChannel.open() على سبيل المثال:
انسخ رمز الكود كما يلي:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
أغلق ServerSocketChannel
قم بإغلاق ServerSocketChannel عن طريق استدعاء الأسلوب ServerSocketChannel.cloud() على سبيل المثال:
انسخ رمز الكود كما يلي:
serverSocketChannel. Close();
الاستماع للاتصالات الواردة الجديدة
استمع للاتصالات الواردة الجديدة من خلال أسلوب ServerSocketChannel.accept(). عندما يعود الأسلوب Accept()، فإنه يقوم بإرجاع قناة المقبس التي تحتوي على الاتصال الوارد الجديد. لذلك، سيتم حظر طريقة Accept () حتى وصول اتصال جديد.
عادةً بدلاً من مجرد الاستماع إلى اتصال واحد، يتم استدعاء الأسلوب Accept() في الحلقة while كما في المثال التالي:
انسخ رمز الكود كما يلي:
بينما (صحيح) {
قناة المقبس - قناة المقبس =
serverSocketChannel.accept();
// افعل شيئًا باستخدام قناة المقبس...
}
بالطبع، يمكنك أيضًا استخدام معايير خروج أخرى إلى جانب true في الحلقة while.
وضع عدم الحظر
يمكن ضبط ServerSocketChannel على وضع عدم الحظر. في وضع عدم الحظر، ستعود طريقة Accept () على الفور. إذا لم يكن هناك اتصال وارد جديد، فستكون قيمة الإرجاع فارغة. لذلك، تحتاج إلى التحقق مما إذا كانت قناة SwitchChannel التي تم إرجاعها فارغة أم لا. يحب:
انسخ رمز الكود كما يلي:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
بينما (صحيح) {
قناة المقبس - قناة المقبس =
serverSocketChannel.accept();
إذا (socketChannel!= فارغة){
// افعل شيئًا باستخدام قناة المقبس...
}
}
قناة مخطط البيانات
(رابط إلى النص الأصلي لهذا القسم، المؤلف: جاكوب جينكوف، المترجم: تشنغ يوتينج، المراجع: دينغ يي)
DatagramChannel في Java NIO هي قناة يمكنها إرسال واستقبال حزم UDP. نظرًا لأن UDP عبارة عن بروتوكول شبكة بدون اتصال، فلا يمكن قراءته وكتابته مثل القنوات الأخرى. يرسل ويستقبل حزم البيانات.
OpenDatagramChannel
إليك كيفية فتح DatagramChannel:
انسخ رمز الكود كما يلي:
قناة DatagramChannel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
يمكن لـ DatagramChannel الذي فتحه هذا المثال استقبال الحزم على منفذ UDP 9999.
تلقي البيانات
تلقي البيانات من DatagramChannel من خلال طريقة التلقي ()، مثل:
انسخ رمز الكود كما يلي:
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
ستقوم طريقة الاستلام () بنسخ محتوى حزمة البيانات المستلمة إلى المخزن المؤقت المحدد. إذا لم يتمكن المخزن المؤقت من استيعاب البيانات المستلمة، فسيتم تجاهل البيانات الزائدة.
إرسال البيانات
إرسال البيانات من DatagramChannel من خلال طريقة الإرسال () مثل:
انسخ رمز الكود كما يلي:
String newData = "سلسلة جديدة للكتابة إلى الملف..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent =channel.send(buf, new InetSocketAddress("jenkov.com", 80));
يرسل هذا المثال سلسلة من الأحرف إلى منفذ UDP رقم 80 لخادم "jenkov.com". نظرًا لأن الخادم لا يراقب هذا المنفذ، فلن يحدث شيء. كما أنه لن يخطرك ما إذا تم استلام الحزمة الصادرة، لأن UDP ليس لديه أي ضمانات فيما يتعلق بتسليم البيانات.
الاتصال بعنوان محدد
يمكن "اتصال" DatagramChannel بعنوان محدد في الشبكة. نظرًا لأن UDP غير متصل، فإن الاتصال بعنوان محدد لا يؤدي إلى إنشاء اتصال حقيقي مثل قناة TCP. بدلاً من ذلك، يتم تأمين DatagramChannel بحيث يمكنها إرسال واستقبال البيانات من عنوان محدد فقط.
هنا مثال:
انسخ رمز الكود كما يلي:
channel.connect(new InetSocketAddress("jenkov.com", 80));
بمجرد الاتصال، يمكنك أيضًا استخدام طريقتي القراءة () والكتابة () تمامًا كما تفعل مع القناة التقليدية. لا توجد ضمانات فيما يتعلق بنقل البيانات. فيما يلي بعض الأمثلة:
انسخ رمز الكود كما يلي:
int bytesRead =channel.read(buf);
int bytesWritten =channel.write(but);
ماسورة
(رابط إلى النص الأصلي لهذا القسم، المؤلف: جاكوب جينكوف، المترجم: هوانغ تشونغ، التدقيق اللغوي: دينغ يي)
أنبوب Java NIO هو اتصال بيانات أحادي الاتجاه بين خيطين. يحتوي الأنبوب على قناة مصدر وقناة بالوعة. سيتم كتابة البيانات إلى قناة الحوض وقراءتها من القناة المصدر.
فيما يلي توضيح لمبدأ الأنابيب:
إنشاء خط أنابيب
افتح الأنبوب من خلال الأسلوب Pipe.open(). على سبيل المثال:
انسخ رمز الكود كما يلي:
أنبوب الأنبوب = Pipe.open();
كتابة البيانات إلى الأنبوب
لكتابة البيانات إلى الأنبوب، تحتاج إلى الوصول إلى قناة الحوض. مثله:
انسخ رمز الكود كما يلي:
Pipe.SinkChannel faucetChannel = Pipe.sink();
اكتب البيانات إلى SinkChannel عن طريق استدعاء طريقة write() الخاصة بـ SinkChannel، مثل هذا:
انسخ رمز الكود كما يلي:
String newData = "سلسلة جديدة للكتابة إلى الملف..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
بينما (buf.hasRemaining()) {
<b>sinkChannel.write(buf);</b>
}
[شفرة]
قراءة البيانات من الأنابيب
لقراءة البيانات من الأنبوب، تحتاج إلى الوصول إلى القناة المصدر، مثل هذا:
[شفرة]
Pipe.SourceChannel sourceChannel = Pipe.source();
قم باستدعاء طريقة القراءة () للقناة المصدر لقراءة البيانات، كما يلي:
انسخ رمز الكود كما يلي:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
ستخبرنا قيمة int التي تم إرجاعها بواسطة طريقة read() بعدد البايتات التي تمت قراءتها في المخزن المؤقت.