نعلم أن تطبيقك لكي يتمكن من الاتصال بأي جهة عبر الشبكة، يجب عليه أولًا الحصول على Socket. والآن دعونا نرى كيف يتم ذلك.
الخطوة الأولى: إنشاء Socket
عندما يطلب التطبيق من نظام التشغيل (OS): «من فضلك، افتح لي Socket»، فإن هذا الطلب يتضمن عادةً بعض المعلومات التي تحدد كيفية تواصل هذا الـSocket.
أول هذه المعلومات هو Protocol Family، أي ما إذا كان الـSocket سيستخدم IPv4 أو IPv6 أو الاتصال المحلي. والثانية هي Socket Type، أي ما إذا كانت البيانات ستُرسل على شكل Stream مستمر، مثل TCP، أو على شكل رسائل منفصلة، مثل UDP. والثالثة هي الـProtocol نفسه.
في لغة C، يبدو الاستدعاء بالشكل التالي:
socketFileDescriptor = socket(SOCKET_FAMILY, SOCKET_TYPE, PROTOCOL);عندما يستقبل نظام التشغيل هذا الاستدعاء، فإنه ينفّذ عدة خطوات. وهذه الخطوات ليست محور تركيزنا هنا، لذلك ما نحتاج إلى فهمه هو أن نظام التشغيل يقوم بما يلي.
أولًا، يقوم بتخصيص Kernel Object صغير يمثّل Communication Endpoint. يعيش هذا الـObject داخل ذاكرة الـKernel، ويحتوي بشكل أساسي على مجموعة من المعلومات المهمة.
يحدد Socket Type الطريقة التي سيتم بها إرسال البيانات، سواء على شكل Stream أو على شكل Messages. ويستخدم الـKernel هذا النوع لاختيار آلية الاتصال المناسبة، مثل TCP أو UDP.
يحدد Protocol Family تنسيق العناوين، مثل IPv4 أو IPv6، كما يحدد آلية Network Layer التي ينبغي استخدامها.
يحدد Protocol التنفيذ الدقيق للـProtocol الذي سيتم استخدامه. وحتى ضمن Protocol Family نفسها، قد تكون هناك عدة خيارات للبروتوكولات. وهذا أمر نادر، لكنه ممكن.
تحتفظ Internal State بحالة الـSocket الحالية، مثل ما إذا كان Connected أو Unconnected أو Listening.
يحتوي الـSocket أيضًا على Pointers تشير إلى Protocol-Specific Logic. وهذه عبارة عن Function Pointers تشير إلى التنفيذات الفعلية المسؤولة عن التعامل مع الـProtocol الذي تم اختياره.
ثانيًا، ينشئ نظام التشغيل File Descriptor داخل الـProcess الخاص بك، أي داخل التطبيق، وذلك من خلال تخصيص رقم صحيح صغير مثل 3 أو 4 أو 5. هذا الرقم هو في الأساس Index يشير إلى Kernel Socket Object الذي تم إنشاؤه في الخطوة السابقة. ويمكن للـProcess استخدام هذا الـFile Descriptor للإشارة إلى الـSocket في System Calls المستقبلية.
من المهم ملاحظة أنه في هذه اللحظة لا توجد أي IP Addresses محددة، ولا توجد Ports مخصصة، ولا توجد Buffers مخصصة لتخزين البيانات. ويكون الـSocket بشكل افتراضي غير متصل.
ما يمتلكه تطبيقك في هذه المرحلة هو Reference إلى Object تديره الـKernel ويمثّل Communication Endpoint. ومن الصحيح أن نقول إن هذا الـSocket نفسه لا يعرف بعد إلى أين يجب أن يذهب. إنه يشبه ظرفًا فارغًا لم يُكتب عليه أي عنوان بعد.
إذًا، في هذه اللحظة لدينا شيئان: Socket Object تديره الـKernel، وFile Descriptor داخل تطبيقنا يشير إلى هذا الـKernel Object.
لكننا ما زلنا نحاول حل اللغز: كيف يمكن لهذا الـKernel Object الصغير، الذي لا يمكن الوصول إليه داخل تطبيقك إلا من خلال رقم صحيح صغير، أن يوصلك في النهاية إلى الإنترنت بالكامل؟
تأتي الإجابة في الخطوة التالية. ويعتمد System Call المستخدم على دور التطبيق والبروتوكول الذي يعمل به:
connect() عميل TCP: إنشاء اتصال
bind() خادم TCP أو نقطة نهاية UDP: حجز عنوان محلي
sendto() مُرسل UDP: إرسال Datagram إلى وجهة محددةالخطوة الثانية: الاتصال بالـSocket + إرسال البيانات
ما يحدث فعليًا في هذه الخطوة يختلف بناءً على أمرين: هل نحن Client، أي المرسل، أم Server، أي المستقبل؛ وهل نستخدم Connection-Oriented Protocol مثل TCP أم Connectionless Protocol مثل UDP.
سنستعرض ثلاثة مسارات رئيسية.
المسار A: أنت Client
الاتصال بالخادم
ضع في اعتبارك أننا نتحدث هنا عن Connection-Based Protocol مثل TCP. أما مع Connectionless Protocols مثل UDP، فلا توجد حاجة لاستدعاء
connect() ثم send(). وبدلًا من ذلك، تتمثل الطريقة القياسية في استخدام sendto() مباشرةً.
لكي نُنشئ الاتصال، نحتاج إلى بعض المعلومات. أولًا: أي Socket يجب استخدامه؟ ثانيًا: يجب أن نمرر Destination Address، والذي يتضمن IP Address الخاص بالمستقبل وPort Number. ثالثًا: نمرر أيضًا حجم هذا العنوان.
تذكّر أن التطبيق الخاص بك وOS Kernel يعيشان في Memory Spaces منفصلة. لذلك يحتاج الـKernel إلى معرفة عدد الـBytes الذي يجب عليه قراءته بالضبط من ذاكرة التطبيق حتى يحصل على العنوان الكامل: لا أقل ولا أكثر.
في لغة C، يبدو الاستدعاء بالشكل التالي:
connect(sockFileDescriptor, address, sizeof(address));وبذلك يخبر تطبيقك نظام التشغيل: «قم بربط هذا الـSocket بهذا العنوان».
استجابةً لذلك، ينفّذ نظام التشغيل عدة عمليات. أولًا، يتحقق من صحة العنوان، ثم يضيف الـDestination، المكوّن من IP Address وPort Number، إلى Socket Object.
ثانيًا، يحتاج نظام التشغيل إلى معرفة هويتك على الشبكة. بمعنى آخر: «عندما تصلني استجابة، إلى أين يجب أن أرسلها؟»
العثور على IP Address: قد يحتوي جهازك على عدة Network Interfaces، مثل Wi-Fi وEthernet، وقد يكون لكل Interface عنوان IP خاص به. ولحسن الحظ، يحتفظ نظام التشغيل بـKernel Routing Table تحتوي على قواعد مثل: «للوصول إلى Network A، استخدم Interface Y؛ وللوصول إلى Network B، استخدم Interface Z».
في Linux، يمكنك تنفيذ الأمر ip route لعرض جدول التوجيه Routing Table. يبدو الأمر مع مثال على النتيجة كما يلي:
$ ip route
default via 10.234.98.197 dev wlo1 proto dhcp src 10.234.98.27 metric 600
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1يبحث نظام التشغيل عن أفضل Route للوصول إلى Destination IP Address. فإذا كانت الوجهة، على سبيل المثال، هي 93.184.216.34، وهو Public IP Address، فقد يستخدم نظام التشغيل الـDefault Route عبر 10.234.98.197، ويختار 10.234.98.27 بوصفه Source IP Address. وتختلف النتيجة حسب جدول التوجيه والواجهات وحالة الشبكة الحالية.
العثور على الـPort: لا يكفي عنوان IP وحده، لأن جهازك يشغّل العديد من الـProcesses. وبما أنك لم تقم بربط الـSocket بـPort محدد، يختار نظام التشغيل Port من Ephemeral Port Range. في Linux، يتراوح هذا النطاق عادةً بين 32768 و60999.
يختار نظام التشغيل Ephemeral Port متاحًا، على سبيل المثال 54321، ويخصصه للـSocket الخاص بك.
الآن أصبح لدى نظام التشغيل عنوانك المحلي الكامل: 10.234.98.27:54321. ويخزّن هذه المعلومات داخل Socket Object، وبذلك يتم تحديد Local Endpoint الذي سيُستخدم في عملية الاتصال.

