اذهب إلى المحتوى

تواصل تطبيقات جافا عبر الشبكة


رضوى العربي

لا تُعدّ الشبكات -بقدر ما يَهُمّ البرامج- أكثر من مجرد مصدرٍ يُمكِن قراءة البيانات منه، أو مقصدٍ يُمكِن إرسال البيانات إليه. يُسطِّح ذلك الأمور إلى درجةٍ كبيرة؛ فليس التعامل مع الشبكات بالتأكيد بنفس سهولة التعامل مع الملفات مثلًا، وتُمكِّنك لغة جافا مع ذلك من إجراء الاتصالات الشبكية باستخدام مجاري تدفق streams الدْخل والخرج بنفس الطريقة التي تَستخدِمها بها للتواصل مع المُستخدِم أو لمعالجة الملفات.

بالرغم من ذلك، تنطوي عملية إجراء اتصال شبكي بين حاسوبين على الكثير من التعقيدات؛ حيث ينبغي أن يتفقا بطريقةٍ ما على فتح اتصالٍ بينهما. بالإضافة إلى ذلك، إذا كان كلُ حاسوبٍ قادرًا على إرسال البيانات إلى الحاسوب الآخر، تُصبِح مزامنة التواصل بينهما مشكلةً أخرى، ولكن تُعدّ الأساسيات في العموم هي نفسها.

تُعدّ java.net إحدى حزم packages جافا القياسية، حيث تتضمَّن عدَّة أصنافٍ للتعامل مع الشبكات، كما تدعم طريقتين مختلفتين لإجراء عمليات الدخل والخرج خلال الشبكة. تعتمد الطريقة الأولى عالية المستوى high level على شبكة الإنترنت العالمية World Wide Web، وتُوفِّر إمكانياتٍ للاتصال الشبكي مماثلةً لتلك الإمكانيات التي يَستخدِمها متصفح الإنترنت عند تحميله للصفحات. يُعدّ java.net.URL و java.net.URLConnection الصنفين الأساسين المُستخدَمين مع هذا النوع من الشبكات.

يُمثِّل أي كائنٍ من النوع URL تمثيلًا مجرّدًا لمُحدِّد مورد مُوحَّد Universal Resource Locator؛ فقد يكون عنوانًا لمستند HTML، أو لأي موردٍ آخر؛ بينما يُمثِّل أي كائنٍ من النوع URLConnection اتصالًا شبكيًا مع إحدى تلك الموارد.

من الجهة الأخرى، تَنظر الطريقة الثانية الأكثر عمومية وأهمية للشبكة بمستوى منخفض قليلًا low level؛ حيث تعتمد على فكرة المقابس socket المُستخدَمة لإنشاء اتصالٍ مع برنامجٍ آخر خلال الشبكة. يشتمل الاتصال عبر الشبكة على مقبسين، واحدٌ في كل طرف من طرفي الاتصال، وتَستخدِم جافا الصنف java.net.Socket لتمثيل المقابس المُستخدَمة بالاتصال الشبكي.

أُخِذَت كلمة مِقبس من التصور المادي لتوصيل الحاسوب بسلكٍ بهدف ربطه بشبكة، ولكن ما نعنيه بالمقبس هنا أنه كائنٌ ينتمي إلى الصنف Socket. يَستطيع أي برنامجٍ أن يحتوي على مجموعةٍ من المقابس بنفس الوقت؛ بحيث يتَصِل كُلٌ منها ببرنامجٍ آخرٍ مُشغَّلٍ على حاسوبٍ آخر ضمن الشبكة، أو ربما على نفس الحاسوب. تَعتمِد جميع تلك الاتصالات على الاتصال الشبكي المادي نفسه.

يتناول هذا مقال مقدمةً مُختصرة عن أساسيات أصناف الشبكات، كما يشرح علاقتها بمجاري الدْخَل والخرج.

محددات الموارد والصنفان URL و URLConnection

يُستخدَم الصنف URL لتمثيل الموارد resources بشبكة الويب العالمية World Wide Web، حيث يَملُك كل موردٍ منها عنوانًا يُميّزها، والذي يحتوي على معلوماتٍ كافية تُمكِّن متصفح الويب من العثور على المورد على الشبكة واسترجاعه. يُعرَف هذا العنوان باسم "محدِّد الموارد المُوحَّد universal resource locator - URL"، كما يُمكِنه في الواقع أن يشير إلى مواردٍ من مصادر أخرى غير الويب، فقد يُشير مثلًا إلى إحدى ملفات الحاسوب.

يُمثِّل أي كائنٍ ينتمي إلى الصنف URL عنوانًا معينًا، وبمجرد حصولك على واحدٍ من تلك الكائنات، يُمكِنك إنشاء كائنٍ من الصنف URLConnection للاتصال مع المورد الموجود بذلك العنوان. يُكْتَب محدِّد الموارد المُوحَّد عادةً بهيئة سلسلةٍ نصية، مثل "http://math.hws.edu/eck/index.html"، ولكن هناك أيضًا محدِّدات مواردٍ نسبية relative url، والتي يُمكِنها تخصيص موقع موردٍ معين بالنسبة لموقع موردٍ آخر، والذي يَعمَل في تلك الحالة كأنه أساسٌ أو سياقٌ لمحدِّد المورد النسبي؛ فإذا كان السياق هو "http://math.hws.edu/eck/" مثلًا، فسيُشير المورد النسبي غير الكامل "index.html" في تلك الحالة إلى الآتي:

 "http://math.hws.edu/eck/index.html"

لاحِظ أن كائنات الصنف URL ليست مجرد سلاسلٍ نصية، ولكن يُمكِن إنشاؤها من التمثيل النصي لمحدِّد موردٍ موحَّد، كما يُمكِن إنشاء تلك الكائنات بالاستعانة بكائن URL آخر يُوفِّر سياقًا معينًا مع سلسلةٍ نصيةٍ تُوفِّر مُحدِّد المورد النسبي لذلك السياق. انظر تعريف البناة constructors المُعرَّفة بالصنف:

public URL(String urlName) throws MalformedURLException

و

public URL(URL context, String relativeName) throws MalformedURLException

حيث تُبلِّغ تلك البناة عن استثناء exception من النوع MalformedURLException، إذا لم تكن السلاسل النصية المُمرَّرة إليها مُمثِلةً لمحدِّدات مواردٍ مُوحَّدةٍ سليمة. لاحِظ أن الصنف MalformedURLException هو صنفٌ فرعيٌ من الصنف IOException، أي أنه يتطلَّب معالجةً إلزاميةً للاستثناءات.

بمجرد حصولنا على كائن URL سليم، يُمكِننا استدعاء تابِعه openConnection()‎ لإجراء اتصالٍ معه؛ حيث يُعيد هذا التابع كائنًا من النوع URLConnection، والذي يُمكِننا استدعاء تابعه getInputStream()‎ لإنشاء كائنٍ من النوع InputStream، ونَستطيع بذلك قراءة بيانات المورد الذي يُمثِّله الكائن. انظر الشيفرة التالية:

