البرمجة

برمجة خادم عميل بـ C++

أمثلة على التعامل مع خادم-عميل (Client-Server) باستعمال لغة ++C

إن نموذج الخادم-العميل (Client-Server) يُعد من أكثر نماذج الشبكات استخدامًا في تطوير التطبيقات الموزعة وأنظمة الاتصال الحديثة. يستند هذا النموذج إلى مبدأ وجود طرفين رئيسيين: الخادم (Server) الذي ينتظر الطلبات ويعالجها، والعميل (Client) الذي يرسل الطلبات للحصول على خدمات أو بيانات معينة. يعتبر التعامل مع هذا النموذج بلغة ++C من المواضيع الأساسية في برمجة الشبكات، نظرًا لقدرة اللغة على التعامل المنخفض المستوى مع الموارد، وخصوصًا مع المقابس (Sockets) والبروتوكولات.

يتطلب التعامل مع نموذج الخادم-العميل فهمًا جيدًا لواجهات برمجة التطبيقات (API) الخاصة بالنظام، مثل مكتبة Sockets في أنظمة UNIX أو Winsock في أنظمة Windows. في هذا المقال الموسع، سنقوم بعرض أمثلة عملية توضح كيفية بناء خادم وعميل باستخدام لغة ++C، مع تغطية تفصيلية لكافة المفاهيم والعمليات اللازمة لإنشاء اتصال فعال بين الطرفين.


المفاهيم الأساسية لنموذج Client-Server

قبل الغوص في التفاصيل البرمجية، من الضروري استعراض بعض المفاهيم الأساسية التي تحكم هذا النموذج:

  • الخادم (Server): هو البرنامج أو الوحدة البرمجية التي تستمع للطلبات من عملاء متعددين، ويقوم بمعالجتها وتقديم الاستجابة المناسبة.

  • العميل (Client): هو البرنامج الذي يبدأ الاتصال بالخادم لطلب خدمة محددة.

  • المقبس (Socket): هو نقطة النهاية في الاتصال بين العميل والخادم، ويُستخدم لإرسال واستقبال البيانات.

  • العنوان والبورت (IP Address and Port): لتحديد الاتصال بشكل فريد بين العميل والخادم.

  • البروتوكول (Protocol): مثل TCP أو UDP، ويُستخدم لتنظيم نقل البيانات.


مكتبة Sockets في C++

على الرغم من أن لغة ++C لا توفر دعماً مباشراً للشبكات في مكتبتها القياسية، إلا أنه يمكن الاستفادة من مكتبات النظام، مثل:

  • , , , و في أنظمة Linux وUNIX.

  • و في Windows.


إعداد بيئة التطوير

لإنشاء تطبيق Client-Server باستخدام ++C، يجب التأكد من الأمور التالية:

  • استخدام مترجم يدعم مكتبات النظام مثل GCC أو MSVC.

  • تشغيل التطبيق مع الصلاحيات المناسبة (خصوصًا الخادم الذي يفتح المنافذ).

  • التأكد من أن جدار الحماية يسمح بمرور البيانات عبر المنفذ المستخدم.


مثال عملي: خادم بسيط باستخدام بروتوكول TCP في ++C (أنظمة UNIX)

كود الخادم (TCP Server):

cpp
#include #include #include #include #define PORT 8080 #define BUFFER_SIZE 1024 int main() { int server_fd, new_socket; struct sockaddr_in address; int opt = 1; int addrlen = sizeof(address); char buffer[BUFFER_SIZE] = {0}; // إنشاء المقبس server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == 0) { perror("فشل في إنشاء المقبس"); exit(EXIT_FAILURE); } // إعداد الخيارات setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // إعداد عنوان الخادم address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); // ربط العنوان بالمقبس if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("فشل في الربط"); exit(EXIT_FAILURE); } // الاستماع لطلبات الاتصال if (listen(server_fd, 3) < 0) { perror("فشل في الاستماع"); exit(EXIT_FAILURE); } std::cout << "الخادم في وضع الاستعداد...\n"; // قبول الاتصال من العميل new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen); if (new_socket < 0) { perror("فشل في القبول"); exit(EXIT_FAILURE); } // استقبال الرسالة read(new_socket, buffer, BUFFER_SIZE); std::cout << "الرسالة المستلمة: " << buffer << std::endl; // إرسال الرد const char* reply = "تم استلام رسالتك"; send(new_socket, reply, strlen(reply), 0); close(new_socket); close(server_fd); return 0; }

كود العميل (TCP Client):

