الـ Interfaces والـ Sockets: الشبكة من الـ Hardware إلى الـ Software
مقدمة
كتبت مقدمة عن الشبكات وطبقات الـ OSI Model في (مقدمة في شبكات الحاسوب - Computer Networking)، في هذا المقال سأبدأ بالشرح ابتداء من الـ Physical Layer، وصولا الى كتابة كود لارسال و استقبال البيانات عبر الشبكة.
الأوامر والأمثلة الموجودة في المقال تم تنفيذها على جهاز يعمل بنظام Ubuntu، يفترض أن تعمل بنفس الطريقة على أي جهاز يعمل بـ Linux، قد تجد بعض الاختلافات البسيطة في حال تنفيذ الأوامر على جهاز يعمل بـ MacOS.
NIC - Network Interface Card
ذكرت في المقال السابق أن الـ Physical Medium هو الوسيط الذي يتم ربط الاجهزة عن طريقه (أسلاك كهربائية، أسلاك نقل الألياف الضوئية، اشارات لاسلكية…)، لكن لا بد من وجود قطعة متصلة بجهاز الحاسوب للتعامل مع الـ Physical Medium، فهي القطعة المسؤولة عن إرسال واستقبال الإشارات اللاسلكية، او القطعة التي تحتوي على المنفذ الخاص بسلك الشبكة.
يحتوي الحاسوب على Network Interface Card (NIC)، وهي القطعة المسؤولة عن استقبال البيانات من الـ Physical Medium أو إرسال البيانات عبره، وقد يحتوي الحاسوب على أكثر من NIC في نفس الوقت.
الصورة السابقة توضح مثالا على NIC يتصل بالشبكة عن طريق منفذ Ethernet، هذا ليس إلا شكلا واحدا من الـ NIC، قد تكون الـ NIC قطعة مثبتة على اللوحة الرئيسية (اللوحة الأم) للحاسوب أو الهاتف “الذكي” أو موصولة بمنفذ USB.
يتعامل الـ NIC مع الـ Physical Medium لذلك يعّد جزءا من الـ Physical Layer، لكن للـ NIC عنوان MAC Address، والتعامل مع الـ Mac Address كما ذكرنا في المقال السابق من ضمن مسؤوليات الـ Data-Link Layer.
تصنيف الـ NIC ضمن أكثر من طبقة من طبقات الـ OSI Model يوضح ما ذكرناه في المقال السابق عن امكانية اختلاف التصميم (Design) عن التنفيذ (Implementation) وعدم انطباقه على الواقع بنسبة 100%
نفذ الأمر التالي لمعرفة الـ NICs الموجودة في جهازك:
sudo lshw -class network -short
هذه النتيجة على الجهاز الذي استخدمه:
H/W path Device Class Description
========================================================
/0/100/1c/0 enp1s0 network RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller
/0/100/1c.5/0 wlp2s0 network Dual Band Wireless-AC 3165 Plus Bluetooth
الـ NIC الموجود في السطر الاول مسؤول عن الـ Ethernet، بينما الثاني خاص بالشبكات اللاسلكية ويدعم Bluetooth و WiFi.
كيف يتعامل نظام التشغيل مع الـ NIC
لكل قطعة مرتبطة بجهاز الحاسوب Device Driver (برنامج تعريف) للتعامل معها، الـ Driver يتواصل مع نظام التشغيل عن طريق الـ APIs التي توفرها نواة نظام التشغيل (Kernel).
في Unix والأنظمة الشبيهة به، توفر الـ Kernel واجهة interface للتعامل مع الشبكات، في الـ Output الخاص بالأمر الذي نفذناه لمعرفة الـ NICs الموجودة في الجهاز، ستجد العمود Device الذي يمثل اسم الجهاز في نظام التشغيل:
H/W path Device Class Description
========================================================
/0/100/1c/0 enp1s0 network RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller
/0/100/1c.5/0 wlp2s0 network Dual Band Wireless-AC 3165 Plus Bluetooth
enp1s0
و wlp2s0
هي أسماء تعطيها الـ Kernel للـ NICs وتسمى Network Interface، يمكننا من خلال هذه الأسماء التعامل مع الشبكة وتنفيذ بعض الأوامر، الـ Network Interface هي تمثيل للـ NIC على مستوى الـ Software.
الأمر التالي مثلا يستعرض الـ MAC Address إضافة إلى بعض المعلومات الأخرى عن الـ Interface المرتبة بـ NIC الـ Wi-Fi على جهازي:
ip link show wlp2s0
الـ Internet Protocol والـ Interface
ما شرحناه سابقا منذ بداية المقال متعلق بالتفاعل بين الـ Physical Layer والـ Data-Link Layer، وإدارة الـ Kernel لذلك عن طريق الـ Network Interface.
بمعرفتك الآن أن تفاعل الـ Software مع الشبكة يتم عن طريق الـ Network Interface، قد تتساءل عن بروتوكول IP وما بني فوقه مثل TCP و UDP؟ لن أفصل في الـ Protocols في هذا المقال، سأكتفي حاليا بالإشارة إلى علاقة الـ IP Protocol بالـ Network Interface.
تعد “العنونة” Addressing من الوظائف الرئيسية لبروتوكول IP حيث يدير البروتوكول العناوين (IP Addresses) والتي يتم استخدامها لاحقا لعملية التوجيه (Routing).
يتم إسناد عناوين الـ IP للـ Network Interfaces، تعدد العناوين لنفس الـ Interface ممكن، تُسند العناوين تلقائيا باستخدام DHCP التي يستخدمها الـ Router الذي تستخدمه غالبا أو تُسند يدويا.
الجهاز المستخدم للتجربة متصل حاليا عن طريق الـ WiFi، الأمر التالي يمكنني من معرفة الـ IP Address المسندة للـ Network Interface
ip addr show wlp2s0
يمكنك رؤية وجود عنوان IPv4 وعنوانَي IPv6.
يوجد Caddy Server مثبت على الجهاز بغرض التجربة، الصورة التالية هي من متصفح على جهاز آخر متصل بنفس الشبكة:
سأضيف IP Address آخر بشكل يدوي:
sudo ip addr add dev wlp2s0 192.168.1.50
تمت إضافة الـ IP Address الجديد دون خسارة الـ IP Address السابق، وهو يعمل دون مشاكل:
سأحاول إضافة IP Address مختلف هذه المرة:
sudo ip addr add dev wlp2s0 192.168.3.3
تمت إضافته بنجاح، لكن الاتصال يفشل:
سبب ذلك أنني متصل بالـ Router و الـ Router في هذه الحالة هو الـ Gateway أو المسؤول الفعلي عن إدارة الاتصال، بالنظر إلى إعدادات الـ Router الخاص بي، وجدت أن IP Address للـ Router هو 192.168.1.1
والـ Subnet Mask هو 255.255.255.0
، الـ IP الذي تمت إضافته لا ينتمي إلى الـ Subnet، كان بالإمكان أن يعمل لو كان الـ Subnet Mask هو 255.255.0.0
.
Virtual Network Interfaces
الـ Network Interfaces التي استعرضناها في القسم السابق تقدم تمثيلا على شكل Software للـ NICs الموجودة في الجهاز، قد تكون الـ Network Interface افتراضية أيضا (Virtual).
استخدمنا سابقا lshw
الذي يعرض الـ Hardware المرتبط بالجهاز، لكن لو استخدمنا أمرا يعرض الـ Interfaces سترى أن بعضها غير مرتبط بجهاز، يمكن استعراضها عن طريق الأمر ip link
:
ip link
الـ Virtual Interfaces مفيدة في بعض أنواع البرامج، فبرامج الـ VPN مثلا تضيف Interfaces يمر الاتصال عن طريقها، فيمكنك إرسال البيانات عبر اتصال VPN المشفر، او إرسالها مباشرة دون الحاجة إلى الـ VPN حتى لو لم تغلق برنامج الـ VPN.
برامج الـ Virtualization التي تساعدك على تشغيل أجهزة وهمية Virtual Machines تضيف Interfaces لعمل Interfaces خاصة بكل VM، او انشاء شبكات وهمية بين الـ VMs.
اذا كنت مهتما بأنواع الـ Network Interfaces أو برمجة انظمة تتعامل معها يمكنك مراجعة المقالات التالية:
- مقال يشرح بشكل مختصر بعض أنواع الـ interfaces
- مقال يشرح طريقة إضافة Interface من نوع tunnel مع كتابة برنامج بلغة C لقراءة الـ Packets منها
- مثال جميل بـ Python لـ Process تُنشئ tunnel interface وتطبع الـ Packets المرسلة والمستقبلة
Loopback Interface
توجد Interface باسم lo
في الصورة السابقة، هذه الـ Interface تسمى Loopback Interface وهي Interface مميزة في نظام التشغيل.
الـ Interface المرتبطة بـ NIC مثل wlp2s0
تستقبل البيانات من الـ NIC أو ترسلها عبره، بينما ترسل الـ Loopback Interface أي بيانات تستقبلها ثانية إلى نفس الجهاز كما في الصورة:
الـ Loopback Interface مفيدة لإنشاء اتصال شبكة بين أكثر من Process (أو نفس الـ Process) على نفس الجهاز، وهذا ما يحصل عند انشاء اتصال واستخدام localhost
أو 127.0.0.1
كـ Host، عند تنفيذ الأمر ip addr show lo
يمكن أن نرى 127.0.0.1
كـ IP Address لهذه الـ Interface:
الـ Socket وتعامل المبرمج مع الشبكات
يوفر نظام التشغيل APIs حتى تتعامل البرامج في الـ Application Layer مع الشبكة، سأشرح بشكل عملي قدر الامكان في هذا القسم من المقال، سأستخدم لغة Go في الأمثلة، لكن المفاهيم نفسها بغض النظر عن اللغة المستخدمة.
الأمثلة الموجودة بلغة Go لن تكون مكتوبة باستخدام net package، بل سأكتبها باستخدام sys/unix package، في معظم الحالات يفضل استخدام net
، لكنني ساستخدم sys/unix
لغايات الشرح، لأن أسماء الـ Functions في sys/unix
تطابق الـ Syscalls الموجودة في نظام التشغيل، مما يسهل شرح المفاهيم أكثر.
التعامل مع الـ File System او الشبكات أو إدارة الـ Processes والـ Threads هي مسؤولية نظام التشغيل، الـ Syscalls هي عبارة عن API يوفره نظام التشغيل للتعامل معها.
أي اتصال عبر الشبكة يتم عن طريق استخدام ()socket
1، الـ Socket هي الطريقة التي يوفرها نظام التشغيل للتعامل مع الشبكة.
عند تنفيذ socket()
Function يُنشئ نظام التشغيل File Descriptor، الـ File Descriptor عبارة عن رقم مرجعي (Reference) لملف في نظام التشغيل، هذا الملف يكون مسؤولا عن الـ Socket من ناحية ارسال واستقبال البيانات.
الملف الخاص بالـ Socket والذي يمثله الـ File Descriptor ليس ملفا فعليا مخزنا على الـ Disk بالضرورة، قد تكون مجموعة من القيم المخزنة في الـ Memory، والـ File Descriptor هو المرجع المستخدم للإشارة إلى هذه القيم.
هذا مثال بسيط على انشاء Socket والاتصال بسيرفر TCP وارسال HTTP Request:
1package main
2
3import (
4 "fmt"
5 "log"
6
7 "golang.org/x/sys/unix"
8)
9
10func main() {
11 // Create a TCP socket and get the file descriptor
12 fileDescriptor, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, unix.IPPROTO_TCP)
13 if err != nil {
14 log.Fatal("Could not create socket: ", err)
15 }
16
17 // Will be executed after return.
18 // It should cleanup the memory
19 defer unix.Close(fileDescriptor)
20
21 // Connect to the TCP server
22 err = unix.Connect(fileDescriptor, &unix.SockaddrInet4{
23 Addr: [4]byte{127, 0, 0, 1},
24 Port: 3000,
25 })
26 if err != nil {
27 log.Fatal("Connection error: ", err)
28 }
29
30 // Send data over socket
31 unix.Write(fileDescriptor, []byte("GET / HTTP/1.0\r\n\r\n"))
32
33 // Read until there is no more data
34 for {
35 msg := make([]byte, 1024)
36 n, _, err := unix.Recvfrom(fileDescriptor, msg, 0)
37
38 if err != nil {
39 log.Fatal(err)
40 }
41
42 if n == 0 {
43 fmt.Println("Done")
44 return
45 }
46
47 fmt.Println(string(msg))
48 }
49}
الكود السابق ينشئ TCP Socket، مما سيعطينا الـ file descriptor وهو عبارة عن Integer، لاحظ أن كل Function يتم استدعاؤها للاتصال او ارسال واستقبال البيانات نمرر لها الـ file descriptor.
الهدف من هذا الكود تعليمي فقط، توجد العديد من الحالات التي لم اتعامل معها، إضافة إلى أن التعامل مع الأخطاء بطباعتها واغلاق البرنامج فقط لا يكفي. لغة Go تتعامل مع الأخطاء كقيم (Values).
هذه نتيجة تنفيذ الكود السابق، مع العلم انني شغلت HTTP Server على نفس الجهاز على البورت 3000:
الـ Interface المستخدمة هي Loopback بما أن الـ Server والـ Client على نفس الجهاز، لا نحدد الـ Interface مباشرة ضمن الكود، لكن الـ Kernel تحدد الـ Interface التي يجب استخدامها بناء على عنوان الـ IP الموجود في الكود إضافة إلى الـ Routing Tables & Routing Rules.
الـ Routing Tables هي عبارة عن مجموعة من القواعد التي تستخدمها الـ Kernel لتحديد الـ Interface التي يجب استخدامها، يتم ذلك بناء على عدة عوامل منها الـ IP Address، وبعض القواعد المعرفة ضمن الـ Routing Tables.
في مثالنا في الأعلى استخدمنا العنوان 127.0.0.1
، وبما أن هذا العنوان مرتبط بالـ Loopback Interface يتم استخدامها.
عند عمل Server يتم عمل bind()
2 على IP Address محدد، عند استخدام 127.0.0.1
فهذا يعني أن الـ Server الخاص بك لن يتم الوصول إليه أبدا من خارج الجهاز، أما اذا استخدمت الـ Private IP الموجود ضمن شبكتك، يمكن للاجهزة في نفس الـ Subnet فقط الوصول الى السيرفر.
قد تحتاج أيضا لاستخدام 0.0.0.0
كـ IP Address، وهو IP Address خاص، استخدامه يعني ابلاغ الـ Kernel بقراءة الـ Packets من جميع الـ Interfaces.
خاتمة
الهدف من هذا المقال توضيح بعض المفاهيم والمصطلحات فقط، فهمك للـ Network Interfaces والـ Sockets مهم جدا، لكن غالبا لن يتعامل أغلب المبرمجين معها بشكل مباشر، إذا كنت مهتما ببرمجة الشبكات والـ Interfaces والـ Drivers، يمكنك تعلم تفاصيل أكثر حول الـ Interfaces وأنواعها بدءا من المقالات التي ذكرتها في الأعلى.
استفدت من مقال From Sockets to NIC: A Big Picture كثيرا عند كتابتي لهذا المقال.
يمكنك أيضا الرجوع إلى كتاب Linux Device Drivers الذي يتحدث عن برمجة الـ Driver في أنظمة Linux، الفصل السابع عشر تحديدا يتحدث عن الـ Network Drivers.