URL url = new URL(urlAddressString);
URLConnection connection = url.openConnection();
InputStream in = connection.getInputStream();

قد يُبلِّغ التابعان openConnection()‎ و getInputStream()‎ عن استثناؤات من النوع IOException. بمجرد إنشاء كائن الصنف InputStream، يُمكِننا أن نقرأ بياناته كما ناقشنا بالأقسام السابقة، بما في ذلك تضمينه داخل مجاري الدخل input stream من أنواعٍ أخرى، مثل BufferedReader أو Scanner. قد تؤدي قراءة بيانات المجرى إلى حدوث استثناءات بالتأكيد.

يتضمَّن الصنف URLConnection توابع نسخ instance methods أخرى مفيدة؛ حيث يُعيد التابع getContentType()‎ مثلًا سلسلةً نصيةً من النوع String تَصِف نوع المعلومات الموجودة بالمورد الذي يُمثِله كائن الصنف URL، كما يُمكِنه إعادة القيمة null إذا لم تكن نوعية المعلومات معروفةً بعد، أو لم يكن تحديد نوعها ممكنًا؛ أي قد لا نتمكَّن من معرفة نوع المعلومات حتى نُنشِئ مجرى المْدْخَلات باستدعاء التابع getInputStream()‎ ثم التابع getContentType()‎، حيث يُعيد هذا التابع سلسلةً نصيةً بصيغةٍ تُعرَف باسم نوع الوسيط "mime type"، مثل "text/plain" و "text/html" و "image/jpeg" و "image/png" وغيرها.

تتكوَّن جميع أنواع الوسائط من جزئين: نوعٌ عام، مثل "text" أو "image"، ونوعٌ أكثر تحديدًا من النوع العام، مثل "html" أو "png"؛ فإذا كنت مهتمًا بالبيانات النصية فقط مثلًا، يُمكِنك فحص فيما إذا بدأت السلسلة النصية المُعادة من التابع getContentType()‎ بكلمة "text". كان الهدف الأساسي من أنواع الوسائط هو مجرد وصف محتويات رسائل البريد الإلكتروني؛ فالاسم "mime" هو أساسًا اختصارٌ لعبارة "Multipurpose Internet Mail Extensions"، ولكنها مُستخدَمةٌ الآن على نطاقٍ واسع لتحديد نوع المعلومات الموجودة بملفٍ أو بموردٍ آخر عمومًا.

لنناقش الآن مثالًا قصيرًا على استخدام كائنٍ من النوع URL لقراءة البيانات الموجودة بمُحدِّد موردٍ معين، حيث يُنشِئ البرنامج الفرعي التالي اتصالًا مع المورد الذي يُمثِّله الكائن، ثم يَتأكَّد من أن البيانات التي يُشير إليها مُحدِّد المورد من النوع النصي، ويَنسَخ بعد ذلك النص إلى الشاشة. قد تُبلِّغ الكثير من العمليات المُضمَّنة بالبرنامج عن استثناءات، ولذلك أضفنا عبارة "throws IOException" أثناء التصريح عن البرنامج الفرعي؛ لنترك القرار للبرنامج المُستدعِي باختيار ما ينبغي فعله عند حدوث خطأٍ معين:

static void readTextFromURL( String urlString ) throws IOException {

     // 1

     URL url = new URL(urlString);
     URLConnection connection = url.openConnection();
     InputStream urlData = connection.getInputStream();

     // 2

     String contentType = connection.getContentType();
     System.out.println("Stream opened with content type: " + contentType);
     System.out.println();
     if (contentType == null || contentType.startsWith("text") == false)
          throw new IOException("URL does not seem to refer to a text file.");
     System.out.println("Fetching context from " + urlString + " ...");
     System.out.println();

     // 3

     BufferedReader in;  // للقراءة من مجرى الدخل الخاص بالاتصال

    in = new BufferedReader( new InputStreamReader(urlData) );

     while (true) {
          String line = in.readLine();
          if (line == null)
               break;
          System.out.println(line);
     }
     in.close();

} // end readTextFromURL()

حيث:

  • [1]: افتح اتصالًا مع مُحدِّد مورد موحَّد واحصل على مجرى دخل لقراءة البيانات منه.
  • [2]: اِفحص فيما إذا كانت المحتويات من النوع النصي.
اقتباس

ملاحظة: ينبغي استدعاء التابع connection.getContentType()‎ بعد استدعاء التابع getInputStream()‎

  • [3]: اِنسَخ الأسطر النصية من مجرى الدخل إلى الشاشة حتى تصِل إلى نهاية الملف، أو حتى يقع خطأ.

يَستخدِم البرنامج FetchURL.java البرنامج الفرعي المُعرَّف بالأعلى. بإمكانك تخصيص مُحدِّد المورد المطلوب أثناء تشغيل البرنامج من خلال سطر الأوامر؛ وإذا لم تُخصِّصه، فسيطلُب منك البرنامج تخصيصه. يَسمَح البرنامج بمُحدِّدات الموارد البادئة بكلمة "http://‎" أو "https://‎" إذا كان المُحدِّد يُشير إلى مورد بشبكة الإنترنت؛ أو تلك البادئة بكلمة "file://‎" إذا كان المُحدِّد يِشير إلى إحدى ملفات حاسوبك؛ أو تلك البادئة بكلمة "ftp://‎" إذا كان المُحدِّد يَستخدِم بروتوكول نقل الملفات File Transfer Protocol.

إذا لم يبدأ بأي من تلك الكلمات، فسيُضيف كلمة "http://‎" تلقائيًا إلى بداية مُحدِّد المورد. يُمكِنك أن تُجرِّب مثلًا مُحدِّد المورد "math.hws.edu/javanotes" لاسترجاع الصفحة الرئيسية لهذا الكتاب من موقعه الإلكتروني، كما يُمكِنك تجريبه أيضًا مع بعض المُدْخَلات غير السليمة، لترى نوعية الأخطاء المختلفة التي قد تَحصُل عليها.

بروتوكول TCP/IP والخوادم والعملاء

يعتمد نقل المعلومات عبر شبكة الانترنت على بروتوكولين، هما بروتوكول التحكم بالنقل Transmission Control Protocol وبروتوكول الإنترنت Internet Protocol، ويُشار إليهما مجتمعين باسم TCP/IP. هناك أيضًا بروتوكولٌ أبسط يُسمَى UDP؛ حيث يُمكِن اِستخدَامه بدلًا من TCP بتطبيقاتٍ معينة، وهو مدعومٌ من قِبل جافا. ولكن سنكتفي هنا بمناقشة TCP/IP الذي يُوفِّر نقلًا موثوقًا ثنائي الاتجاه للمعلومات بين حواسيب الشبكات.

