حلقة الأحداث
استجب للمقابس القابلة للقراءة
حلقة الأحداث درس مجاني في C Academy على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في C Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة C Academy 4 دروس في المجموع.
قلب الخادم
حلقة الأحداث هي المحرك الذي يشغّل خادمًا يستخدم epoll. فهي تنتظر بشكل متكرر الواصفات الجاهزة، ثم توزّع العمل على المعالجات.
بنيتها ثابتة دائمًا: استدعاء epoll_wait()، والتكرار على الأحداث المُعادة، والتعامل مع كل منها، ثم تكرار الحلقة. وكل شيء آخر يعتمد على هذا الهيكل.
for (;;) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) handle(&events[i]);
}استدعاء epoll_wait
يتلقى epoll_wait() واصف epoll، ومصفوفة إخراج من عناصر epoll_event، وسعة المصفوفة، ومهلة بالميلي ثانية.
يتوقف حتى يصبح واصف واحد على الأقل جاهزًا، أو تنقضي المهلة، ثم يعيد عدد الأحداث التي ملأها في مصفوفتك. وتكون الإدخالات الناتجة فقط صالحة.
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (n == -1) { perror("epoll_wait"); }معامل المهلة
تؤدي مهلة مقدارها -1 إلى التوقف إلى أجل غير مسمى حتى وصول حدث. أما القيمة 0 فتعيد النتيجة فورًا، أي إنها تستطلع دون نوم.
وتحدد القيمة الموجبة حدًا أقصى لمدة الانتظار، وهذا مفيد إذا كنت تحتاج أيضًا إلى تشغيل مهام دورية، مثل تفريغ السجلات أو إنهاء الاتصالات الخاملة.
int n = epoll_wait(epfd, events, MAX_EVENTS, 1000); /* up to 1s */
if (n == 0) run_periodic_tasks();تحديد حجم مصفوفة الأحداث
تحدد سعة المصفوفة الحد الأقصى لعدد الأحداث الجاهزة التي يمكن لاستدعاء واحد من epoll_wait() إعادتها. وإذا كان عدد الأحداث الجاهزة أكبر، فتُعاد البقية في الاستدعاء التالي.
يكفي حجم متوسط مثل 64 أو 1024؛ إذ يوزّع epoll الأحداث المعلقة بالتناوب بعدل بين الاستدعاءات، لذلك لا يُحرم أي حدث من المعالجة.
#define MAX_EVENTS 64
struct epoll_event events[MAX_EVENTS];توزيع الأحداث على مقبس الاستماع
تحقق أولًا داخل الحلقة مما إذا كان الواصف الجاهز هو مقبس الاستماع. إذا كان كذلك، فهذا يعني أن اتصالات جديدة تنتظر.
تقبل هذه الاتصالات، وتضبطها على الوضع غير الحاجب، وتسجلها في epoll. أما الواصفات الأخرى فهي عملاء حاليون لديهم بيانات للقراءة أو مساحة للكتابة.
if (events[i].data.fd == listen_fd) {
accept_new_connections(epfd, listen_fd);
} else {
handle_client(epfd, &events[i]);
}قبول الاتصالات داخل حلقة
قد يعني حدث جاهزية واحد على مقبس الاستماع وجود عدة اتصالات معلقة. كرر استدعاء accept() حتى يعيد EAGAIN.
هذا ضروري في نمط التفعيل بالحافة، وهو ممارسة جيدة في جميع الحالات، حتى لا تظل الاتصالات تنتظر حتى الاستيقاظ التالي.
for (;;) {
int c = accept(listen_fd, NULL, NULL);
if (c == -1) { if (errno == EAGAIN) break; else break; }
set_nonblocking(c);
add_to_epoll(epfd, c);
}معالجة الأحداث القابلة للقراءة
عند وقوع EPOLLIN على عميل، اقرأ منه. في نمط التفعيل بالمستوى، يكفي استدعاء recv() واحد لكل استيقاظ.
تعني القيمة المعادة 0 أن العميل أغلق الاتصال؛ فنظّف موارده. أما القيمة السالبة مع EAGAIN فتعني أنك استهلكت كل البيانات المتاحة حاليًا.
ssize_t r = recv(fd, buf, sizeof buf, 0);
if (r == 0) { close_conn(epfd, fd); }
else if (r > 0) { process(buf, r); }تفريغ البيانات في نمط التفعيل بالحافة
مع EPOLLET، يجب أن تقرأ حتى يعيد recv() القيمة EAGAIN. لا تنبّهك النواة إلا عند الانتقال إلى حالة قابلية القراءة، ولذلك ستظل البايتات المتبقية دون معالجة حتى يحدث تغير جديد.
ضع القراءة داخل حلقة، ولا تخرج منها إلا عند ظهور EAGAIN أو نهاية التدفق.
for (;;) {
ssize_t r = recv(fd, buf, sizeof buf, 0);
if (r > 0) process(buf, r);
else if (r == 0) { close_conn(epfd, fd); break; }
else { if (errno == EAGAIN) break; else { close_conn(epfd, fd); break; } }
}معالجة الأحداث القابلة للكتابة
يقع EPOLLOUT عندما يستطيع المقبس قبول مزيد من البيانات الصادرة. ولا تحتاج إليه إلا عند وجود بايتات مخزنة مؤقتًا لم تُرسل بالكامل في وقت سابق.
فرّغ المخزن المؤقت؛ وعندما يصبح فارغًا، أعد الاهتمام إلى EPOLLIN فقط باستخدام EPOLL_CTL_MOD لتجنب سيل مستمر من إشعارات الجاهزية للكتابة.
if (events[i].events & EPOLLOUT) {
flush_pending(fd);
if (buffer_empty(fd)) watch_read_only(epfd, fd);
}معالجة حالات الانتظار المتقطعة
قد يعيد epoll_wait() القيمة -1 مع errno == EINTR إذا قاطعه إشارة. وهذا ليس خطأً حقيقيًا؛ أعد المحاولة فحسب.
تواصل الحلقات المتينة عملها عند EINTR، ولا تعتبر رموز الأخطاء الأخرى قاتلة إلا عند الحاجة. وقد يؤدي نسيان ذلك إلى تعطل الخادم عند تلقيه إشارة للمرة الأولى.
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (n == -1) {
if (errno == EINTR) continue;
perror("epoll_wait"); break;
}حلقة واحدة، وعملاء كثيرون
اجمع المكونات معًا، وستتنقل خيوط واحدة باستمرار بين الواصفات الجاهزة: قبول، وقراءة، وكتابة، وإغلاق، ثم تكرار.
لأن الخيط ينام داخل epoll_wait() عند الخمول، ولا يعالج أثناء الانشغال إلا العمل الجاهز، يستطيع هذا التصميم التعامل بسهولة مع عشرات الآلاف من الاتصالات المتزامنة.
تحقق سريع
اختبر مدى فهمك لحلقة الأحداث.
مراجعة
تستدعي حلقة الأحداث epoll_wait()، وتكرر على الأحداث الجاهزة، ثم توزّع المعالجة: قبول الاتصالات على مقبس الاستماع، والقراءة عند EPOLLIN، وتفريغ البيانات عند EPOLLOUT، والتنظيف عند انقطاع الاتصال.
كرّر عمليات القبول والقراءة، وأعد المحاولة عند EINTR، وفرّغ البيانات بالكامل مع EPOLLET. بعد ذلك سنبني خادم صدى كاملًا.
الأسئلة الشائعة
هل درس «حلقة الأحداث» مجاني؟
نعم — نص درس «حلقة الأحداث» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة C Academy، انتقل إلى CoddyKit PRO. تتضمن دورة C Academy 4 دروس في المجموع.
ماذا ستتعلم في «حلقة الأحداث»؟
استجب للمقابس القابلة للقراءة تتمرن على C Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ C Academy؟
لا تُشترط خبرة سابقة. C Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.
كم من الوقت يستغرق درس «حلقة الأحداث»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس C Academy هذا؟
نعم. كل درس في C Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.