cpp
#include #include #include #include #define PORT 8080 #define BUFFER_SIZE 1024 int main() { int sock = 0; struct sockaddr_in serv_addr; char buffer[BUFFER_SIZE] = {0}; // إنشاء مقبس sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { std::cerr << "فشل في إنشاء المقبس\n"; return -1; } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); // تحويل عنوان IP من النص إلى البنية if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) { std::cerr << "عنوان غير صالح\n"; return -1; } // الاتصال بالخادم if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { std::cerr << "فشل في الاتصال\n"; return -1; } const char* message = "مرحباً من العميل"; send(sock, message, strlen(message), 0); read(sock, buffer, BUFFER_SIZE); std::cout << "رد الخادم: " << buffer << std::endl; close(sock); return 0; }

تحليل معمق لخطوات الاتصال

1. إنشاء المقبس (Socket)

تُستخدم الدالة socket() لإنشاء نقطة اتصال. تتطلب ثلاثة معاملات:

  • العائلة (AF_INET لـ IPv4).

  • نوع الاتصال (SOCK_STREAM لـ TCP).

  • البروتوكول (0 لاختيار الافتراضي المناسب).

2. ربط المقبس بعنوان (Bind)

بالنسبة للخادم، يجب ربط المقبس بعنوان IP ورقم منفذ باستخدام bind().

3. الاستماع للطلبات (Listen)

يستدعي الخادم الدالة listen() لتحديد عدد الاتصالات المنتظرة في قائمة الانتظار.

4. القبول (Accept)

يستخدم الخادم accept() لاستقبال اتصال جديد من عميل.

5. الاتصال بالخادم (Connect)

العميل يستخدم connect() للاتصال بالخادم عبر عنوانه ومنفذه.

6. الإرسال والاستقبال

يُستخدم send() وrecv() أو read() وwrite() لتبادل البيانات.

7. الإغلاق (Close)

يجب دائمًا إغلاق المقبس بعد الانتهاء باستخدام close().


نموذج خادم-عميل باستخدام UDP في ++C

على عكس TCP، فإن بروتوكول UDP لا يعتمد على الاتصال الثابت. يمكن إرسال الرسائل مباشرة دون إنشاء قناة اتصال.

خادم UDP:

cpp
#include #include #include #include #define PORT 8080 #define BUFFER_SIZE 1024 int main() { int sockfd; char buffer[BUFFER_SIZE]; struct sockaddr_in servaddr, cliaddr; socklen_t len; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("فشل في إنشاء المقبس"); exit(EXIT_FAILURE); } servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = INADDR_ANY; servaddr.sin_port = htons(PORT); bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)); len = sizeof(cliaddr); int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&cliaddr, &len); buffer[n] = '\0'; std::cout << "الرسالة المستلمة: " << buffer << std::endl; const char* reply = "تم الاستلام (UDP)"; sendto(sockfd, reply, strlen(reply), 0, (const struct sockaddr *)&cliaddr, len); close(sockfd); return 0; }

عميل UDP:

cpp
#include #include #include #include #define PORT 8080 #define BUFFER_SIZE 1024 int main() { int sockfd; char buffer[BUFFER_SIZE]; struct sockaddr_in servaddr; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("فشل في إنشاء المقبس"); exit(EXIT_FAILURE); } servaddr.sin_family = AF_INET; servaddr.sin_port = htons(PORT); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); const char* message = "مرحباً من عميل UDP"; sendto(sockfd, message, strlen(message), 0, (const struct sockaddr *)&servaddr, sizeof(servaddr)); socklen_t len = sizeof(servaddr); int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&servaddr, &len); buffer[n] = '\0'; std::cout << "رد الخادم: " << buffer << std::endl; close(sockfd); return 0; }

مقارنة بين TCP و UDP في بيئة C++

الخاصية TCP UDP
الاتصال موجه اتصال (Connection-Oriented) غير موجه اتصال (Connectionless)
السرعة أبطأ بسبب موثوقية الاتصال أسرع ولكن أقل موثوقية
الاستخدامات الشائعة نقل الملفات، البريد، HTTP بث الفيديو، الألعاب
الموثوقية عالية منخفضة
الترتيب يحافظ على ترتيب البيانات لا يضمن الترتيب
حجم الرأس (Header) أكبر أصغر

الخاتمة

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


المراجع:

  1. W. Richard Stevens, UNIX Network Programming, Volume 1: The Sockets Networking API, Prentice Hall.

  2. MSDN Documentation – Winsock Reference: https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-start-page