لكي يتمكَّن برنامجان من تبادل المعلومات عبر بروتوكول TCP/IP، ينبغي أن يُنشِئ كلٌ منهما مقبسًا socket، كما ينبغي أن يكون المقبسان متصلين ببعضهما. تُنقَل المعلومات بينهما بعد إنشاء هذا الاتصال باستخدام مجاري تدفق streams دْخَل وخَرج. يَملُك كل برنامجٍ منهما مجريين للمُدخلات وللمُخرجات، ويَستطيع أحدهما مثلًا إرسال بعض البيانات إلى مجرى الخرج الخاص به، وستُنقَل إلى الحاسوب الآخر، ومنه إلى مجرى الدْخَل الخاص بالبرنامج الموجود على الطرف الآخر من الاتصال الشبكي. عندما يقرأ هذا البرنامج البيانات من مجرى الدْخَل الخاص به، يكون قد تَسلَّم بذلك البيانات التي أُرسلَت إليه عبر الشبكة.

تَكْمُن الصعوبة في إنشاء الاتصال الشبكي ذاته من الأساس، حيث تنطوي العملية على مقبسين. يجب أن يُنشِئ أحد البرنامجين مقبسًا socket ويتركه ينتظر طلب اتصالٍ من المقبس الآخر، ويُقال أن المقبس المُنتظِر يَستمِع للاتصال. ويُنشِئ البرنامج الآخر من الجهة الأخرى من الاتصال المُفترَض مقبسًا آخرًا ليُرسِل طلب اتصالٍ إلى المقبس المُستمِع، والذي يَستجيب بدوره عندما يَتَسلّم طلب الاتصال، وبذلك يكون الاتصال قد اُنشِئ. بمجرد إنشاء الاتصال، يستطيع كل برنامج الحصول على مجريين للدخل والخرج لإرسال البيانات عبر الاتصال، ويستمر الاتصال بين البرنامجين من خلال تلك المجاري حتى يُقرِّر أحدهما إغلاقه.

يُطلَق على البرنامج المُنشِئ لمقبس الاستماع listening socket اسم "الخادم server"؛ بينما يُطلَق على المقبس اسم "مقبس الخادم server socket". في المقابل، يُطلَق على البرنامج الذي يَتصِل بالخادم اسم "العميل client"؛ بينما يُطلَق على المقبس الذي يَستخدِمه لإجراء الاتصال اسم "مقبس العميل client socket". تكمن الفكرة ببساطة في أن الخادم يَقبُع بمكانٍ ما داخل الشبكة منتظرًا طلبات الاتصال من العملاء. يُمكِننا إذًا أن نفكر بالخادم وكأنه يُقدِم نوعًا معينًا من الخدمات، في حين يحاول العميل الوصول إلى تلك الخدمة عن طريق الاتصال بالخادم. يُعرَف ذلك باسم نموذج العميل / الخادم client/server model لنقل المعلومات عبر الشبكة.

يستطيع الخادم أيضًا وبالكثير من التطبيقات أن يُوفِّر اتصالات لأكثر من عميلٍ واحدٍ بنفس الوقت؛ فعندما يَتصِّل عميلٌ معين بمقبس الاستماع الخاص بخادمٍ من هذا النوع، لا يتوقف المقبس عن الاستماع، وإنما يستمر بالاستماع إلى أي طلبات اتصالٍ إضافية بنفس الوقت الذي يَخدِّم خلاله العميل الأول. يتطلَّب ذلك استخدام الخيوط threads التي سنناقش طريقة عملها بالمقال التالي.

يَستخدِم الصنف URL -الذي نُوقِش ببداية هذا المقال- مقبس عميلٍ لإجراء أي اتصالٍ ضروري عبر الشبكة؛ بينما يتواجد في الجهة الأخرى لذلك الاتصال برنامج خادم لاستقبال طلب الاتصال من كائن الصنف URL، ويقرأ طلبه بخصوص إحدى الملفات الموجودة بحاسوب الخادم، ثم يَستجيب للطلب بإرسال محتويات الملف المطلوب إلى ذلك الكائن عبر الشبكة. وأخيرًا، يُغلِق الخادم الاتصال بعد انتهائه من نقل البيانات.

يجب أن يَجِد برنامج العميل طريقةً ما لتخصيص أي حاسوبٍ من ضمن كل تلك الحواسيب الموجودة بالشبكة يريد الاتصال به. في الحقيقة، يَملُك كل حاسوبٍ بشبكة الإنترنت عنوان IP يُميّزه عن غيره؛ كما يُمكِن الإشارة إلى كثيرٍ من الحواسيب باستخدام أسماء المجال domain names، مثل "math.hws.edu" أو "http://www.whitehouse.gov" (انظر مقال "الإنترنت وما بعده وعلاقته بجافا").

تتكوَّن عناوين IP (أو IPv4) التقليدية من أعدادٍ صحيحةٍ من "32 بت"، وتُكْتَب عادةً بصيغةٍ عشريةٍ مُنقطَّة، مثل "64.89.144.237"؛ بحيث يُمثِّل كل عددٍ من الأعداد الأربعة ضمن ذلك العنوان عددًا صحيحًا من "8 بتات" بنطاقٍ يتراوح من "0" إلى "255". يتوفَّر الآن إصدارٌ أحدث من بروتوكول الإنترنت هو IPv6؛ حيث تتكوَّن عناوينه من أعدادٍ صحيحة من "128 بت"، وتُكْتَب عادةً بصيغةٍ ست عشريّة hexadecimal، ويَستخدِم نقطتين وربما بعض المعلومات الإضافية الآخرى. ما يزال الإصدار الأحدث IPv6 نادرًا من الناحية العملية.

يُمكِن لأي حاسوب أن يمتلك مجموعةً من عناوين IP، كما قد يمتلك عناوين IPv4 و IPv6، والتي يُعرَف إحداها عادةً باسم "عنوان الاسترجاع loopback"؛ حيث تستخدِم البرامج عنوان الاسترجاع، إذا كانت تريد التواصل مع برامجٍ أخرى على نفس الحاسوب. يَملُك عنوان الاسترجاع عنوان IPv4‎ هو ‏"127.0.0.1"، ويُمكِننا الإشارة إليه باستخدام اسم المجال "localhost".

بالإضافة إلى ذلك، قد يكون هناك عناوين IP أخرى مرتبطة باتصالٍ شبكي مادي، كما يحتوي عادةً أي حاسوب على أداةٍ لعرض عناوين IP الموجودة به. كتب المؤلف البرنامج ShowMyNetwork.java ليَفعَل الشيء نفسه، وحَصَل على الخرج التالي بعد أن شغَّله على حاسوبه:

   en1 :  /192.168.1.47  /fe80:0:0:0:211:24ff:fe9c:5271%5  
   lo0 :  /127.0.0.1  /fe80:0:0:0:0:0:0:1%1  /0:0:0:0:0:0:0:1%0

