أساسيات الشبكات البرمجيات المستخدمة في بناء الشبكات الحاسوبية


عبد الصمد العماري

تُعدّ معماريات الشبكة وتوصيفات البروتوكول من الأمور الأساسية، لكن المخطط الجيد ليس كافيًا لتفسير النجاح الهائل للإنترنت: لقد زاد عدد أجهزة الحاسوب المتصلة بالإنترنت بصورة هائلة على مدار أكثر من ثلاثة عقود (رغم صعوبة الحصول على أرقام دقيقة). إذ قُدِّر عدد مستخدمي الإنترنت بحوالي 4.5 مليار بنهاية عام 2019، أي ما يفوق نصف سكان العالم.

فما الذي يفسر نجاح الإنترنت؟ هناك بالتأكيد العديد من العوامل المساهمة (بما في ذلك البنية الجيدة)، ولكن هناك شيء واحد أعطى للإنترنت هذا النجاح الهائل، وهو أن الكثير من وظائفها تُوفِّرها البرامج التي تعمل على أجهزة الحاسوب ذات الأغراض العامة. وتكمن أهمية ذلك في إمكانية إضافة وظائف جديدة بسهولة من خلال "مجرد مسألة برمجيّة صغيرة". ونتيجة لذلك، ظهرت التطبيقات والخدمات الجديدة بوتيرة لا تصدق.

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

في السنوات التي تلت ظهور الطبعة الانجليزية الأولى من هذا الكتاب، أصبحت كتابة التطبيقات المتصلة بالشبكة نشاطًا رئيسيًا وليست وظيفة لعددٍ قليل من المتخصصين. وقد لعبت العديد من العوامل دورًا في ذلك، بما في ذلك وجود أدوات أفضل لتسهيل المهمة وفتح أسواق جديدة مثل تطبيقات الهواتف الذكية.

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

واجهة برمجة التطبيقات API (المقابس Sockets)

عندما يتعلّق الأمر بتنفيذ تطبيق شبكي، تكون نقطة البداية هي الواجهة التي تصدّرها الشبكة. ونظرًا لأن معظم بروتوكولات الشبكة موجودة في البرمجيات (خاصة تلك الموجودة في المستوى الأعلى في مجموعة البروتوكولات)، ولأن جميع أنظمة الحاسوب تقريبًا تنفذ بروتوكولات الشبكة كجزءٍ من نظام التشغيل، فعندما نشير إلى الواجهة "التي تصدّرها الشبكة"، فإننا نشير بصفة عامة إلى الواجهة التي يوفّرها نظام التشغيل لنظامه الفرعي الخاصّ بالشبكات. غالبًا ما تسمى هذه الواجهة واجهة برمجة التطبيقات (Application Programming Interface أو اختصارًا API) في الشبكة.

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

قبل توصيف واجهة المقبس، من المهم أن تبقي المسألتين التاليتين منفصلتين في ذهنك. المسألة الأولى أن كل بروتوكول يوفر مجموعة معينة من الخدمات (services)، والثانية أن API توفر صياغة نحوية (syntax) يمكن من خلالها استدعاء هذه الخدمات على نظام حاسوبٍ معين. بعد ذلك يكون التنفيذ مسؤولًا عن ربط (mapping) مجموعة العمليات والكائنات الملموسة المحددة بواسطة واجهة برمجة التطبيقات (API) مع مجموعة الخدمات المجرّدة التي يحددها البروتوكول. إذا أنجزت عملًا جيدًا في تعريف الواجهة، فسيكون من الممكن استخدام الصياغة النحوية للواجهة من أجل استدعاء خدمات العديد من البروتوكولات المختلفة. لقد كان هذا الشّمول بالتأكيد هدفًا رئيسيًا لاستخدام واجهة المقبس رغم أنّه بعيد عن الكمال.

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

تتمثّل الخطوة الأولى في إنشاء مقبس باستخدام العملية التالية:

int socket(int domain, int type, int protocol);

