المقابس: عميل وخادم TCP
المقابس: عميل وخادم TCP
يُعدّ TCP العمود الفقري للتواصل الموثوق عبر الإنترنت: فهو يضمن التسليم والترتيب واكتشاف الأخطاء. في Java، تُعرّض حزمة java.net بروتوكول TCP من خلال فئتين — Socket (الجانب العميل) وServerSocket (المستمع). إنّ فهم كيفية تعاونهما، وكيف تتدفق البيانات عبر المجاري التي يوفّرانها، أمرٌ أساسي لكل شيء بدءًا من بناء بروتوكول خاص وصولًا إلى فهم كيفية عمل HTTP وبرامج تشغيل قواعد البيانات في العمق.
كيف يعمل TCP في فقرة واحدة
اتصال TCP هو قناة بيانات ثنائية الاتجاه بين طرفين (عنوان IP + رقم منفذ). يربط الخادم منفذًا ويستمع إليه. يبدأ العميل إجراء المصافحة الثلاثية (three-way handshake). عند اكتمال المصافحة، يملك الطرفان مقبسًا متصلًا يمكنهما القراءة منه والكتابة إليه بشكل مستقل. تُسلَّم البيانات بالترتيب؛ ويقوم نظام التشغيل بتخزين القطع مؤقتًا وإعادة إرسال المفقود منها بشكل شفاف. هذه الموثوقية هي ما يُميّز TCP عن UDP.
ServerSocket: الجانب المستمع
تملك ServerSocket منفذًا وتتوقف عند accept() إلى أن يتصل عميل. تُعيد كل استدعاء لـ accept() كائن Socket جديدًا يمثّل ذلك العميل تحديدًا — بينما يستمر مقبس الخادم في الاستماع. النمط الكلاسيكي هو تمرير كل مقبس مقبول إلى خيط تنفيذ أو منفّذ (executor) حتى يتمكن الخادم من معالجة عملاء متعددين في آنٍ واحد.
Executors.newVirtualThreadPerTaskExecutor() خيط افتراضي خفيف الوزن لكل عميل بتكلفة شبه معدومة — دون الحاجة لضبط حجم مجموعة الخيوط. للإصدارات Java 17/19 استخدم Executors.newCachedThreadPool() بدلًا من ذلك.
Socket: الجانب العميل
يبني العميل كائن Socket مع اسم مضيف الخادم ورقم المنفذ. يُنفّذ المُنشئ نفسه إجراء المصافحة الثلاثية؛ إن لم يكن الخادم متاحًا أو كان المنفذ مغلقًا، فسيُطلق استثناء ConnectException. بعد الإنشاء، تجري القراءة والكتابة عبر InputStream/OutputStream، تمامًا كأي عملية I/O أخرى في Java.
تغليف المجاري: لماذا يهم
تُعيد socket.getInputStream() كائن InputStream خامًا. قراءة البايتات الخام مباشرةً أمرٌ شاق وعُرضة للأخطاء في البروتوكولات النصية. إضافة طبقات التغليف يوفر إطار البيانات ومعالجة مجموعة المحارف:
InputStreamReader— يحوّل البايتات إلى محارف باستخدام مجموعة محارف محددة (دائمًا حدّدStandardCharsets.UTF_8صراحةً).BufferedReader— يضيف التخزين المؤقت والدالة الحيويةreadLine()التي تقرأ حتى\nأو\r\n.PrintWriter(out, true)— المعامل الثانيtrueيُفعّل التدفق الفوري بعد كلprintln(). بدونه تظل البيانات في المخزن المؤقت ولا يستقبلها الطرف الآخر قط.
PrintWriter بدون تدفق فوري تلقائي، أو OutputStream بدون استدعاء flush()، تظل البيانات في مخزن الإرسال الخاص بنظام التشغيل ولا يستقبلها الخادم أبدًا — يظل البرنامج معلقًا ينتظر إلى الأبد.
إدارة الموارد والإغلاق الجزئي
يحتفظ كائن Socket بمعرّف ملف (file descriptor). أغلقه دائمًا في كتلة finally أو الأفضل — استخدم try-with-resources. إغلاق المقبس يُرسل طلب TCP FIN، مُشيرًا للطرف الآخر بأنه لن تُرسل مزيد من البيانات. عندها تُعيد readLine() للطرف الآخر null، مما يتيح له الخروج من حلقة القراءة بشكل نظيف.
يمكنك أيضًا الإغلاق الجزئي: استدع socket.shutdownOutput() للإشارة إلى انتهاء الكتابة مع إبقاء القراءة ممكنة. هكذا يُشير عملاء HTTP/1.0 إلى نهاية الطلب دون قطع الاتصال.
المهلات الزمنية: ضرورة في كود الإنتاج
يتوقف Socket افتراضيًا إلى أجل غير مسمى عند connect() وread(). خادمٌ يتوقف عن الاستجابة سيُجمّد خيطك إلى الأبد. دائمًا اضبط المهل الزمنية:
- مهلة الاتصال: استخدم التحميل الزائد ذا المعاملين لـ
connect()بدلًا من المُنشئ. - مهلة القراءة (SO_TIMEOUT):
socket.setSoTimeout(millis)— تُطلقSocketTimeoutExceptionإن توقفت قراءة أطول من الحد المضبوط.
الطابور الاحتياطي (Backlog): قائمة انتظار القبول
يتحكّم المعامل الثاني لـ new ServerSocket(port, backlog) في عدد الاتصالات التي لم يُقبلها البرنامج بعد والتي يُخزّنها نظام التشغيل في قائمة الانتظار. إن كانت حلقة القبول بطيئة وامتلأت القائمة، تُرفض محاولات الاتصال الجديدة برسالة "connection refused". القيمة الافتراضية 50، وهي كافية للخوادم ذات الحركة المنخفضة لكنها صغيرة جدًا للسيناريوهات عالية الإنتاجية.
المقايضات الرئيسية في لمحة سريعة
- خيط واحد لكل عميل مقابل NIO (غير متزامن): الخيوط الافتراضية تجعل نهج خيط واحد لكل عميل عمليًا مجددًا لمعظم الخوادم؛ أما الخوادم المبنية على
Selectorمن NIO فلا تزال أفضل حين يكون لديك مئات الآلاف من الاتصالات الطويلة الخاملة (مثل خوادم الدردشة). - البروتوكولات النصية مقابل الثنائية: تناسب
BufferedReader/PrintWriterالنصوص المقسّمة بالأسطر. للبروتوكولات الثنائية أو ذات الطول المسبق استخدمDataInputStream/DataOutputStreamأوObjectInputStream/ObjectOutputStream. - المقابس العادية مقابل
SSLSocket: لا ترسل بيانات حساسة عبر مقبس عادي. غلّفه بـSSLSocketFactoryللحصول على TLS — الواجهة البرمجية ذاتها مع مصافحة إضافية.
telnet localhost 9000 أو nc localhost 9000 يتيح لك كتابة أسطر ورؤية الردود فورًا، مما يُسرّع تصحيح الأخطاء بشكل كبير.
الخلاصة
تربط ServerSocket منفذًا؛ وتُعيد كل استدعاء لـ accept() كائن Socket لعميل واحد. غلّف مجاري المقبس بـ BufferedReader/PrintWriter للبروتوكولات النصية، دائمًا حدّد UTF-8، وفعّل دائمًا التدفق الفوري التلقائي. مرّر كل مقبس مقبول إلى خيط افتراضي أو منفّذ لتحقيق التزامن. اضبط مهل الاتصال والقراءة في كل عميل للإنتاج. أغلق المقابس بـ try-with-resources لضمان انتشار TCP FIN بشكل نظيف. هذه الأساسيات تدعم كل مكتبة شبكة Java عالية المستوى ستستخدمها على الإطلاق.