تُشيِر أول كلمة بكل سطرٍ منهما إلى اسم بطاقة الشبكة network interface، والتي يَفهَم معناها نظام التشغيل فقط، كما يحتوي كل سطرٍ على عناوين IP الخاصة بالبطاقة؛ حيث تُشير بطاقة "lo0" على سبيل المثال إلى عنوان الاسترجاع loopback الذي يَملُك عادةً عنوان IPv4‏ هو "127.0.0.1". في الحقيقة، إن العدد "192.168.1.47" هو الأكثر أهميةً من بين كل تلك الأعداد؛ فهو يُمثِّل عنوان IPv4 المُستخدَم للاتصالات عبر الشبكة؛ أما الأعداد الآخرى فهي عناوين IPv6. ملاحظة: لا تُعدّ الخطوط المائلة ببداية كل عنوان جزءًا فعليًا منه.

قد يحتوي الحاسوب على عدّة برامجٍ تُجرِي اتصالات شبكية بنفس الوقت، أو على برنامجٍ واحد يتبادل المعلومات مع مجموعةٍ من الحواسيب الأخرى؛ حيث يملك كلُّ اتصالٍ شبكي رقم مَنفَذ port number إلى جانب عنوان IP. يتكوَّن رقم المَنفَذ ببساطة من عددٍ صحيحٍ موجبٍ من "16 بت". لا يَستمِع الخادم إلى الاتصالات في العموم، وإنما يَستمِع إلى الاتصالات الواقعة برقم منفذٍ معين، ولذلك يجب أن يَعرِف أي عميلٍ مُحتمَل لخادمٍ معين كُلًا من عنوان الإنترنت (أو اسم المجال) الخاص بالحاسوب الذي يَعمَل عليه ذلك الخادم، وكذلك رقم المَنفَذ الذي يَستمِع إليه الخادم.

تَستمِع خوادم الإنترنت عمومًا إلى الاتصالات الواقعة برقم المنفذ "80"، كما تحدث بعض خدمات الويب القياسية الأخرى بأرقام منافذٍ قياسيةٍ أخرى. تُعدّ أرقام المنافذ الأقل من "1024" ضمن أرقام المنافذ القياسية، وهي في الواقع محجوزةٌ لخدماتٍ معينة، ولذلك إذا أنشأت خادمك الخاص، ينبغي أن تَستخدِم رقم منفذٍ أكبر من "1024".

المقابس والصنف Socket

تُوفِّر حزمة java.net الصنفين ServerSocket و Socket لتنفيذ اتصالات بروتوكول TCP/IP؛ حيث يُمثِل كائنٌ من الصنف ServerSocket مقبس استماع listening socket ينتظر طلبات الاتصال من العملاء؛ بينما يُمثِّل كائنٌ من الصنف Socket طرفًا واحدًا من اتصالٍ فعلي، حيث يمكن أن يُمثِّل عميلًا قد أرسل طلبًا إلى خادم.

يستطيع الخادم إنشاء كائنٍ من هذا الصنف -أي Socket-، ويطلب منه معالجة طلب اتصالٍ من عميلٍ معين، ويَسمَح ذلك للخادم بإنشاء عدة كائناتٍ من ذلك الصنف لمعالجة اتصالات عديدة. في المقابل، لا تُشارِك كائنات الصنف ServerSocket بالاتصالات، فهي فقط تستمع إلى طلبات الاتصال، وُتنشِئ كائناتٍ من الصنف Socket لمعالجة الاتصالات الفعلية.

عندما تُنشِئ كائنًا من الصنف ServerSocket، عليك أن تُخصِّص رقم المَنفَذ port number الذي سيستمع إليه الخادم. انظر الباني constructor الخاص بهذا الصنف:

public ServerSocket(int port) throws IOException

يجب أن يقع رقم المَنفَذ ضمن نطاقٍ يتراوح من "0" إلى "65535"، كما يجب أن يكون أكبر من "1024". يُبلِّغ الباني عن استثناء من النوع SecurityException، إذا كان رقم المَنفَذ المُخصَّص أقل من "1024"؛ كما يُبلِّغ عن استثناء من النوع IOException، إذا كان رقم المَنفَذ المُحدَّد مُستخدَمًا بالفعل. يُمكِنك مع ذلك تمرير القيمة "0" مثل معاملٍ للتابع لتخبره بأن الخادم بإمكانه الاستماع إلى أي رقم مَنفَذٍ متاح.

بمجرّد إنشاء كائنٍ من الصنف ServerSocket، فسيبدأ بالاستماع إلى طلبات الاتصال من العملاء. يَستقبِل التابع accept()‎ المُعرَّف بالصنف ServerSocket طلبًا، ثم يُنشِئ اتصالًا مع العميل، ويعيد كائنًا من النوع Socket يُمكِن اِستخدَامه للاتصال مع العميل. يُعرَّف التابع accept()‎ على النحو التالي:

public Socket accept() throws IOException

عندما تَستدعِي التابع accept()‎، فإنه لا يعيد قيمته حتى يتسلَّم طلب اتصال، أو حتى يقع خطأ ما، ولذلك يُعدّ تابعًا مُعطِّلًا block أثناء انتظاره للطلب؛ لأن البرنامج -أو بتعبير أدق الخيط thread الذي اِستدعَى التابع- لا يستطيع فعل ذلك بأي شيءٍ آخر، بينما تستطيع الخيوط الآخرى أن تُكمِل عملها بصورةٍ طبيعية. يُمكِنك استدعاء accept()‎ مرةً بعد أخرى لتَستقبِل عدة طلبات اتصال، وسيستمر كائن الصنف ServerSocket بالاستماع إلى طلبات الاتصال إلى أن يُغلَق باستدعاء تابعه close()‎، أو إلى أن يَحدُث خطأ، أو أن ينتهي البرنامج بطريقةٍ ما.

لنفترض أننا نريد إنشاء خادمٍ يَستمِع إلى رقم المَنفَذ "1728"، ويستمر باستقبال طلبات الاتصال طوال فترة تشغيل البرنامج. إذا كان provideService(Socket)‎ تابعًا مسؤولًا عن معالجة اتصالٍ مع عميلٍ واحد، يُمكِننا أن نَكْتُب برنامج الخادم التالي:

try {
   ServerSocket server = new ServerSocket(1728);
   while (true) {
      Socket connection = server.accept();
      provideService(connection);
   }
}
catch (IOException e) {
   System.out.println("Server shut down with error: " + e);
}

من جهة العميل، يُمكِننا إنشاء كائنٍ من النوع Socket باستخدام أحد البُناة constructors المُعرَّفة بالصنف Socket. يُمكِننا استخدام الباني التالي لنتمكَّن من الاتصال بخادمٍ معين نَعرِف الحاسوب الذي يَعمَل عليه وكذلك رقم المَنفَذ الذي يَستمِع إليه:

public Socket(String computer, int port) throws IOException

بإمكاننا تمرير اسم المجال domain name أو عنوان IP على أنه قيمةٌ للمعامل الأول بالباني السابق. سيُعطِّل block هذا الباني التنفيذ حتى يُنشَئ الاتصال أو حتى يَحدُث خطأً.

إذا كان لدينا مقبسٌ مُتصِلٌ بغض النظر عن طريقة إنشائه، يُمكِننا استدعاء أيٍّ من توابع الصنف Socket، مثل التابعين getInputStream()‎ و getOutputStream()‎ الذين يعيدان كائناتٍ من النوع InputStream و OutputStream على الترتيب، وبذلك، نكون قد حَصلنا على مجاري تدفق بإمكاننا اِستخدَامها لنقل المعلومات عبر هذا الاتصال. تُوضِح الشيفرة التالية الخطوط العريضة لتابع يُجرِي اتصالًا من طرف العميل:

// 1
void doClientConnection(String computerName, int serverPort) {
   Socket connection;
   InputStream in;
   OutputStream out;
   try {
      connection = new Socket(computerName,serverPort);
      in = connection.getInputStream();
      out = connection.getOutputStream();
   }
   catch (IOException e) {
      System.out.println(
          "Attempt to create connection failed with error: " + e);
      return;
   }
    .
    .  // ‫استخدم المجريين in و out لتبادل المعلومات مع الخادم
    .
   try {
      connection.close();
           // قد تعتمد على الخادم لغلق الاتصال بدلًا من غلقه بنفسك

   }
   catch (IOException e) {
   }
}  // end doClientConnection()

[1] افتح اتصالًا مع الحاسوب ورقم المَنفَذ المُخصَّصين للخادم، ثم انقل المعلومات عبر الاتصال.

تُسهِّل كل تلك الأصناف من التعقيدات الكثيرة التي ينطوي عليها نقل المعلومات عبر الشبكات؛ أما إذا كنت تَجِد هذا صعبًا بالفعل، فإنه إذًا أكثر صعوبة. إذا كانت الشبكات جديرةً بالثقة كليًا، فلربما كان الأمر بنفس السهولة التي وُصِفت بها هنا، ولكنها ليست كذلك؛ ولهذا ينبغي كتابة برامج الشبكات بصورةٍ متينة robust تُمكِّنها من التعامل مع أخطاء الشبكات والبشر، ولكننا لن نناقش هذا هنا. لقد شرحنا الأفكار الأساسية لبرمجة الشبكات عمومًا، وسنكتفي بما تعرَّضنا له من تطبيقاتٍ بسيطة. لنفحص الآن أمثلةً قليلة على برمجة الخادم / العميل.

برنامج عميل/خادم بسيط

يتكوَّن المثال الأول من برنامجين تتوفَّر شيفرتهما بالملفين DateClient.java و DateServer.java؛ حيث يُمثِّل الأول عميلًا شبكيًا network client بسيطًا؛ بينما يُمثِّل الآخر خادمًا server. يُنشِئ العميل اتصالًا مع الخادم، ويقرأ سطرًا نصيًا واحدًا منه، ثم يَعرِضه على الشاشة؛ حيث يتكوَّن هذا السطر من التاريخ والتوقيت الحالي بالحاسوب الذي يَعمَل عليه الخادم. يجب بالطبع أن يَعرِف العميل أي حاسوبٍ يَعمَل عليه الخادم، وكذلك رقم المَنفَذ port الذي يَستمِع إليه الخادم حتى يتمكَّن من إنشاء اتصالٍ معه.

يُمكِن أن يقع رقم المنفذ بين "1025" و "65535" عمومًا (الأرقام الواقعة بين "1" و "1024" محجوزةٌ للخدمات القياسية، ولا ينبغي استخدامها للخوادم الأخرى)، ولا يُحدِِث ذلك أي فرقٍ بشرط أن يَستخدِم الخادم والعميل نفس رقم المنفذ، ولنفترض أن الخادم بهذا المثال يَستمِع إلى رقم المنفذ "32007". يُمكِنك تمرير اسم أو عنوان IP الخاص بحاسوب الخادم مثل وسيط سطر أوامر للبرنامج DateClient أثناء تشغيله؛ فإذا كان الخادم مثلًا مُشغَّلًا على حاسوبٍ اسمه "math.hws.edu"، يُمكِنك أن تُشغِّل العميل باستخدام الأمر java DateClient math.hws.edu. في حالة عدم تخصيص حاسوب الخادم على أنه وسيط بسطر الأوامر، سيطلب البرنامج منك أن تُدْخِله. انظر الشيفرة الكاملة لبرنامج العميل:

import java.net.*;
import java.util.Scanner;
import java.io.*;

// 1
public class DateClient {

    public static final int LISTENING_PORT = 32007;

    public static void main(String[] args) {

        String hostName;         // اسم حاسوب الخادم
        Socket connection;       // رقم منفذ الاتصال مع الخادم
        BufferedReader incoming; // لقراءة البيانات من الاتصال

        // اقرأ حاسوب الخادم من سطر الأوامر

        if (args.length > 0)
            hostName = args[0];
        else {
            Scanner stdin = new Scanner(System.in);
            System.out.print("Enter computer name or IP address: ");
            hostName = stdin.nextLine();
        }

        // أجرِ الاتصال ثم اقرأ سطرًا نصيًا واعرضه
        try {
            connection = new Socket( hostName, LISTENING_PORT );
            incoming = new BufferedReader( 
                             new InputStreamReader(connection.getInputStream()) );
            String lineFromServer = incoming.readLine();
            if (lineFromServer == null) {
                    // 2
                throw new IOException("Connection was opened, " + 
                        "but server did not send any data.");
            }
            System.out.println();
            System.out.println(lineFromServer);
            System.out.println();
            incoming.close();
        }
        catch (Exception e) {
            System.out.println("Error:  " + e);
        }

    }  // end main()


} //end class DateClient

حيث:

  • [1]: يَفتَح هذا البرنامج اتصالًا مع الحاسوب المُخصَّص مثل وسيطٍ أول بسطر الأوامر؛ وإذا لم يكن مخصَّصًا بعد، اطلب من المُستخدِم أن يُدْخِل الحاسوب الذي يرغب في الاتصال به. يُجرى البرنامج الاتصال على رقم المنفذ LISTENING_PORT، ويقرأ سطرًا نصيًا واحدًا من الاتصال ثم يُغلق الاتصال، ويُرسِل أخيرًا النص المقروء إلى الخرج القياسي. الهدف من هذا البرنامج هو استخدامه مع البرنامج DateServer الذي يُرسِل كُلًا من التوقيت والتاريخ الحاليين بالحاسوب الذي يَعمَل عليه الخادم.
  • [2]: أعاد التابع incoming.readLine()‎ القيمة الفارغة مما يُعدّ إشارةً إلى وصوله إلى نهاية المجرى.