تأخذ هذه العملية ثلاث وسائط لسببٍ مهمٍّ هو أنّ واجهة المقبس مصمَّمة لتكون عامة بما يكفي لدعم أي مجموعة بروتوكولات أساسية. على وجه التحديد، يحدّد الوسيط النطاق domain عائلةَ البروتوكولات التي ستُستخدَم : يشير PF_INET إلى عائلة الإنترنت، ويشير PF_UNIX إلى مجموعة تعليمات يونيكس، ويشير PF_PACKET إلى الوصول المباشر إلى واجهة الشبكة (أي أنه يتجاوز رزمة البروتوكولات TCP/IP). يشير الوسيط النوع type إلى دلالات الاتصال، إذ يستخدم SOCK_STREAM للإشارة إلى تدفق بايتات، أمّا SOCK_DGRAM فهو بديل يشير إلى خدمة موجهة للرسالة، مثل تلك التي يوفرها بروتوكول UDP، وحدّد الوسيط protocol البروتوكولَ المحدد الذي سيُستخدَم هنا. في حالتنا هذه، سيكون الوسيط هو UNSPEC لأن الجمع بين PF_INET و SOCK_STREAM يعني TCP. أخيرًا، تكون القيمة المُعادة من socket مِقبضًا (handle) للمقبس الجديد الذي أُنشِئ، أي معرّفًا يمكننا من خلاله الرجوع إلى المقبس في المستقبل، ويمكن إعطاؤه كوسيط للعمليات اللاحقة على هذا المقبس.

تعتمد الخطوة التالية على ما إذا كان الجهاز عميلًا أو خادومًا. على جهاز الخادوم، تؤدي عملية التطبيق إلى فتحٍ سلبيٍّ (passive open)، بمعنى أنّ الخادوم يصرّح باستعداده لقبول الاتصالات، ولكنه لا ينشئ اتصالًا فعليًا. يفعل الخادوم ذلك باستدعاء العمليات الثلاث التالية:

int bind(int socket, struct sockaddr *address, int addr_len);
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr *address, int *addr_len);

تربط عملية الربط bind، كما يوحي اسمها، على ربط المقبس الجديد الذي أُنشِئ بالعنوان address المحدد. هذا هو عنوان الشبكة الخاصّ بالطرف المحلي أي الخادوم. لاحظ أنه عند استخدامه في بروتوكولات الإنترنت، يكون وسيط العنوان address عبارةً عن بنية بيانات يتضمن عنوان IP الخاص بالخادوم ورقم منفذ TCP. تُستخدَم المنافذ لتحديد العمليّات بطريقة غير مباشرة، فهي شكلٌ من أشكال مفاتيح demux. ويكون رقم المنفذ في العادة عبارةً عن بعض الأرقام التي يُعرَف جيّدًا أنّها خاصّة بالخدمة المقدمة؛ على سبيل المثال، تقبل خواديم الويب عادةً الاتصالات على المنفذ 80.

تحدّد عملية الاستماع listen بعد ذلك عدد الاتصالات التي يمكن أن تكون معلّقةً على المقبس المحدّد. وأخيرًا، تقوم وظيفة القبول accept بالفتح السلبي. إنها عمليةٌ حاجزة (blocking operation) لا تُرجِع أي قيمة إلا بعد أن يُنشِئ طرفٌ بعيدٌ اتصالًا، وعندما يكون الأمر كذلك، فهي تُرجِع مقبسًا جديدًا يتوافق مع هذا الاتصال الذي أُنشِئ لتوّه، ويتضمّن وسيط العنوان address عنوان طرف الاتصال البعيد. لاحظ أنه عندما تُرجِع العملية accept، يكون المقبس الأصلي الذي قُدّم كوسيط مازال موجودًا وما زال يتوافق مع الفتح السلبي؛ إنّه يُستخدَم في الاستدعاءات المستقبلية للعملية accept. تؤدي عملية التطبيق إلى فتحٍ نشط على جهاز العميل؛ أي أنّ العميل يصرّح بمن يريد التواصل معه باستدعاء العملية التالية:

int connect(int socket, struct sockaddr *address, int addr_len);

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

بمجرد إنشاء الاتصال، تستدعي عمليات التطبيق العمليتين التاليتين لإرسال البيانات وتلقيها:

int send(int socket, char *message, int msg_len, int flags);
int recv(int socket, char *buffer, int buf_len, int flags);

ترسل العملية الأولى الرسالة message المحدّدة عبر المقبس socket المحدد، بينما تتلقى العملية الثانية رسالةً من المقبس socket المحدد في المخزَن المؤقت buffer المحدد. تأخذ كلتا العمليتين مجموعةً من الرايات flags التي تتحكم في تفاصيل معيّنة للعملية.

ثورة التطبيقات المدعَّمة بالمقابس

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

مثال تطبيقي

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

طرف العميل (Client)