لاحِظ أن الشيفرة المسؤولة عن الاتصال مع الخادم مُضمَّنةٌ داخل تعليمة try..catch، حتى تَلتقِط استثناءات النوع IOException، والتي يُحتمَل وقوعها أثناء فتح الاتصال أو غلقه أو قراءة البيانات من مجرى الدْخَل. أحطنا مجرى الدْخَل الخاص بالاتصال بكائن من النوع BufferedReader، والذي يحتوي على التابع readLine()‎؛ وهذا يُسهّل قراءة سطرٍ واحدٍ من المُدْخَلات. إذا أعاد التابع القيمة null، يكون الخادم قد أغلق الاتصال دون أن يُرسِل أي بيانات.

حتى يَعمَل هذا البرنامج دون أخطاء، ينبغي أن تُشغِّل برنامج الخادم أولًا على الحاسوب الذي يُحاوِل العميل الاتصال به، حيث يُمكِنك أن تُشغِّل برنامجي العميل والخادم على نفس الحاسوب. على سبيل المثال، افتح نافذتي سطر أوامر، ثم شغِّل الخادم بإحداها، وشغِّل العميل بالنافذة الأخرى. علاوةً على ذلك، تَستطِيع أغلب الحواسيب استخدام اسم المجال "localhost" وعنوان IP التالي "127.0.0.1" للإشارة الى ذاتها، ولهذا يُمكِنك استخدام الأمر java DateClient localhost لتطلب من البرنامج DateClient الاتصال مع الخادم المُشغَّل على نفس الحاسوب؛ وإذا لم يَنجَح معك الأمر، جرِّب الأمر java DateClient 127.0.0.1.

أطلقنا اسم DateServer على برنامج الخادم المقابل لبرنامج العميل DataClient، حيث يُنشِئ برنامج DateServer مقبسًا socket من النوع ServerSocket للاستماع إلى طلبات الاتصال برقم المنفذ "32007". ويَستمِر بعد ذلك بتنفيذ حلقة تكرار loop لا نهائية تَستقبِل طلبات الاتصال وتُعالِجها. تستمر حلقة التكرار بالعمل حتى ينتهي البرنامج بطريقةٍ ما، مثل كتابة CONTROL-C بنافذة سطر الأوامر التي شغَّلت الخادم منها.

عندما يَستقبِل الخادم طلب اتصالٍ من عميلٍ معين، فإنه يَستدعِي برنامجًا فرعيًا لمعالجة هذا الاتصال، حيث يلتقط البرنامج الفرعي أي استثناءات من النوع Exception حتى لا ينهار الخادم، وهذا أمرٌ منطقي؛ فلا ينبغي للخادم أن يُغلَق لمجرد أن اتصالًا واحدًا مع عميل معين قد فشل لسببٍ ما؛ فقد يكون العميل هو سبب الخطأ أساسًا. بخلاف التقاطه للاستثناءات، فإنه يُنشِئ كائنًا من النوع PrintWriter لإرسال البيانات عبر الاتصال، ويُرسِل تحديدًا التاريخ والتوقيت الحالي إلى ذلك المجرى، ثم يُغلِق الاتصال؛ حيث يَستخدِم البرنامج الصنف القياسي java.util.Date للحصول على التوقيت الحالي، وتُمثِل كائنات الصنف Date تاريخًا وتوقيتًا محددًا، ويُنشِئ الباني الافتراضي new Date()‎ كائنًا يُمثِّل توقيت إنشائه. انظر الشيفرة الكاملة لبرنامج الخادم:

import java.net.*;
import java.io.*;
import java.util.Date;

// 1
public class DateServer {

    public static final int LISTENING_PORT = 32007;

    public static void main(String[] args) {

        ServerSocket listener;  // يستمع إلى طلبات الاتصال
        Socket connection;      // للتواصل مع البرنامج المتصل

        // 2

        try {
            listener = new ServerSocket(LISTENING_PORT);
            System.out.println("Listening on port " + LISTENING_PORT);
            while (true) {
                    // استقبل طلب الاتصال التالي وعالِجه
                connection = listener.accept(); 
                sendDate(connection);
            }
        }
        catch (Exception e) {
            System.out.println("Sorry, the server has shut down.");
            System.out.println("Error:  " + e);
            return;
        }

    }  // end main()


    // 3
    private static void sendDate(Socket client) {
        try {
            System.out.println("Connection from " +  
                    client.getInetAddress().toString() );
            Date now = new Date();  // التوقيت والتاريخ الحالي
            PrintWriter outgoing;   // مجرى لإرسال البيانات
            outgoing = new PrintWriter( client.getOutputStream() );
            outgoing.println( now.toString() );
            outgoing.flush();  // تأكّد من إرسال البيانات
            client.close();
        }
        catch (Exception e){
            System.out.println("Error: " + e);
        }
    } // end sendDate()


} //end class DateServer

حيث:

  • [1] يُمثِّل هذا البرنامج خادمًا يستمع إلى طلبات الاتصال على رقم المَنفَذ المخصَّص بواسطة الثابت LISTENING_PORT. عند فتح اتصال، يُرسِل البرنامج التوقيت الحالي إلى المقبس المُتصِل، ويستمر البرنامج في تسلُّم طلبات الاتصال ومعالجتها حتى يُغلِق عن طريق الضغط على CONTROL-C على سبيل المثال. ملاحظة: يعالِج الخادم طلبات الاتصال عند وصولها بدلًا من إنشاء خيطٍ thread منفصلٍ لمعالجتها.
  • [2] اِستقبِل طلبات الاتصال وعالِجها للأبد أو إلى حين وقوع خطأ. ملاحظة: يلتقط البرنامج sendDate()‎ الأخطاء الواقعة أثناء نقل البيانات مع برنامجٍ مُتصِل ويعالجها حتى لا ينهار الخادم.
  • [3] يُمثِّل المعامل client مقبسًا متصلًا بالفعل مع برنامجٍ آخر، لذلك احصل على مجرى خرج لهذا الاتصال، وأرسل إليه التوقيت الحالي، ثم أغلق الاتصال.

عندما تُشغِّل البرنامج DateServer بواجهة سطر الأوامر، فسيستمر بانتظار طلبات الاتصال ويُبلِّغ عنها عندما يتسلّمها. في المقابل، إذا أردت إتاحته على الحاسوب باستمرار، فينبغي أن تُشغِّله مثل عفريت daemon (وهو أمرٌ لن نناقش طريقة تنفيذه هنا)؛ وهو مثل برنامجٍ يَعمَل خفيةً وباستمرار على الحاسوب بغض النظر عن المُستخدِم. يُمكِنك ضبط الحاسوب ليبدأ بتشغيل هذا البرنامج تلقائيًا بمجرد أن تُشغِّله -أي الحاسوب-، وسيَعمَل بذلك البرنامج بالخلفية حتى إذا كنت تَستخدِم الحاسوب لأغراضٍ أخرى.

تُشغِّل الحواسيب المسؤولة عن إتاحة بعض الصفحات على شبكة الويب العالمية مثلًا عفريتًا يستمع إلى طلبات العملاء لتلك الصفحات ثم ينقلها إليهم، ويشبه ذلك البرنامج DateServer، كما يُمكِن تشغيله يدويًا ببساطة لاختباره؛ فكل تلك الأمثلة بالنهاية غير متينة كفاية ولا تتمتع بخاصياتٍ مكتملة كفاية لتشغيلها خوادمًا فعلًا. بالمناسبة، تُعدّ كلمة "daemon" تهجئةً مختلفةً لكلمة "demon" وتُنطَق بنفس الطريقة.

ملاحظة: بعد أن يَستدعِي البرنامج التابع outgoing.println()‎ لإرسال سطرٍ واحدٍ من البيانات إلى العميل؛ يَستدعِي أيضًا التابع outgoing.flush()‎ المُتاح بجميع أصناف مجاري الخرج، ليتأكَّد من إرسال البيانات التي كتبها بالمجرى بالفعل إلى مقصدها. في العموم، عليك استدعاء تلك الدالة بكل مرةٍ تَستخدِم بها مجرى خرج لإرسال بياناتٍ عبر اتصالٍ شبكي؛ لأنك إن لم تَفعَل ذلك، فقد يستمر المجرى بتخزين البيانات إلى أن يَصِل حجمها إلى القدر الكافي، ثم يُرسِلها بهدف رفع الكفاءة، ولكنه قد يؤدي إلى بعض التأخير غير المقبول إذا كان العميل ينتظر الرد، بل ربما حتى لا تُرسَل بعض البيانات عند غلق المقبس socket؛ ولذلك من الضروري استدعاء flush()‎ قبل غلق أي اتصال. يُعدّ هذا واحدًا من الحالات التي قد تتصرف خلاله تنفيذات جافا المختلفة بطرائقٍ مختلفة. في العموم، إذا لم تستدعي التابع flush مع مجاري تدفق الخرج، فلربما سيَعمَل تطبيقك على بعض أنواع الحواسيب ولكنه قد لا يَعمَل أيضًا على بعضها الآخر.

برنامج محادثة عبر الشبكة

كان الخادم بالبرنامج السابق DateServer يُرسِل المعلومات إلى العميل ليقرأها. في الواقع، يُمكِننا أيضًا إنشاء اتصالٍ ثنائي الاتجاه بين العميل والخادم. سنفحص أولًا برنامجًا بسيطًا مُكوَّنًا من عميل وخادم؛ حيث سيُمكِّن البرنامج المُستخدِمين بطرفي الاتصال من إرسال الرسائل إلى بعضهما بعضًا. يَعمَل البرنامج ببيئة سطر الأوامر command-line environment، حيث يستطيع كل مُستخدِم كتابة رسائله، وسينتظر الخادم -بهذا المثال- طلب اتصالٍ من عميلٍ واحد فقط، ثم سيتوقَّف عن الاستماع إلى أي طلبات اتصال جديدة؛ أي لن يتمكَّن أي عميلٍ آخر غير العميل الأول من الاتصال بالخادم.

بعد أن يَتصِل الخادم والعميل معًا، سيَعمَل البرنامج بكلا الطرفين بنفس الطريقة تقريبًا. سيَكتُب المُستخدِم بطرف العميل رسالةً ويُرسِلها إلى الخادم الذي سيَعرِضها بدوره إلى المُستخدِم بطرف الخادم، ثم سيَكْتُب المُستخدِم بطرف الخادم رسالةً لتنتقل بعدها إلى العميل الذي يُمكِنه كتابة رسالةٍ أخرى، وهكذا. سيستمر الأمر إلى أن يُقرِّر أي مُستخدِمٍ منهما كتابة كلمة "quit" برسالة؛ وعندما يحدث ذلك، يُغلَق الاتصال وينتهي البرنامج بكلا الطرفين.

يتشابه برنامجا الخادم والعميل إلى حدٍ كبير، بينما يختلفان فقط بطريقة فتح الاتصال؛ فبرنامج العميل مُصمَّم ليُرسِل أول رسالةٍ؛ بينما يُصمَّم الخادم لاستقبالها. يُمكِنك الإطلاع على برنامجي الخادم والعميل بالملفين CLChatClient.java و CLChatServer.java، حيث يُشير الاسم "CLChat" إلى اختصارٍ لعبارة "command-line chat"، أي محادثة عبر سطر الأوامر. تَعرِض الشيفرة التالية برنامج الخادم (برنامج العميل مشابه):

import java.net.*;
import java.util.Scanner;
import java.io.*;


// 1
public class CLChatServer {

    // رقم المنفذ الافتراضي الذي ينبيغ الاستماع إليه إذا لم يُخصِّصه المُستخدم
    static final int DEFAULT_PORT = 1728;

    // 2

    static final String HANDSHAKE = "CLChat";

    // يَسبِق هذا المحرف جميع الرسائل المُرسَلة
    static final char MESSAGE = '0';

    // يُرسَل هذا المحرف إلى البرنامج المتصل عندما يغلق المستخدم الاتصال
    static final char CLOSE = '1';


    public static void main(String[] args) {

        int port;   // رقم المنفذ الذي يستمع إليه الخادم

        ServerSocket listener;  // يستمع إلى طلبات الاتصال
        Socket connection;      // للتواصل مع العميل

        BufferedReader incoming;  // مجرى لاستقبال البيانات من العميل

        PrintWriter outgoing;     // مجرى لإرسال البيانات إلى العميل 
        String messageOut;        // رسالة ينبغي إرسالها إلى العميل
        String messageIn;         // رسالة ينبغي استقبالها من العميل

        // ‫مغلِّف للكائن System.in لقراءة أسطر مدخلة من المستخدم
        Scanner userInput;        

        // 3

        if (args.length == 0) 
            port = DEFAULT_PORT;
        else {
            try {
                port= Integer.parseInt(args[0]);
                if (port < 0 || port > 65535)
                    throw new NumberFormatException();
            }
            catch (NumberFormatException e) {
                System.out.println("Illegal port number, " + args[0]);
                return;
            }
        }

        // 4

        try {
            listener = new ServerSocket(port);
            System.out.println("Listening on port " + listener.getLocalPort());
            connection = listener.accept();
            listener.close();  
            incoming = new BufferedReader( 
                    new InputStreamReader(connection.getInputStream()) );
            outgoing = new PrintWriter(connection.getOutputStream());
            outgoing.println(HANDSHAKE);  // Send handshake to client.
            outgoing.flush();
            messageIn = incoming.readLine();  // Receive handshake from client.
            if (! HANDSHAKE.equals(messageIn) ) {
                throw new Exception("Connected program is not a CLChat!");
            }
            System.out.println("Connected.  Waiting for the first message.");
        }
        catch (Exception e) {
            System.out.println("An error occurred while opening connection.");
            System.out.println(e.toString());
            return;
        }

        // 5

        try {
            userInput = new Scanner(System.in);
            System.out.println("NOTE: Enter 'quit' to end the program.\n");
            while (true) {
                System.out.println("WAITING...");
                messageIn = incoming.readLine();
                if (messageIn.length() > 0) {
                        // 6
                    if (messageIn.charAt(0) == CLOSE) {
                        System.out.println("Connection closed at other end.");
                        connection.close();
                        break;
                    }
                    messageIn = messageIn.substring(1);
                }
                System.out.println("RECEIVED:  " + messageIn);
                System.out.print("SEND:      ");
                messageOut = userInput.nextLine();
                if (messageOut.equalsIgnoreCase("quit"))  {
                        // 7
                    outgoing.println(CLOSE);
                    outgoing.flush();  // تأكّد من إرسال البيانات
                    connection.close();
                    System.out.println("Connection closed.");
                    break;
                }
                outgoing.println(MESSAGE + messageOut);
                outgoing.flush(); // تأكَّد من إرسال البيانات
                if (outgoing.checkError()) {
                    throw new IOException("Error occurred while transmitting message.");
                }
            }
        }
        catch (Exception e) {
            System.out.println("Sorry, an error has occurred.  Connection lost.");
            System.out.println("Error:  " + e);
            System.exit(1);
        }

    }  // end main()



} //end class CLChatServer

حيث:

  • [1] يُمثِّل هذا البرنامج أحد طرفي برنامج محادثةٍ بسيط عبر سطر الأوامر، حيث يَعمَل البرنامج كأنه خادم ينتظر طلبات الاتصال من البرنامج CLChatClient. يُمكِن تخصيص رقم المنفذ الذي يستمع إليه الخادم مثل وسيط بسطر الأوامر؛ ويَستخدِم البرنامج في حالة عدم تخصيصه رقم المنفذ الافتراضي المُخصَّص عبر الثابت DEFAULT_PORT. ملاحظة: يستمع الخادم إلى أي رقم منفذٍ متاح، في حالة تخصيص العدد صفر رقمًا للمنفذ. يدعم هذا البرنامج اتصالًا واحد فقط؛ فبمجرد فتح الاتصال، يتوقف مقبس الاستماع، ويُرسِل طرفي الاتصال بعد ذلك رسالةً نصيةً لتحقيق الاتصال إلى بعضهما بعضًا، ليتأكّد كلا الطرفين من أن البرنامج على الطرف الآخر من النوع الصحيح، وفي تلك الحالة، يبدأ البرنامجان المتصلان بتبادل الرسائل. لا بُدّ أن يُرسِل برنامج العميل الرسالة الأولى، وبإمكان المُستخدِم بأيٍّ من الطرفين إغلاق الاتصال بإدخال السلسلة النصية "quit". ملاحظة: لا بُدّ أن يكون المحرف الأول بأي رسالةٍ نصيةٍ مُرسَلة عبر الشبكة مساويًا للقيمة "0" أو "1"، حيث يُفسَّر على أنه أمر.
  • [2] سلسلة نصية لتحقيق الاتصال؛ حيث يرسل طرفي الاتصال تلك الرسالة النصية إلى بعضهما بمجرد فتح الاتصال لنتأكّد من أن الطرف الآخر هو برنامج CLChat.
  • [3] اقرأ رقم المنفذ من سطر الأوامر أو اِستخدِم رقم المنفذ الافتراضي إذا لم يُخصِّصه المُستخدم.
  • [4] انتظر طلب اتصال؛ وعندما يَصِل طلب اتصال، أغلق المستمع، وأنشِئ مجاري تدفق لتبادل البيانات والتحقق من الاتصال.
  • [5] تبادل الرسائل مع الطرف الآخر من الاتصال حتى يُغلِق أحدهما الاتصال. ينتظر لخادم الرسالة الأولى من العميل، ويتبادل بعد ذلك الطرفان الرسائل جيئةً وذهابًا.
  • [6] يعد المِحرف الأول من الرسالة أمرًا. إذا كان الأمر هو إغلاق الاتصال، أغلقه؛ أما إذا لم يَكن كذلك، اِحذِف محرف الأمر من الرسالة وأكمل المعالجة.
  • [7] يرغب المُستخدِم بإغلاق الاتصال. بلِّغ الطرف الآخر وأغلق الاتصال.

يُعدّ هذا البرنامج أكثر متانةً robust نوعًا ما من البرنامج DateServer؛ لأنه يُجرِي تحقيق اتصال handshake ليتأكّد من أن العميل الذي يحاول الاتصال به هو بالفعل البرنامج CLChatClient. يُجرَى تحقيق الاتصال بتبادل بعض المعلومات بين العميل والخادم على أنه جزءٌ من عملية إنشاء الاتصال قبل إرسال أي بياناتٍ فعلية، ويُرسِل طرفا الاتصال في تلك الحالة سلسلةً نصيةً إلى الطرف الآخر لتعريف هويته؛ حيث يُعدّ تحقيق الاتصال جزءًا من بروتوكول إجراء اتصال -أنشأه الكاتب- بين البرنامجين CLChatClient وCLChatServer.

يُعدّ أي بروتوكول توصيفًا مفًصَّلًا لما يُمكِن تبادله من بياناتٍ ورسائلٍ عبر اتصالٍ معين، وكذلك طريقة تمثيل تلك البيانات، وبأي ترتيبٍ ينبغي إرسالها. ويُعدّ تصميم البروتوكول جانبًا مهمًا بتطبيقات الخوادم/العملاء. ينطوي بروتوكول "CLChat" على جانبٍ آخر بالإضافة إلى تحقيق الاتصال؛ حيث يَنُصّ على أن المحرف الأول بأي سطرٍ نصي يُرسَل عبر الاتصال هو أمر. إذا كان المِحرف الأول يُساوِي "0"، فيُمثِّل السطر رسالةً من مُستخدمٍ لآخر؛ أما إذا كان يُساوِي "1"، فسيُشير السطر إلى أن أحدهما قد أدخَل الأمر "quit"، مما يؤدي إلى غلق الاتصال.

اقتباس

ملاحظة: إذا أردت أن تُجرِّب هذا البرنامج على حاسوبٍ واحد، يُمكِنك استخدام نافذتي سطر أوامر. اُكتُب بإحداها الأمر java CLChatServer لتشغيل الخادم، واُكتُب الأمر java CLChatClient localhost بالأخرى للاتصال بالخادم المُشغَّل على نفس الحاسوب. إذا لم يَكُن الخادم مستمِعًا للمَنفَذ الافتراضي، يُمكِنك تخصيص رقم المَنفَذ خيارًا ثانيًا للبرنامج. لاحِظ أنه في حالة عدم تخصيص معلومات الاتصال بسطر الأوامر، فسيَطلُبها منك البرنامج.

ترجمة -بتصرّف- للقسم Section 4: Networking من فصل Chapter 11: Input/Output Streams, Files, and Networking من كتاب Introduction to Programming Using Java.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...