نبدأ مع جانب العميل، الذي يأخذ اسم الجهاز البعيد كوسيط. ثم يستدعي أداة لينكس لترجمة هذا الاسم إلى عنوان IP للمضيف البعيد. الخطوة التالية هي إنشاء بنية بيانات العنوان (sin) الذي تترقّبه واجهة المقبس. لاحظ أن بنية البيانات هذه تحدّد أننا سنستخدم المقبس للاتصال بالإنترنت (AF_INET). في مثالنا، نستخدم منفذ TCP ذي الرّقم 5432 على أنّه منفذ الخادوم المعروف؛ هذا يعني أنّه منفذٌ لم يُعيّن لأي خدمة إنترنت أخرى. الخطوة الأخيرة في إعداد الاتصال هي استدعاء العملتيين socket و connect. عندما تعيد العملية، يُؤسَّس الاتصال مباشرةً بعد ذلك ويَدخل برنامج العميل في حلقته الرئيسية (main loop)، والتي تقرأ النص من المدخل الاعتيادي وترسله عبر المقبس.

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>

#define SERVER_PORT 5432
#define MAX_LINE 256

int
main(int argc, char * argv[])
{
FILE *fp;
struct hostent *hp;
struct sockaddr_in sin;
char *host;
char buf[MAX_LINE];
int s;
int len;

if (argc==2) {
host = argv[1];
}
else {
fprintf(stderr, "usage: simplex-talk host\n");
exit(1);
}

/* ترجمة اسم المضيف إلى عنوان أي بي النظير */    
hp = gethostbyname(host);
if (!hp) {
fprintf(stderr, "simplex-talk: unknown host: %s\n", host);
exit(1);
}

/* بناء بينة بيانات العنوان */
bzero((char *)&sin, sizeof(sin));
sin.sin_family = AF_INET;
bcopy(hp->h_addr, (char *)&sin.sin_addr, hp->h_length);
sin.sin_port = htons(SERVER_PORT);

/* active open */
if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
perror("simplex-talk: socket");
exit(1);
}
if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("simplex-talk: connect");
close(s);
exit(1);
}
/* الحلقة الرئيسية: الحصول على سطور من النص وإرسالها */
while (fgets(buf, sizeof(buf), stdin)) {
buf[MAX_LINE-1] = '\0';
len = strlen(buf) + 1;
send(s, buf, len, 0);
}
}

طرف الخادوم (Server)

يجري الأمر كذلك في الخادوم بنفس البساطة. فهو يُنشِئ أولًا بنية بيانات العنوان عبر تحديد رقم المنفذ الخاص به (SERVER_PORT). وبما أنّه لا يُحدّد عنوان IP، فإن برنامج التطبيق يكون على استعدادٍ لقبول الاتصالات على أيّ عنوان IP للمضيف المحلي. بعد ذلك، يُجري الخادوم الخطوات الأولية التي يتضمّنها الفتح السلبي؛ إذ يُنشِئ المقبس، ويربطه بالعنوان المحلّي، ويُعيّن الحد الأقصى لعدد الاتصالات المعلّقة المسموح بها. وأخيرًا، تترقّب الحلقة الرئيسية محاولة الاتصال من مضيف بعيدٍ، وعندما يتمّ ذلك، فهي تتلقى الحروف التي تصل عند الاتصال وتطبعها:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>

#define SERVER_PORT  5432
#define MAX_PENDING  5
#define MAX_LINE     256

int
main()
{
struct sockaddr_in sin;
char buf[MAX_LINE];
int buf_len, addr_len;
int s, new_s;

/* بناء بنية بيانات العنوان */
bzero((char *)&sin, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(SERVER_PORT);

/* إعداد الفتح السلبي */
if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
perror("simplex-talk: socket");
exit(1);
}
if ((bind(s, (struct sockaddr *)&sin, sizeof(sin))) < 0) {
perror("simplex-talk: bind");
exit(1);
}
listen(s, MAX_PENDING);

/* انتظر الاتصال، ثم استقبل النص اطبعه */
while(1) {
if ((new_s = accept(s, (struct sockaddr *)&sin, &addr_len)) < 0) {
perror("simplex-talk: accept");
exit(1);
}
while (buf_len = recv(new_s, buf, sizeof(buf), 0))
fputs(buf, stdout);
close(new_s);
}
}

ترجمة -وبتصرّف- للقسم Software من فصل Foundation من كتاب Computer Networks: A Systems Approach





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن