> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# FreeSWITCH للمنصات الكبيرة ومراكز الاتصال Contact Center
- URL: https://arabroot.io/articles/freeswitch-للمنصات-الكبيرة-ومراكز-الاتصال/
- Published: 2026-10-19T07:00:00.000Z
- Updated: 2026-10-20T20:46:42.000Z
- Description: نبني FreeSWITCH من المصدر في Docker مع طابور مركز اتصال وسكربت Lua وEvent Socket، ثم نقيس كم مكالمة في الثانية يتحمل.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الاستضافة الذاتية والتطبيقات, تطبيقات لفريقك, الشبكات والوصول, الاستضافة الذاتية

في [تتبع مكالمة SIP عبر الشبكة](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D9%85%D9%83%D8%A7%D9%84%D9%85%D8%A9-sip-%D8%B9%D8%A8%D8%B1-%D8%A7%D9%84%D8%B4%D8%A8%D9%83%D8%A9-%D9%88%D8%A7%D9%84%D8%B5%D9%88%D8%AA-%D9%81%D9%8A-%D8%A7%D8%AA%D8%AC%D8%A7%D9%87-%D9%88%D8%A7%D8%AD%D8%AF/) استخدمنا Asterisk مقسماً لمكتب فيه هاتفان، وهذا هو الاستخدام الذي صمم له أغلب الناس مقسمهم. ولكن هناك نوعاً آخر من الأنظمة: مركز اتصال Contact Center فيه مئات الوكلاء Agents، أو منصة تبيع خدمة الاتصال لعملاء كثيرين، أو نظام يتصل بالعملاء آلياً ويقرأ لهم رسالة، وفي هذه الأنظمة لا يكفي أن يرن الهاتف ويصل الصوت، وإنما يحتاج تطبيقك أن يعرف كل ما يحدث في كل مكالمة لحظة حدوثه، وأن يتحكم فيها من الكود، وأن يتحمل السيرفر عشرات المكالمات الجديدة في الثانية. وهذا هو المكان الذي تجد فيه FreeSWITCH.

المواضيع في السلسلة:

- [مصطلحات الاتصالات الهاتفية VoIP للمبتدئين](https://arabroot.io/articles/%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA-voip-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)
- [تتبع مكالمة SIP عبر الشبكة: لماذا يصل الصوت في اتجاه واحد؟](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D9%85%D9%83%D8%A7%D9%84%D9%85%D8%A9-sip-%D8%B9%D8%A8%D8%B1-%D8%A7%D9%84%D8%B4%D8%A8%D9%83%D8%A9-%D9%88%D8%A7%D9%84%D8%B5%D9%88%D8%AA-%D9%81%D9%8A-%D8%A7%D8%AA%D8%AC%D8%A7%D9%87-%D9%88%D8%A7%D8%AD%D8%AF/)
- [لماذا يتقطع الصوت في مكالمات VoIP؟ قياس Jitter وPacket Loss](https://arabroot.io/articles/%D9%84%D9%85%D8%A7%D8%B0%D8%A7-%D9%8A%D8%AA%D9%82%D8%B7%D8%B9-%D8%A7%D9%84%D8%B5%D9%88%D8%AA-%D9%81%D9%8A-%D9%85%D9%83%D8%A7%D9%84%D9%85%D8%A7%D8%AA-voip/)
- [ربط المقسم PBX بشبكة الهاتف عبر SIP Trunk والأرقام DID](https://arabroot.io/articles/%D8%B1%D8%A8%D8%B7-%D8%A7%D9%84%D9%85%D9%82%D8%B3%D9%85-%D8%A8%D8%B4%D8%A8%D9%83%D8%A9-%D8%A7%D9%84%D9%87%D8%A7%D8%AA%D9%81-%D8%B9%D8%A8%D8%B1-sip-trunk/)
- [دليل Asterisk العملي: من التحويلة Extension إلى الرد الآلي IVR](https://arabroot.io/articles/%D8%AF%D9%84%D9%8A%D9%84-asterisk-%D8%A7%D9%84%D8%B9%D9%85%D9%84%D9%8A-%D9%85%D9%86-%D8%A7%D9%84%D8%AA%D8%AD%D9%88%D9%8A%D9%84%D8%A9-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%B1%D8%AF-%D8%A7%D9%84%D8%A7%D9%84%D9%8A/)
- [FreePBX: مقسم هاتف PBX كامل تديره من المتصفح](https://arabroot.io/articles/freepbx-%D9%85%D9%82%D8%B3%D9%85-%D9%87%D8%A7%D8%AA%D9%81-%D9%83%D8%A7%D9%85%D9%84-%D8%AA%D8%AF%D9%8A%D8%B1%D9%87-%D9%85%D9%86-%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D9%81%D8%AD/)
- FreeSWITCH للمنصات الكبيرة ومراكز الاتصال Contact Center (الموضوع الحالي)
- [3CX أم FreePBX أم FreeSWITCH؟ كيف تختار المقسم PBX](https://arabroot.io/articles/3cx-%D8%A7%D9%85-freepbx-%D8%A7%D9%85-freeswitch/)
- [هواتف SIP المكتبية والتطبيقات: لماذا لا يرن الجوال النائم؟](https://arabroot.io/articles/%D9%87%D9%88%D8%A7%D8%AA%D9%81-sip-%D8%A7%D9%84%D9%85%D9%83%D8%AA%D8%A8%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84/)
- [مركز اتصال Call Center على Asterisk: الطابور وتقرير SLA](https://arabroot.io/articles/%D9%85%D8%B1%D9%83%D8%B2-%D8%A7%D8%AA%D8%B5%D8%A7%D9%84-call-center-%D8%B9%D9%84%D9%89-asterisk/)
- [تأمين المقسم من الاحتيال على المكالمات Toll Fraud](https://arabroot.io/articles/%D8%AA%D8%A3%D9%85%D9%8A%D9%86-%D8%A7%D9%84%D9%85%D9%82%D8%B3%D9%85-%D9%85%D9%86-%D8%A7%D9%84%D8%A7%D8%AD%D8%AA%D9%8A%D8%A7%D9%84-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D9%85%D9%83%D8%A7%D9%84%D9%85%D8%A7%D8%AA/)
- [موازنة الحمل Load Balancing أمام خوادم Asterisk مع Kamailio](https://arabroot.io/articles/%D9%85%D9%88%D8%A7%D8%B2%D9%86%D8%A9-%D8%A7%D9%84%D8%AD%D9%85%D9%84-%D8%A3%D9%85%D8%A7%D9%85-%D8%AE%D9%88%D8%A7%D8%AF%D9%85-asterisk-%D9%85%D8%B9-kamailio/)

وفي هذا المقال سوف نتناول:

- ما هو FreeSWITCH، وكيف يختلف تصميمه عن Asterisk، ومتى تختاره ومتى لا تختاره.
- الترخيص MPL 1.1، وقصة الحزم الجاهزة التي أصبحت تحتاج حساباً في SignalWire، ولماذا سوف نبنيه من المصدر.
- بناء Docker Image من المصدر بإصدارات مثبتة، وأول تشغيل والأخطاء التي سوف تراها فيه.
- تسجيل هاتفين ومكالمة بينهما، ثم خطة اتصال XML وسكربت Lua وطابور مركز اتصال بوحدة mod\_callcenter.
- مراقبة المكالمات والتحكم فيها من برنامج Python عبر Event Socket واختصاراً ESL.
- اختبار حمل Load Test بأداة SIPp: كم مكالمة في الثانية يتحمل، وما الذي ينهار أولاً.

## ما هو FreeSWITCH؟

[FreeSWITCH](https://github.com/signalwire/freeswitch?ref=arabroot.io) محرك اتصالات مفتوح المصدر يستقبل المكالمات ويوجهها ويعالج صوتها، ويصف نفسه في ملف README بأنه حزمة اتصالات برمجية Software Defined Telecom Stack تعمل من Raspberry Pi إلى سيرفر بأنوية كثيرة. وتصميمه يقوم على فكرتين: نواة Core صغيرة تدير الجلسات Sessions والصوت والأحداث Events، ووحدات Modules حولها تحمل عند الحاجة، فبروتوكول SIP وحدة اسمها mod\_sofia، وخطة الاتصال Dialplan وحدة، ولغة Lua وحدة، والطابور وحدة، والاتصال من برنامج خارجي وحدة. والصورة التالية تبين ذلك:

![عملية FreeSWITCH في المنتصف فيها النواة Core التي تدير الجلسات والصوت والأحداث، وحولها ست مجموعات وحدات: Endpoints مثل mod_sofia وmod_verto، وDialplan مثل mod_dialplan_xml، وApplications مثل mod_dptools وmod_callcenter وmod_conference، وCodecs وFormats، وLanguages مثل mod_lua، وEvent Handlers مثل mod_event_socket على المنفذ 8021، وعلى اليسار هواتف SIP والمتصفح ومزود SIP، وعلى اليمين ملفات XML للإعداد وتطبيقك الذي يستقبل الأحداث ويرسل الأوامر](https://arabroot.io/content/images/2026/10/freeswitch-01-architecture.webp)

كل ميزة وحدة Module، وتطبيقك يتكلم مع FreeSWITCH عبر Event Socket

في الصورة أعلاه لاحظ التالي:

- الوحدات Endpoints هي التي تتكلم مع العالم الخارجي، وأهمها mod\_sofia لبروتوكول SIP، وكل ملف إعداد له يسمى Profile، وفي الإعداد الافتراضي ملفان: `internal` على المنفذ 5060 للهواتف التي تسجل بكلمة مرور، و`external` على المنفذ 5080 لمزودي الخدمة.
- خطة الاتصال في mod\_dialplan\_xml ملفات XML فيها سياقات Contexts، وفي كل سياق امتدادات Extensions، وكل امتداد شرط Condition على رقم أو متغير، وأفعال Actions تنفذ إذا تحقق الشرط.
- كل شيء يحدث داخل FreeSWITCH يصدر حدثاً Event، مثل إنشاء قناة ورد المكالمة وإنهائها، وتستقبل هذه الأحداث وحدة mod\_event\_socket وترسلها إلى برنامجك عبر TCP على المنفذ 8021، ومنها أيضاً يرسل برنامجك الأوامر.

وقد يتساءل البعض: إذا كان Asterisk يقدم خطة اتصال وواجهات برمجية AMI وARI، فما الفرق؟ والإجابة أن الفرق في نقطة البداية، فAsterisk بدأ مقسماً للمكاتب ثم أضيفت إليه الواجهات البرمجية، أما FreeSWITCH فبني من البداية ليكون محركاً يتحكم فيه تطبيق خارجي، ولذلك لا توجد فيه واجهة إدارة ولا مفهوم «مقسم مكتب» جاهز، وتجد تحته منصات ومراكز اتصال بنت منتجها الخاص فوقه. وبالتالي إذا كنت تحتاج مقسماً لمكتبك تديره من المتصفح فهذا ليس اختيارك، والأنسب لك FreePBX كما سنرى في [آخر المقال](#when).

## الترخيص والحزم الجاهزة

FreeSWITCH مرخص بترخيص Mozilla Public License 1.1 كما في [ملف الترخيص](https://github.com/signalwire/freeswitch/blob/v1.11.3/LICENSE?ref=arabroot.io)، وهذا يعني أنك تستطيع بناء منتج تجاري مغلق فوقه، بشرط أن تنشر أي تعديل تقوم به على ملفات FreeSWITCH نفسها إذا وزعت البرنامج، وهو ترخيص أريح للشركات من GPL الذي يحكم Asterisk. ويرعى المشروع شركة SignalWire التي أسسها مطوروه الأصليون بحسب ملف README، وآخر إصدار عند كتابة المقال [1.11.3](https://github.com/signalwire/freeswitch/releases/tag/v1.11.3?ref=arabroot.io) في 28 أغسطس 2026، بعد الإصدار 1.11.0 في مايو 2026 الذي أزال نحو ثلاثين وحدة قديمة وأضاف دعم Debian 13.

وهنا تفصيل مهم قبل التثبيت: الطريقة التي يوصي بها المشروع لتثبيت الحزم الجاهزة على Debian هي السكربت `fsget`، وبحسب [صفحة التثبيت في المستودع](https://github.com/signalwire/freeswitch/blob/v1.11.3/scripts/packaging/README.md?ref=arabroot.io) فإنه يحتاج توكن من نوع SignalWire Personal Access Token واختصاراً PAT، أو توكن API لعملاء النسخة التجارية:

```bash
curl -sSL https://freeswitch.org/fsget | bash -s <PAT or API token> [release|prerelease] [install]
```

أي أن عليك أن تنشئ حساباً في SignalWire ثم توكن من لوحة التحكم قبل أن تستطيع تنزيل الحزم، و[دليل البداية الرسمي](https://developer.signalwire.com/freeswitch/foundations/getting-started?ref=arabroot.io) يقول ذلك صراحة: الحساب مطلوب للحزم الجاهزة فقط، والكود على GitHub يبقى متاحاً، والبناء من المصدر لا يحتاج توكن. وهناك أيضاً نسخة تجارية اسمها FreeSWITCH Enterprise لها مستودع خاص باشتراك.

هل هذا مشكلة؟ ليس بالضرورة، فالحساب مجاني، ولكن انتبه لأمرين: الأول أن السيرفرات التي تبني Images أو تثبت الحزم آلياً في خط CI/CD تحتاج هذا التوكن، فعاملها كسر Secret، والثاني أن الوصول إلى الحزم أصبح مرتبطاً بحساب عند جهة واحدة. وفي هذا المقال سوف نبني FreeSWITCH من المصدر، والسبب أن هذه الطريقة لا تحتاج حساباً، وتعلمك ما هي مكتبات FreeSWITCH، وتجعلك تختار الوحدات التي تحتاجها فقط.

💡

لا توجد Docker Image رسمية محدثة على Docker Hub، وملفات Docker في مجلد `docker/release` في المستودع ما زالت تبني الإصدار 1.6 على Debian 8، لذلك اعتمدنا على [مثال البناء من المصدر](https://github.com/signalwire/freeswitch/blob/v1.11.3/docker/examples/Debian11/Dockerfile?ref=arabroot.io) في المستودع نفسه، وحدثناه إلى Debian 13 وثبتنا إصدار كل مكتبة.

## بناء Docker Image من المصدر

FreeSWITCH يحتاج أربع مكتبات من مستودعات منفصلة قبل أن يبنى: `libks` من SignalWire للأدوات المشتركة، و`sofia-sip` وهي مكتبة SIP التي تبنى عليها mod\_sofia، و`spandsp` لمعالجة الإشارات مثل نغمات الأرقام DTMF والفاكس، والمستودع نفسه. قم بإنشاء مجلد للتجربة وفيه المجلد `fs`، وضع فيه الملف `Dockerfile` التالي:

```dockerfile
FROM debian:13.7-slim
ARG FS_TAG=v1.11.3
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      git ca-certificates build-essential cmake automake autoconf libtool-bin pkg-config nasm \
      libssl-dev zlib1g-dev uuid-dev libtiff-dev libjpeg-dev libncurses-dev libexpat1-dev \
      libpcre2-dev libedit-dev libsqlite3-dev libcurl4-openssl-dev libspeexdsp-dev libspeex-dev libogg-dev \
      libldns-dev liblua5.2-dev libopus-dev libsndfile1-dev procps \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src
RUN git clone -b v2.0.11 https://github.com/signalwire/libks \
 && git clone --depth 1 -b v1.13.18 https://github.com/freeswitch/sofia-sip \
 && git clone --depth 1 -b v3.1.1 https://github.com/freeswitch/spandsp \
 && git clone --depth 1 -b $FS_TAG https://github.com/signalwire/freeswitch
RUN cd libks && cmake . -DCMAKE_INSTALL_PREFIX=/usr && make -j"$(nproc)" install
RUN cd sofia-sip && ./bootstrap.sh && ./configure --with-pic --with-glib=no --without-doxygen --disable-stun --prefix=/usr \
 && make -j"$(nproc)" && make install
RUN cd spandsp && ./bootstrap.sh && ./configure --with-pic --prefix=/usr && make -j"$(nproc)" && make install
COPY modules.conf /usr/src/freeswitch/modules.conf
RUN cd freeswitch && ./bootstrap.sh -j && ./configure --disable-libvpx --disable-libyuv \
 && make -j"$(nproc)" && make install && ldconfig
ENV PATH=/usr/local/freeswitch/bin:$PATH
CMD ["freeswitch", "-nonat", "-nf", "-nc"]
```

وبجانبه الملف `modules.conf`، وهو قائمة الوحدات التي سوف تبنى، فإذا وضعته قبل `bootstrap.sh` لا يستبدله السكربت بالقائمة الافتراضية الطويلة:

```text
loggers/mod_console
loggers/mod_logfile
applications/mod_commands
applications/mod_dptools
applications/mod_db
applications/mod_hash
applications/mod_expr
applications/mod_esf
applications/mod_callcenter
applications/mod_conference
applications/mod_voicemail
codecs/mod_opus
dialplans/mod_dialplan_xml
endpoints/mod_sofia
endpoints/mod_loopback
event_handlers/mod_event_socket
formats/mod_native_file
formats/mod_sndfile
formats/mod_tone_stream
languages/mod_lua
say/mod_say_en
```

في الإعداد أعلاه لاحظ التالي:

- كل مستودع مثبت على وسم Tag محدد، فإذا بنيت ال Image بعد سنة تحصل على النتيجة نفسها، ولا تقم باستخدام الفرع الرئيسي master في الإنتاج، والسبب أنه يتغير كل يوم.
- `--disable-libvpx --disable-libyuv` يتخطى مكتبات الفيديو لأننا لا نحتاجها في مقسم صوت، وهذا يختصر وقت البناء.
- الخيار `-nonat` يمنع FreeSWITCH من البحث عن راوتر يدعم UPnP أو NAT-PMP عند التشغيل، و`-nf` يبقيه في المقدمة كما يحتاج Docker، و`-nc` يلغي الطرفية التفاعلية.
- التوزيعة الأساسية `debian:13.7-slim` مثبتة برقم الإصدار، وليس `latest`.

الآن نبني ال Image:

```bash
docker build -t arc-freeswitch:1.11.3 fs
docker run --rm arc-freeswitch:1.11.3 freeswitch -version
```

والبناء يأخذ نحو عشرين دقيقة على جهاز بثمانية أنوية، أغلبها في ترجمة FreeSWITCH نفسه، والمخرج سوف يكون كما يلي:

```text
FreeSWITCH version: 1.11.3-release+git~20260828T204948Z~ef32e20529~64bit (git ef32e20 2026-08-28 20:49:48Z 64bit)
```

⚠️

لاحظ أن `libks` وحدها تنسخ دون `--depth 1`. في أول محاولة نسخناها نسخاً سطحياً Shallow Clone مثل البقية، ففشل البناء بالخطأ `fatal: Not a valid object name v2.0.11^` ثم `CMake Error at CMakeLists.txt:319 (string): string sub-command REPLACE requires at least four arguments`، والسبب أن ملف CMake في libks يبحث عن الوسم السابق في تاريخ Git ليكتب سجل التغييرات لحزمة Debian، والنسخ السطحي لا يحمل هذا التاريخ.

## خريطة التجربة

سوف نشغل كل شيء في شبكة Docker داخلية واحدة دون طريق إلى الخارج: FreeSWITCH، وثلاثة هواتف برمجية baresip، الأول 1000 يتصل والآخران 1001 و1002 وكيلان في الطابور، وبرنامج Python صغير يتصل ب ESL، وأداة SIPp لاختبار الحمل. وقد استخدمنا baresip نفسه في [مقال تتبع المكالمة](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D9%85%D9%83%D8%A7%D9%84%D9%85%D8%A9-sip-%D8%B9%D8%A8%D8%B1-%D8%A7%D9%84%D8%B4%D8%A8%D9%83%D8%A9-%D9%88%D8%A7%D9%84%D8%B5%D9%88%D8%AA-%D9%81%D9%8A-%D8%A7%D8%AA%D8%AC%D8%A7%D9%87-%D9%88%D8%A7%D8%AD%D8%AF/#lab-phones)، والصورة التالية تبين الخريطة:

![شبكة Docker داخلية باسم arc-fs-net بالعنوان 10.66.70.0/24 فيها FreeSWITCH على 10.66.70.10 بالمنافذ 5060 و5080 و8021، وثلاثة هواتف على اليسار: 1000 المتصل على 10.66.70.20 والوكيلان 1001 على .21 و1002 على .22 تتصل عبر SIP على 5060، وعلى اليمين عميل ESL بلغة Python على .30 يتصل عبر TCP 8021، وSIPp على .40 يرسل INVITE إلى الرقم 9999 على المنفذ 5080](https://arabroot.io/content/images/2026/10/freeswitch-02-lab-map.webp)

الهواتف على ال Profile الداخلي، وSIPp على الخارجي كما يأتي مزود الخدمة

ونحتاج Image ثانية صغيرة للأدوات، فيها baresip وSIPp وPython، وكل حزمة مثبتة برقم إصدارها في Ubuntu 24.04\. قم بإنشاء المجلد `tools` وفيه الملف `Dockerfile`:

```dockerfile
FROM ubuntu:24.04
RUN apt-get update \
 && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      sip-tester=1:3.7.2-1build3 \
      baresip-core=1.0.0-4build14 \
      tcpdump=4.99.4-3ubuntu4.24.04.1 \
      python3=3.12.3-0ubuntu2.1 \
      sox iproute2 \
 && rm -rf /var/lib/apt/lists/*
```

```bash
docker build -t arc-voip-tools:1 tools
docker network create --internal --subnet 10.66.70.0/24 arc-fs-net
```

## التشغيل الأول: أخطاء الإعداد الافتراضي

بعد التثبيت تجد الإعداد في `/usr/local/freeswitch/conf`، وهو ما يسمى الإعداد الافتراضي Vanilla Config، وهو إعداد للعرض فيه عشرات الامتدادات التجريبية. وحتى نعدله من جهازنا نخرجه من ال Image إلى المجلد `conf`، ثم نشغل FreeSWITCH ونربط المجلد به:

```bash
docker create --name arc-fs-tmp arc-freeswitch:1.11.3
docker cp arc-fs-tmp:/usr/local/freeswitch/conf ./conf
docker rm arc-fs-tmp
mkdir -p scripts

docker run -d --name arc-fs-pbx --network arc-fs-net --ip 10.66.70.10 \
  -v "$PWD/conf:/usr/local/freeswitch/conf" \
  -v "$PWD/scripts:/usr/local/freeswitch/scripts" \
  arc-freeswitch:1.11.3
docker exec arc-fs-pbx grep -aE '\[(ERR|CRIT)\]' /usr/local/freeswitch/log/freeswitch.log
```

وإذا كنت تعمل من Git Bash على Windows فاكتب قبل الأوامر `export MSYS_NO_PATHCONV=1`، وإلا حول Git Bash المسارات التي تبدأ ب `/` إلى مسارات Windows قبل أن تصل إلى Docker، واستخدم `$(pwd -W)` بدل `$PWD` في الخيار `-v`. والمخرج سوف يكون كما يلي، بعد أن اختصرناه إلى الأسطر المهمة، فالقائمة الكاملة أطول:

```text
2026-10-20 08:38:05.608033 0.00% [CRIT] switch_loadable_module.c:1815 Error Loading module /usr/local/freeswitch/mod/mod_enum.so
2026-10-20 08:38:05.608068 0.00% [CRIT] switch_loadable_module.c:1815 Error Loading module /usr/local/freeswitch/mod/mod_cdr_csv.so
2026-10-20 08:38:05.612906 0.00% [ERR] sofia.c:5175 Invalid ext-rtp-ip
2026-10-20 08:38:05.612938 0.00% [ERR] sofia.c:5272 Invalid ext-sip-ip
2026-10-20 08:38:17.123943 0.00% [CRIT] switch_loadable_module.c:1815 Error Loading module /usr/local/freeswitch/mod/mod_sofia.so
2026-10-20 08:38:17.124790 0.00% [CRIT] switch_loadable_module.c:1815 Error Loading module /usr/local/freeswitch/mod/mod_rtc.so
2026-10-20 08:38:17.124881 0.00% [CRIT] switch_loadable_module.c:1815 Error Loading module /usr/local/freeswitch/mod/mod_verto.so
2026-10-20 08:38:17.124903 0.00% [CRIT] switch_loadable_module.c:1815 Error Loading module /usr/local/freeswitch/mod/mod_signalwire.so
2026-10-20 08:38:17.158878 0.00% [CRIT] switch_loadable_module.c:1815 Error Loading module /usr/local/freeswitch/mod/mod_fifo.so
2026-10-20 08:38:17.315118 0.00% [CRIT] switch_loadable_module.c:1815 Error Loading module /usr/local/freeswitch/mod/mod_local_stream.so
```

في المخرج أعلاه لاحظ التالي:

- أغلب أسطر `Error Loading module` لوحدات لم نبنها أصلاً، مثل mod\_enum وmod\_verto وmod\_signalwire، والسبب أن الملف `autoload_configs/modules.conf.xml` في الإعداد الافتراضي يطلب قائمة طويلة، وهذه أخطاء غير ضارة ولكنها تخفي الخطأ المهم بينها.
- الخطأ المهم هو فشل mod\_sofia نفسه، أي أنه لا يوجد SIP على الإطلاق، وقبله السطران `Invalid ext-rtp-ip` و`Invalid ext-sip-ip`. والسبب في الملف `vars.xml`: السطران `stun-set` فيه يطلبان من سيرفر STUN على الإنترنت `stun.freeswitch.org` أن يخبر FreeSWITCH بعنوانه العام، والشبكة داخلية فلا يصل الطلب، فتبقى قيمة `external_rtp_ip` غير صالحة ويرفض mod\_sofia ال Profile بسببها.

والحل في خطوتين: نكتب قائمة الوحدات التي بنيناها فقط في `modules.conf.xml`، ونجعل العنوان الخارجي هو عنوان السيرفر نفسه بدل STUN، لأن السيرفر في التجربة ليس خلف NAT، وإذا كان سيرفرك في السحابة خلف NAT فضع عنوانه العام هنا بدل الاعتماد على STUN عند كل تشغيل:

```bash
cat > conf/autoload_configs/modules.conf.xml <<'EOF'
<configuration name="modules.conf" description="Modules">
  <modules>
    <load module="mod_console"/>
    <load module="mod_logfile"/>
    <load module="mod_event_socket"/>
    <load module="mod_sofia"/>
    <load module="mod_loopback"/>
    <load module="mod_commands"/>
    <load module="mod_conference"/>
    <load module="mod_db"/>
    <load module="mod_dptools"/>
    <load module="mod_expr"/>
    <load module="mod_esf"/>
    <load module="mod_hash"/>
    <load module="mod_voicemail"/>
    <load module="mod_callcenter"/>
    <load module="mod_dialplan_xml"/>
    <load module="mod_opus"/>
    <load module="mod_sndfile"/>
    <load module="mod_native_file"/>
    <load module="mod_tone_stream"/>
    <load module="mod_lua"/>
    <load module="mod_say_en"/>
  </modules>
</configuration>
EOF
sed -i 's|cmd="stun-set" data="external_rtp_ip=stun:stun.freeswitch.org"|cmd="set" data="external_rtp_ip=$${local_ip_v4}"|;
        s|cmd="stun-set" data="external_sip_ip=stun:stun.freeswitch.org"|cmd="set" data="external_sip_ip=$${local_ip_v4}"|;
        s|data="hold_music=local_stream://moh"|data="hold_music=tone_stream://%(400,200,400,450);loops=-1"|' conf/vars.xml
docker restart arc-fs-pbx
docker exec arc-fs-pbx fs_cli -x "sofia status"
```

والسطر الأخير في `sed` يجعل موسيقى الانتظار نغمة يولدها FreeSWITCH بنفسه، لأننا لم نثبت ملفات الصوت. والمخرج سوف يكون كما يلي:

```text
                     Name	   Type	                                      Data	State
=================================================================================================
            external-ipv6	profile	                  sip:mod_sofia@[::1]:5080	RUNNING (0)
                127.0.0.1	  alias	                                  internal	ALIASED
                 external	profile	              sip:mod_sofia@127.0.0.1:5080	RUNNING (0)
    external::example.com	gateway	                   sip:joeuser@example.com	NOREG
            internal-ipv6	profile	                  sip:mod_sofia@[::1]:5060	RUNNING (0)
                 internal	profile	              sip:mod_sofia@127.0.0.1:5060	RUNNING (0)
=================================================================================================
4 profiles 1 alias
```

ال Profiles تعمل، ولكن انظر إلى العنوان: `127.0.0.1`، أي أن FreeSWITCH يستمع على عنوان لا يصل إليه أي هاتف خارج ال Container. والسبب أن المتغير `$${local_ip_v4}` يحسب من الطريق الافتراضي Default Route إلى الإنترنت، والشبكة الداخلية لا يوجد فيها طريق افتراضي، فرجع FreeSWITCH إلى عنوان ال Loopback. وهذا يحدث أيضاً على سيرفرات حقيقية معزولة عن الإنترنت، والحل أن تكتب العنوان بنفسك في أول `vars.xml`:

```bash
sed -i '15i\  <X-PRE-PROCESS cmd="set" data="local_ip_v4=10.66.70.10"/>' conf/vars.xml
docker restart arc-fs-pbx
docker exec arc-fs-pbx fs_cli -x "sofia status" | grep -E "profile|alias"
```

والمخرج سوف يكون كما يلي:

```text
            external-ipv6	profile	                  sip:mod_sofia@[::1]:5080	RUNNING (0)
              10.66.70.10	  alias	                                  internal	ALIASED
                 external	profile	            sip:mod_sofia@10.66.70.10:5080	RUNNING (0)
            internal-ipv6	profile	                  sip:mod_sofia@[::1]:5060	RUNNING (0)
                 internal	profile	            sip:mod_sofia@10.66.70.10:5060	RUNNING (0)
4 profiles 1 alias
```

ولاحظ أيضاً البوابة `external::example.com`، وهي مثال لخط SIP Trunk في الإعداد الافتراضي، ويجب أن تحذفها مع بقية الأمثلة قبل الإنتاج، وسوف نشرح ربط الخط الحقيقي في مقال SIP Trunk ضمن هذه المرحلة.

## تسجيل الهواتف وأول مكالمة

الإعداد الافتراضي فيه عشرون مستخدماً من 1000 إلى 1019 في المجلد `conf/directory/default`، وكلمة مرورهم جميعاً المتغير `default_password` في `vars.xml`، وقيمته `1234`. سوف نبدأ بها عمداً حتى نرى ماذا يحدث، ثم نغيرها. نجهز ملف نغمة يرسلها كل هاتف بدل الميكروفون، وإعداداً لكل هاتف مثل [مقال تتبع المكالمة](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D9%85%D9%83%D8%A7%D9%84%D9%85%D8%A9-sip-%D8%B9%D8%A8%D8%B1-%D8%A7%D9%84%D8%B4%D8%A8%D9%83%D8%A9-%D9%88%D8%A7%D9%84%D8%B5%D9%88%D8%AA-%D9%81%D9%8A-%D8%A7%D8%AA%D8%AC%D8%A7%D9%87-%D9%88%D8%A7%D8%AD%D8%AF/#lab-phones)، ثم نشغل الوكيلين:

```bash
mkdir -p sounds
docker run --rm -v "$PWD/sounds:/sounds" arc-voip-tools:1 \
  sox -n -r 8000 -c 1 -b 16 /sounds/tone.wav synth 60 sine 440 vol 0.5

for e in 1000 1001 1002; do mkdir -p phone-$e rec-$e
cat > phone-$e/config <<'EOF'
poll_method      epoll
sip_listen       0.0.0.0:5060
audio_source     aufile,/sounds/tone.wav
audio_player     alsa,null
audio_alert      alsa,null
rtp_ports        20000-20019
module_path      /usr/lib/baresip/modules
module           g711.so
module           aufile.so
module           menu.so
module_app       account.so
module           alsa.so
module           sndfile.so
module           stdio.so
snd_path         /rec
EOF
echo "<sip:$e@10.66.70.10>;auth_pass=1234;answermode=auto" > phone-$e/accounts
done

for e in 1001 1002; do
  docker run -d --name arc-fs-phone-$e --network arc-fs-net --ip 10.66.70.$((e-980)) \
    -v "$PWD/phone-$e:/conf" -v "$PWD/sounds:/sounds:ro" -v "$PWD/rec-$e:/rec" \
    arc-voip-tools:1 baresip -f /conf
done
docker exec arc-fs-pbx fs_cli -x "show registrations"
```

والمخرج سوف يكون كما يلي:

```text
reg_user,realm,token,url,expires,network_ip,network_port,network_proto,hostname,metadata
1001,10.66.70.10,5931a4213cf200a9,sofia/internal/sip:1001-0x55e17f22c520@10.66.70.21:5060,1792487641,10.66.70.21,5060,udp,e5be12e9978b,
1002,10.66.70.10,6f2ebba303f92d9c,sofia/internal/sip:1002-0x559418cf4520@10.66.70.22:5060,1792487573,10.66.70.22,5060,udp,e5be12e9978b,

2 total.
```

الهاتف 1000 سوف نشغله عند كل مكالمة فقط، ونرسل له أمر الاتصال من سطر الأوامر، لذلك نضع ذلك في السكربت `call.sh`:

```bash
cat > call.sh <<'EOF'
#!/bin/sh
# usage: ./call.sh <number> <seconds in call>   (phone 1000 dials, then hangs up)
(sleep 3; echo "/dial $1"; sleep $2; echo "/hangup"; sleep 2; echo "q") | \
  docker run -i --rm --name arc-fs-phone-1000 --network arc-fs-net --ip 10.66.70.20 \
    -v "$PWD/phone-1000:/conf" -v "$PWD/sounds:/sounds:ro" -v "$PWD/rec-1000:/rec" \
    arc-voip-tools:1 baresip -f /conf 2>&1 | tr '\r' '\n' | grep -aE 'audio=|terminated' | tail -2
EOF
chmod +x call.sh
./call.sh 1001 20
docker exec arc-fs-pbx grep -aE 'CRIT|EXECUTE.*(sleep|bridge)' /usr/local/freeswitch/log/freeswitch.log
```

والمخرج سوف يكون كما يلي، بعد أن حذفنا رقم القناة من أول كل سطر:

```text
2026-10-20 08:10:17.679878 [CRIT] mod_dptools.c:1865 WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
2026-10-20 08:10:17.679878 [CRIT] mod_dptools.c:1865 Open /usr/local/freeswitch/conf/vars.xml and change the default_password.
2026-10-20 08:10:17.679878 [CRIT] mod_dptools.c:1865 Once changed type reloadxml at the console.
2026-10-20 08:10:17.679878 [CRIT] mod_dptools.c:1865 WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
EXECUTE [depth=0] sofia/internal/1000@10.66.70.10 sleep(10000)
EXECUTE [depth=0] sofia/internal/1000@10.66.70.10 bridge(user/1001@10.66.70.10)
```

المكالمة وصلت، ولكن بعد عشر ثوان كاملة من الانتظار، والسبب أن خطة الاتصال الافتراضية تفحص كلمة المرور قبل كل مكالمة، فإذا كانت `1234` كتبت التحذير في السجل وانتظرت عشر ثوان بالفعل `sleep(10000)`، أي أن المشروع يزعجك عمداً حتى تغيرها. والملف `vars.xml` نفسه يقول في أوله إن تركها يعرضك للاحتيال على المكالمات Toll Fraud، وهذا صحيح، فكلمة مرور مكونة من أربعة أرقام لعشرين تحويلة تخمن في دقائق. نغيرها إلى قيمة عشوائية، ونعيد تحميل الإعداد ب `reloadxml`، ونحدث الهواتف:

```bash
PASS=$(openssl rand -hex 12)
sed -i "s/data=\"default_password=1234\"/data=\"default_password=$PASS\"/" conf/vars.xml
docker exec arc-fs-pbx fs_cli -x reloadxml
for e in 1000 1001 1002; do
  echo "<sip:$e@10.66.70.10>;auth_pass=$PASS;answermode=auto" > phone-$e/accounts
done
docker restart arc-fs-phone-1001 arc-fs-phone-1002
./call.sh 1001 15
docker exec arc-fs-phone-1001 ls -l /rec | tail -2
```

والمخرج سوف يكون كما يلي:

```text
[0:00:13] audio=61440/61013 (bit/s)
sip:1000@10.66.70.10: Call with sip:1001@10.66.70.10 terminated (duration: 14 secs)
-rw-r--r-- 1 root root 217964 Oct 20 08:39 dump-2026-10-20-08-39-10-dec.wav
-rw-r--r-- 1 root root 225644 Oct 20 08:39 dump-2026-10-20-08-39-10-enc.wav
```

اختفى الانتظار، والصوت يمر في الاتجاهين بمعدل قريب من 64 kbit/s، وهو معدل الترميز G.711 الذي اتفق عليه الطرفان، وملف ما سمعه الوكيل `dec.wav` نحو 210 KB، أي أن الصوت وصل فعلاً وليس فقط أن المكالمة اتصلت.

## خطة الاتصال XML وسكربت Lua

خطة الاتصال الافتراضية في `conf/dialplan/default.xml`، وهي تقرأ في آخرها كل ملف في المجلد `conf/dialplan/default/`، فنضع امتداداتنا هناك بدل تعديل الملف الكبير. ولكن قبل أن تختار الأرقام انتبه: FreeSWITCH يقرأ الامتدادات بالترتيب، وأول امتداد يتحقق شرطه ينفذ، والإعداد الافتراضي يستخدم أرقاماً كثيرة للعرض، فالرقم 4000 مثلاً هو البريد الصوتي، والأرقام من 3000 إلى 3099 غرف مؤتمرات، لذلك اخترنا 7100 و7200\. قم بإنشاء الملف `conf/dialplan/default/00_lab.xml`:

```xml
<include>
  <!-- 7100: a Lua script decides where the call goes -->
  <extension name="lab_business_hours">
    <condition field="destination_number" expression="^7100$">
      <action application="lua" data="business_hours.lua 8 16"/>
    </condition>
  </extension>
  <!-- 7200: the support queue of mod_callcenter -->
  <extension name="lab_support_queue">
    <condition field="destination_number" expression="^7200$">
      <action application="answer"/>
      <action application="set" data="cc_export_vars=caller_id_number"/>
      <action application="callcenter" data="support@default"/>
    </condition>
  </extension>
</include>
```

في الإعداد أعلاه لاحظ التالي:

- الشرط `condition` تعبير نمطي Regular Expression على الحقل `destination_number`، أي الرقم المطلوب، ويمكن أن يكون الشرط على أي متغير في القناة، مثل رقم المتصل أو الوقت.
- الرقم 7100 لا يفعل شيئاً بنفسه، وإنما يسلم المكالمة لسكربت Lua مع رقمين هما بداية الدوام ونهايته، فإذا تغير الدوام عدلت خطة الاتصال ولم تلمس السكربت.
- الرقم 7200 يرد على المكالمة ثم يدخلها الطابور `support@default`، وهو طابور موجود في الإعداد الافتراضي في `autoload_configs/callcenter.conf.xml` بالاستراتيجية `longest-idle-agent`، أي أن المكالمة تذهب إلى الوكيل الذي انتظر أطول مدة دون مكالمة.

وقد يتساءل البعض: لماذا Lua إذا كانت خطة الاتصال XML تستطيع أن تفحص الوقت بنفسها؟ والإجابة أن XML يناسب التوجيه الثابت، أما عندما يحتاج القرار إلى منطق، مثل قراءة جدول الإجازات من قاعدة بيانات أو سؤال نظام CRM عن العميل، فمن الأسهل أن تكتبه في لغة برمجة، ووحدة [mod\_lua](https://developer.signalwire.com/freeswitch/module-reference/languages/mod%5Flua?ref=arabroot.io) تشغل السكربت داخل FreeSWITCH نفسه، فيستطيع أن يرد على المكالمة ويشغل الأصوات ويحولها. والسكربت `scripts/business_hours.lua` كما يلي:

```lua
-- Routes a call by working hours: to the queue during work, a short tone and hang up outside.
-- argv[1] = opening hour, argv[2] = closing hour (24h clock, container time)
local open_h  = tonumber(argv[1]) or 8
local close_h = tonumber(argv[2]) or 16
local hour    = tonumber(os.date("%H"))
local wday    = tonumber(os.date("%w"))          -- 0 = Sunday ... 6 = Saturday
local is_open = wday <= 4 and hour >= open_h and hour < close_h

local caller = session:getVariable("caller_id_number")
freeswitch.consoleLog("NOTICE", string.format(
  "business_hours: caller=%s day=%d hour=%d window=%d-%d open=%s\n",
  caller, wday, hour, open_h, close_h, tostring(is_open)))

if is_open then
  session:execute("transfer", "7200 XML default")
else
  session:answer()
  session:execute("playback", "tone_stream://%(400,200,480,620);loops=3")
  session:hangup("NORMAL_CLEARING")
end
```

في السكربت أعلاه لاحظ التالي:

- المتغير `session` هو المكالمة الحالية، ويضعه FreeSWITCH في السكربت تلقائياً، ومنه نقرأ متغيرات القناة وننفذ أي تطبيق من تطبيقات خطة الاتصال ب `session:execute`.
- الأيام من الأحد إلى الخميس هي أيام العمل في هذا المثال، فغيرها بحسب جدولك.
- الوقت هو وقت ال Container، وهو UTC ما لم تضبط المنطقة الزمنية، وهذا خطأ شائع يجعل المقسم يغلق أبوابه قبل نهاية الدوام بثلاث ساعات، فاضبط المتغير `TZ` أو احسب الفرق في السكربت.

وقبل أن نجرب الطابور نحتاج وكلاء فيه، ونضيفهم من سطر الأوامر بالأمر `callcenter_config` كما في [فصل الطوابير في الوثائق](https://developer.signalwire.com/freeswitch/applications/call-queues?ref=arabroot.io): نضيف الوكيل من نوع `callback`، أي أن الطابور يتصل به عند وصول مكالمة، ونحدد كيف يصل إليه، ونجعله متاحاً، ثم نربطه بالطابور في مستوى Tier:

```bash
for a in 1001 1002; do
  docker exec arc-fs-pbx fs_cli -x "callcenter_config agent add $a@default callback"
  docker exec arc-fs-pbx fs_cli -x "callcenter_config agent set contact $a@default user/$a"
  docker exec arc-fs-pbx fs_cli -x "callcenter_config agent set status $a@default Available"
  docker exec arc-fs-pbx fs_cli -x "callcenter_config tier add support@default $a@default 1 1"
done
docker exec arc-fs-pbx fs_cli -x "callcenter_config queue list agents support@default"
```

والمخرج سوف يكون كما يلي، بعد أن اختصرنا الأعمدة:

```text
name|instance_id|uuid|type|contact|status|busy_delay_time|no_answer_delay_time|last_bridge_start|last_bridge_end
1001@default|single_box||callback|user/1001|Available|0|0|0|0
1002@default|single_box||callback|user/1002|Available|0|0|0|0
+OK
```

والوكلاء الذين تضيفهم بهذه الأوامر يحفظون في قاعدة بيانات SQLite داخل FreeSWITCH، فيبقون بعد إعادة التشغيل، وهذا ما رأيناه عندما أعدنا تشغيل ال Container لاحقاً. وفي مركز اتصال حقيقي يقوم تطبيقك بهذه الأوامر عبر ESL، مثل جعل الوكيل في استراحة `On Break` عندما يضغط زراً في شاشته.

## مراقبة كل شيء من برنامجك: Event Socket

الآن نريد أن نرى ما يحدث في المكالمة من خارج FreeSWITCH، وهذا عمل Event Socket. والبروتوكول نصي بسيط عبر TCP: يتصل برنامجك بالمنفذ 8021، ويرسل `auth` وكلمة المرور، ثم يطلب الأحداث التي تهمه بالأمر `event plain`، ثم يقرأ كل حدث كأسطر `Header: value`. والبرنامج التالي `esl/esl_watch.py` يفعل ذلك دون أي مكتبة خارجية، ويطبع سطراً لكل حدث، ويرسل أمراً اختيارياً عبر `bgapi`:

```python
#!/usr/bin/env python3
# Connects to the FreeSWITCH Event Socket, subscribes to a few events and prints one line per event.
# usage: esl_watch.py <host> <password> [api command to send first]
import socket, sys, time
from urllib.parse import unquote

host, password = sys.argv[1], sys.argv[2]
s = socket.create_connection((host, 8021))
buf = b""

def read_msg():
    """Reads one ESL message: headers until an empty line, then a body of Content-Length bytes."""
    global buf
    while b"\n\n" not in buf:
        chunk = s.recv(65536)
        if not chunk:
            sys.exit(0)
        buf += chunk
    head, buf = buf.split(b"\n\n", 1)
    headers = dict(l.split(": ", 1) for l in head.decode().splitlines() if ": " in l)
    n = int(headers.get("Content-Length", 0))
    while len(buf) < n:
        buf += s.recv(65536)
    body, buf = buf[:n], buf[n:]
    return headers, body.decode(errors="replace")

def send(cmd):
    s.sendall((cmd + "\n\n").encode())

h, body = read_msg()                         # expected: Content-Type: auth/request
if h.get("Content-Type") != "auth/request":
    print(h.get("Content-Type"), body.strip())
    sys.exit(1)
send(f"auth {password}")
print(read_msg()[0].get("Reply-Text"))
send("event plain CHANNEL_CREATE CHANNEL_ANSWER CHANNEL_HANGUP_COMPLETE CUSTOM callcenter::info")
print(read_msg()[0].get("Reply-Text"))
if len(sys.argv) > 3:
    send("bgapi " + sys.argv[3])

while True:
    h, body = read_msg()
    if h.get("Content-Type") == "command/reply":
        print("reply:", h.get("Reply-Text"), h.get("Job-UUID", ""))
        continue
    if h.get("Content-Type") != "text/event-plain":
        continue
    ev = dict(l.split(": ", 1) for l in body.splitlines() if ": " in l)
    ev = {k: unquote(v) for k, v in ev.items()}
    name = ev.get("Event-Name")
    if name == "BACKGROUND_JOB":
        continue
    if name == "CUSTOM":
        name = ev.get("Event-Subclass") + " " + ev.get("CC-Action", "")
        extra = f"queue={ev.get('CC-Queue','')} agent={ev.get('CC-Agent','')} {ev.get('CC-Agent-State','') or ev.get('CC-Agent-Status','')}"
    else:
        extra = (f"{ev.get('Caller-Caller-ID-Number','')} -> {ev.get('Caller-Destination-Number','')}"
                 f" state={ev.get('Answer-State','')} {ev.get('Hangup-Cause','')}")
    print(time.strftime("%H:%M:%S"), f"{name:<32}", ev.get("Unique-ID", "")[:8], extra, flush=True)
```

نشغله في Container على العنوان 10.66.70.30 بكلمة المرور الافتراضية `ClueCon`:

```bash
docker run --rm --name arc-fs-esl --network arc-fs-net --ip 10.66.70.30 \
  -v "$PWD/esl:/esl:ro" arc-voip-tools:1 python3 /esl/esl_watch.py 10.66.70.10 ClueCon
```

والمخرج سوف يكون كما يلي:

```text
text/rude-rejection Access Denied, go away.
```

وفي سجل FreeSWITCH:

```text
[WARNING] mod_event_socket.c:2682 IP ::ffff:10.66.70.30 Rejected by acl "loopback.auto"
```

والسبب أن الإعداد الافتراضي في `event_socket.conf.xml` يستمع على كل العناوين `::`، ولكن سطر قائمة السماح ACL معلق فيه، وعندها بحسب [وثائق Event Socket](https://developer.signalwire.com/freeswitch/integration/event-socket?ref=arabroot.io) تطبق الوحدة القائمة `loopback.auto` التي لا تسمح إلا للجهاز نفسه. وهذا السلوك الآمن هو ما تريده، ولكن انتبه إلى أن كلمة المرور الافتراضية `ClueCon` معروفة للجميع، وأن من يصل إلى المنفذ 8021 بكلمة المرور يستطيع أن ينشئ مكالمات على حسابك ويقرأ كل ما يحدث، لذلك لا تقم بفتح هذا المنفذ للإنترنت أبداً. نضيف قائمة باسم `esl_clients` تسمح للجهاز نفسه ولعميلنا فقط، ونغير كلمة المرور:

```bash
EP=$(openssl rand -hex 16)
sed -i 's|  </network-lists>|    <list name="esl_clients" default="deny">\n      <node type="allow" cidr="127.0.0.1/32"/>\n      <node type="allow" cidr="10.66.70.30/32"/>\n    </list>\n  </network-lists>|' \
  conf/autoload_configs/acl.conf.xml
cat > conf/autoload_configs/event_socket.conf.xml <<EOF
<configuration name="event_socket.conf" description="Socket Client">
  <settings>
    <param name="nat-map" value="false"/>
    <param name="listen-ip" value="0.0.0.0"/>
    <param name="listen-port" value="8021"/>
    <param name="password" value="$EP"/>
    <param name="apply-inbound-acl" value="esl_clients"/>
  </settings>
</configuration>
EOF
docker exec arc-fs-pbx fs_cli -x "reloadacl"
docker exec arc-fs-pbx fs_cli -x "reload mod_event_socket"
docker exec arc-fs-pbx sh -c "printf '[default]\npassword => $EP\n' > /root/.fs_cli_conf"
```

والسطر الأخير مهم، لأن `fs_cli` نفسه عميل ESL، ويستخدم `ClueCon` إذا لم يجد كلمة مرور في الملف `~/.fs_cli_conf`. الآن نشغل العميل في الخلفية، ونتصل مرتين بالرقم 7100:

```bash
docker run -d --name arc-fs-esl --network arc-fs-net --ip 10.66.70.30 -e PYTHONUNBUFFERED=1 \
  -v "$PWD/esl:/esl:ro" arc-voip-tools:1 python3 /esl/esl_watch.py 10.66.70.10 $EP
./call.sh 7100 10
./call.sh 7100 10
docker logs arc-fs-esl
```

والمخرج سوف يكون كما يلي، وهو المكالمتان كما رآهما برنامجنا:

```text
+OK accepted
+OK event listener enabled plain
08:14:06 CHANNEL_CREATE                   13a25fd9 1000 -> 7100 state=ringing
08:14:06 CHANNEL_ANSWER                   13a25fd9 1000 -> 7200 state=answered
08:14:06 callcenter::info member-queue-start 13a25fd9 queue=support@default agent=
08:14:06 callcenter::info members-count    queue=support@default agent=
08:14:06 callcenter::info agent-state-change  queue= agent=1001@default Receiving
08:14:06 callcenter::info agent-offering   queue=support@default agent=1001@default
08:14:06 CHANNEL_CREATE                   9b455b7e 1000 -> 1001-0x55a07960d520 state=ringing
08:14:06 CHANNEL_ANSWER                   9b455b7e 1000 -> 1001-0x55a07960d520 state=answered
08:14:06 callcenter::info bridge-agent-start 9b455b7e queue=support@default agent=1001@default
08:14:06 callcenter::info members-count    queue=support@default agent=
08:14:06 callcenter::info agent-state-change  queue= agent=1001@default In a queue call
08:14:17 CHANNEL_HANGUP_COMPLETE          9b455b7e 1000 -> 1001-0x55a07960d520 state=hangup NORMAL_CLEARING
08:14:17 callcenter::info bridge-agent-end 9b455b7e queue=support@default agent=1001@default
08:14:17 callcenter::info member-queue-end 13a25fd9 queue=support@default agent=1001@default
08:14:17 callcenter::info agent-state-change  queue= agent=1001@default Waiting
08:14:17 CHANNEL_HANGUP_COMPLETE          13a25fd9 1000 -> 7200 state=hangup NORMAL_CLEARING
08:14:27 CHANNEL_CREATE                   df1c0fef 1000 -> 7100 state=ringing
08:14:27 CHANNEL_ANSWER                   df1c0fef 1000 -> 7200 state=answered
08:14:27 callcenter::info member-queue-start df1c0fef queue=support@default agent=
08:14:27 callcenter::info members-count    queue=support@default agent=
08:14:27 callcenter::info agent-state-change  queue= agent=1002@default Receiving
08:14:27 callcenter::info agent-offering   queue=support@default agent=1002@default
08:14:27 CHANNEL_CREATE                   47a1d006 1000 -> 1002-0x55729b37e520 state=ringing
08:14:28 CHANNEL_ANSWER                   47a1d006 1000 -> 1002-0x55729b37e520 state=answered
08:14:28 callcenter::info bridge-agent-start 47a1d006 queue=support@default agent=1002@default
08:14:28 callcenter::info members-count    queue=support@default agent=
08:14:28 callcenter::info agent-state-change  queue= agent=1002@default In a queue call
08:14:37 CHANNEL_HANGUP_COMPLETE          df1c0fef 1000 -> 7200 state=hangup NORMAL_CLEARING
08:14:37 CHANNEL_HANGUP_COMPLETE          47a1d006 1000 -> 1002-0x55729b37e520 state=hangup NORMAL_CLEARING
08:14:37 callcenter::info bridge-agent-end 47a1d006 queue=support@default agent=1002@default
08:14:37 callcenter::info member-queue-end df1c0fef queue=support@default agent=1002@default
08:14:37 callcenter::info agent-state-change  queue= agent=1002@default Waiting
```

في المخرج أعلاه لاحظ التالي:

- القناة الأولى `13a25fd9` بدأت بالرقم 7100، ثم ظهرت في حدث الرد بالرقم 7200، أي أن سكربت Lua قرر أن الدوام قائم وحول المكالمة، ولم يرد على المكالمة إلا تطبيق `answer` في امتداد الطابور.
- كل خطوة في الطابور حدث مستقل: دخول المتصل `member-queue-start`، وعرض المكالمة على الوكيل `agent-offering`، وبدء الحديث `bridge-agent-start`، وتغير حالة الوكيل من `Receiving` إلى `In a queue call` ثم `Waiting`، ومن هذه الأحداث وحدها تبني شاشة مشرف مركز الاتصال Wallboard وتقاريره.
- المكالمة الأولى ذهبت إلى 1001 والثانية إلى 1002، وهذا هو `longest-idle-agent`: بعد المكالمة الأولى أصبح 1002 هو الذي انتظر أطول.
- الطابور ينشئ لكل وكيل قناة جديدة `9b455b7e`، ثم يجسر Bridge بينها وبين قناة المتصل، فكل مكالمة في الطابور قناتان.

والصورة التالية تلخص طريق المكالمة:

![الهاتف 1000 يتصل بالرقم 7100، ثم أربع خطوات: خطة الاتصال XML تطابق 7100 وتشغل business_hours.lua 8 16، ثم Lua يحول المكالمة إلى 7200 إذا كان الدوام قائماً أو يشغل نغمة وينهيها، ثم mod_callcenter في الطابور support@default بالاستراتيجية longest-idle-agent يعطي المكالمة الأولى للوكيل 1001 والثانية للوكيل 1002، وتحتها صندوق يقول إن كل خطوة حدث على Event Socket مثل CHANNEL_CREATE وagent-offering وbridge-agent-start يصل إلى عميل ESL](https://arabroot.io/content/images/2026/10/freeswitch-03-queue-flow.webp)

خطة الاتصال تقرر، وLua يضيف المنطق، والطابور يوزع، وبرنامجك يرى كل خطوة

الآن نكسر شرط الدوام عمداً حتى نرى الفرع الثاني من السكربت، فنجعل النافذة من 9 إلى 17، وال Container يعمل على UTC والساعة فيه 8:

```bash
sed -i 's/business_hours.lua 8 16/business_hours.lua 9 17/' conf/dialplan/default/00_lab.xml
docker exec arc-fs-pbx fs_cli -x reloadxml
./call.sh 7100 6
docker exec arc-fs-pbx grep -a 'business_hours:' /usr/local/freeswitch/log/freeswitch.log | tail -1
```

والمخرج سوف يكون كما يلي:

```text
[0:00:03] audio=0/0 (bit/s)
sip:1000@10.66.70.10: Call with sip:7100@10.66.70.10 terminated (duration: 3 secs)
2026-10-20 08:14:59.040876 32.50% [NOTICE] switch_cpp.cpp:1466 business_hours: caller=1000 day=2 hour=8 window=9-17 open=false
```

سمع المتصل النغمة ثلاث مرات ثم أنهى FreeSWITCH المكالمة، دون أن يدخل الطابور. وفي الواقع تشغل هنا رسالة مسجلة بمواعيد الدوام، أو تحول المتصل إلى البريد الصوتي. أعد النافذة إلى `8 16` قبل أن تكمل.

### التحكم من برنامجك: الاتصال بنقرة Click-to-Call

الأحداث نصف القصة، والنصف الآخر أن برنامجك يرسل أوامر. والأمر الأشهر `originate` الذي ينشئ مكالمة من لا شيء، وهو ما يحدث في نظام CRM عندما يضغط الموظف على رقم العميل فيرن هاتفه أولاً، ثم يتصل النظام بالعميل. نرسله عبر البرنامج نفسه، والأمر يقول: اتصل بالهاتف 1001 برقم متصل 7000، وعندما يرد اجسره مع 1002:

```bash
docker rm -f arc-fs-esl
docker run --rm --name arc-fs-esl --network arc-fs-net --ip 10.66.70.30 -e PYTHONUNBUFFERED=1 \
  -v "$PWD/esl:/esl:ro" arc-voip-tools:1 python3 /esl/esl_watch.py 10.66.70.10 $EP \
  "originate {origination_caller_id_number=7000}user/1001 &bridge(user/1002)"
```

وأنهينا المكالمة بعد ثوان من نافذة أخرى بالأمر `fs_cli -x "hupall NORMAL_CLEARING"`، والمخرج سوف يكون كما يلي:

```text
+OK accepted
+OK event listener enabled plain
reply: +OK Job-UUID: eb8fa8af-2381-4112-906f-66685dfc7b3b eb8fa8af-2381-4112-906f-66685dfc7b3b
08:16:08 CHANNEL_CREATE                   3ed07246 7000 -> 1001-0x55a07960d520 state=ringing
08:16:09 CHANNEL_ANSWER                   3ed07246 7000 -> 1001-0x55a07960d520 state=answered
08:16:09 CHANNEL_CREATE                   8a997863 1001 -> 1002-0x55729b37e520 state=ringing
08:16:09 CHANNEL_ANSWER                   8a997863 1001 -> 1002-0x55729b37e520 state=answered
08:16:16 CHANNEL_HANGUP_COMPLETE          8a997863 1001 -> 1002-0x55729b37e520 state=hangup NORMAL_CLEARING
08:16:16 CHANNEL_HANGUP_COMPLETE          3ed07246 1001-0x55a07960d520 -> 1001-0x55a07960d520 state=hangup NORMAL_CLEARING
```

ولاحظ أن `bgapi` يرجع فوراً برقم مهمة Job-UUID، ثم تصل نتيجة المكالمة كأحداث، وهذا هو الشكل الصحيح في تطبيق حقيقي، لأن `api` العادي ينتظر حتى يرد الهاتف أو تنتهي المهلة، فيتجمد برنامجك أثناء ذلك.

⚠️

في تجربة سابقة أرسلنا `originate` يتصل بالهاتف 1001 ثم يدخله الطابور 7200، فعرض الطابور المكالمة على الوكيل 1001 نفسه، ورد عليها هاتفه البرمجي لأنه يقبل مكالمة ثانية، فأصبح الوكيل يتكلم مع نفسه وحسبت له مكالمة مجابة في `callcenter_config agent list`. لذلك لا تقم باستخدام هاتف وكيل لاختبار الطابور الذي هو فيه، واجعل هواتف الوكلاء ترفض المكالمة الثانية Call Waiting، أو اجعل الوكيل في استراحة قبل أن يتصل بالطابور.

## اختبار الحمل بأداة SIPp: كم مكالمة في الثانية؟

السؤال الذي يطرحه كل من يبني منصة: كم مكالمة يتحمل السيرفر؟ وهنا رقمان مختلفان يخلط بينهما كثيرون: عدد المكالمات المتزامنة Concurrent Calls، وعدد المكالمات الجديدة في الثانية Calls per Second واختصاراً CPS. والثاني هو الذي ينهار أولاً في مراكز الاتصال والحملات الآلية، لأن بدء المكالمة أغلى من استمرارها. وFreeSWITCH يحمي نفسه بحدين في `autoload_configs/switch.conf.xml`، وقيمتهما الافتراضية بحسب [وثائق الإعداد الأساسي](https://developer.signalwire.com/freeswitch/configuration/core-settings?ref=arabroot.io): `max-sessions` بقيمة 1000 جلسة، و`sessions-per-second` بقيمة 30 جلسة جديدة في الثانية.

وسوف نستخدم [SIPp](https://github.com/SIPp/sipp?ref=arabroot.io)، وهي أداة اختبار SIP مفتوحة المصدر، بالسيناريو المدمج `uac`، أي أنها ترسل INVITE وتنتظر الرد وترسل ACK، ثم تنتظر مدة المكالمة وترسل BYE. ونضيف رقماً في السياق العام `public` الذي يستقبل مكالمات ال Profile الخارجي على المنفذ 5080، يرد على المكالمة ويرسل نغمة إلى أن يغلقها المتصل، أي أن FreeSWITCH يولد صوتاً حقيقياً لكل مكالمة. قم بإنشاء الملف `conf/dialplan/public/00_load.xml`:

```xml
<include>
  <!-- 9999: load test target. Answers and plays a tone until the caller hangs up -->
  <extension name="lab_load_test">
    <condition field="destination_number" expression="^9999$">
      <action application="answer"/>
      <action application="playback" data="tone_stream://%(1000,0,440);loops=-1"/>
    </condition>
  </extension>
  <!-- 9998: the same call without media from FreeSWITCH (signalling only) -->
  <extension name="lab_load_test_silent">
    <condition field="destination_number" expression="^9998$">
      <action application="answer"/>
      <action application="park"/>
    </condition>
  </extension>
</include>
```

والرقم 9998 يرد ثم يوقف المكالمة `park` دون أن يرسل صوتاً، وسوف نحتاجه للمقارنة. ثم السكربت `load.sh` الذي يشغل SIPp، ويقيس استهلاك المعالج وعدد المكالمات أثناء الاختبار، ثم يطبع جدول الرسائل وعدد المكالمات الناجحة والفاشلة وتوزيع زمن الرد:

```bash
cat > load.sh <<'EOF'
#!/bin/sh
# usage: ./load.sh <calls per second> <total calls> <call length ms> [extension, default 9999]
( sleep 15; docker stats --no-stream --format 'cpu during test: {{.CPUPerc}}  mem: {{.MemUsage}}' arc-fs-pbx;
  docker exec arc-fs-pbx fs_cli -x 'show calls count' | grep total ) &
docker run --rm --name arc-fs-sipp --network arc-fs-net --ip 10.66.70.40 arc-voip-tools:1 sh -c "
  sipp 10.66.70.10:5080 -sn uac -s ${4:-9999} -i 10.66.70.40 -r $1 -m $2 -d $3 -l 5000 \
       -nostdin -trace_screen -screen_file /tmp/s.txt >/dev/null 2>&1
  awk '/Messages  Retrans/{n++} n==1' /tmp/s.txt | sed -n '1,10p'
  grep -E 'Successful call|Failed call|Call Rate' /tmp/s.txt | head -3; awk '/Average Response Time Repartition/{f++} f==1' /tmp/s.txt | sed -n '1,11p'"
wait
docker exec arc-fs-pbx fs_cli -x status | grep -E 'per Sec|session\(s\) - peak'
EOF
chmod +x load.sh
```

### 20 مكالمة في الثانية: تحت الحد

نبدأ ب 20 مكالمة جديدة في الثانية، كل منها خمس ثوان، و600 مكالمة في المجموع، أي نحو مئة مكالمة متزامنة:

```bash
./load.sh 20 600 5000
```

والمخرج سوف يكون كما يلي:

```text
cpu during test: 122.09%  mem: 119.4MiB / 31.29GiB
101 total.
                                 Messages  Retrans   Timeout   Unexpected-Msg
0 :      INVITE ---------->         600       0         0
1 :         100 <----------         600       0         0         0
2 :         180 <----------         0         0         0         0
3 :         183 <----------         0         0         0         0
4 :         200 <----------  E-RTD1 600       0         0         0
5 :         ACK ---------->         600       0
6 :       Pause [   5000ms]         600                           0
7 :         BYE ---------->         600       0         0
8 :         200 <----------         600       0         0         0
  Call Rate              |    0.000 cps              |   17.150 cps
  Successful call        |        0                  |      600
  Failed call            |        0                  |        0
0 session(s) - peak 104, last 5min 104
0 session(s) per Sec out of max 30, peak 21, last 5min 21
```

كل المكالمات نجحت، ولا توجد إعادة إرسال Retransmission واحدة، وFreeSWITCH وصل إلى 104 مكالمات متزامنة و21 جلسة جديدة في الثانية من حد 30\. ومعدل 17.150 في سطر `Call Rate` متوسط على كل مدة الاختبار، بما فيها الثواني الأخيرة التي ينتظر فيها SIPp انتهاء آخر المكالمات. ولاحظ استهلاك المعالج: 122%، أي أكثر من نواة كاملة لمئة مكالمة، وسوف نعود لهذا الرقم.

### 60 مكالمة في الثانية: حد sessions-per-second

```bash
./load.sh 60 1800 5000
```

والمخرج سوف يكون كما يلي:

```text
cpu during test: 185.49%  mem: 159MiB / 31.29GiB
149 total.
                                 Messages  Retrans   Timeout   Unexpected-Msg
0 :      INVITE ---------->         1800      0         0
1 :         100 <----------         875       0         0         925
2 :         180 <----------         0         0         0         0
3 :         183 <----------         0         0         0         0
4 :         200 <----------  E-RTD1 875       0         0         0
5 :         ACK ---------->         875       0
6 :       Pause [   5000ms]         875                           0
7 :         BYE ---------->         875       0         0
8 :         200 <----------         875       0         0         0
  Call Rate              |    0.000 cps              |   51.408 cps
  Successful call        |        0                  |      875
  Failed call            |        0                  |      925
0 session(s) - peak 156, last 5min 156
0 session(s) per Sec out of max 30, peak 62, last 5min 62
```

أكثر من نصف المكالمات فشلت: 925 من 1800، ووصلها رد غير متوقع بدل `100 Trying`. ولكي نعرف ما هو الرد شغلنا SIPp مع الخيار `-trace_err` فوجدنا:

```text
received 'SIP/2.0 503 Maximum Calls In Progress
```

وفي سجل FreeSWITCH:

```text
[CRIT] switch_core_session.c:2427 Throttle Error! 145
[CRIT] switch_time.c:1244 Over Session Rate of 30!
```

والسيرفر لم يكن مشغولاً، فالمعالج عند 185% من ثمان أنوية، وإنما رفض FreeSWITCH كل مكالمة فوق 30 في الثانية كما أمرناه، وأرسل للمتصل `503`، وهذا ما تريده في الإنتاج، فالمزود أو ال Load Balancer الذي أمامك يفهم 503 ويرسل المكالمة إلى سيرفر آخر بدل أن ينهار سيرفرك. ولكن إذا كان سيرفرك قادراً على أكثر فهذا الحد الافتراضي يرفض مكالمات حقيقية، فنرفعه أثناء التشغيل بالأمر `fsctl sps` ونعيد الاختبار نفسه:

```bash
docker exec arc-fs-pbx fs_cli -x "fsctl sps 300"
./load.sh 60 1800 5000
```

والمخرج سوف يكون كما يلي:

```text
+OK sessions per second: 300

cpu during test: 217.00%  mem: 470.7MiB / 31.29GiB
301 total.
                                 Messages  Retrans   Timeout   Unexpected-Msg
0 :      INVITE ---------->         1800      0         0
1 :         100 <----------         1800      0         0         0
2 :         180 <----------         0         0         0         0
3 :         183 <----------         0         0         0         0
4 :         200 <----------  E-RTD1 1800      0         0         0
5 :         ACK ---------->         1800      0
6 :       Pause [   5000ms]         1800                          0
7 :         BYE ---------->         1800      0         0
8 :         200 <----------         1800      0         0         0
  Call Rate              |    0.000 cps              |   51.352 cps
  Successful call        |        0                  |     1800
  Failed call            |        0                  |        0
  Average Response Time Repartition 1
             0 ms <= n <         10 ms :        545
            10 ms <= n <         20 ms :        376
            20 ms <= n <         30 ms :        245
            30 ms <= n <         40 ms :        179
            40 ms <= n <         50 ms :        122
            50 ms <= n <        100 ms :        193
           100 ms <= n <        150 ms :         64
           150 ms <= n <        200 ms :         33
                   n >=        200 ms :         43
```

نجحت المكالمات كلها، و300 مكالمة متزامنة، وأغلب الردود في أقل من 50 ملي ثانية، و43 فقط تجاوزت 200 ملي ثانية. ويجدر الإشارة هنا إلى أننا كررنا هذا الاختبار نفسه، فظهرت في مرة منها 480 إعادة إرسال ل INVITE مع أن كل المكالمات نجحت، والسبب أن جهاز الاختبار كان يشغل أعمالاً أخرى في الوقت نفسه، لذلك لا تقم بالحكم من تشغيل واحد، وكرر كل اختبار ثلاث مرات على الأقل.

### 100 مكالمة في الثانية: الصوت هو الذي ينهار

```bash
./load.sh 100 3000 5000
```

والمخرج سوف يكون كما يلي:

```text
cpu during test: 368.72%  mem: 542.9MiB / 31.29GiB
516 total.
                                 Messages  Retrans   Timeout   Unexpected-Msg
0 :      INVITE ---------->         3000      9632      927
1 :         100 <----------         1998      5         0         68
2 :         180 <----------         0         0         0         0
3 :         183 <----------         0         0         0         0
4 :         200 <----------  E-RTD1 2005      302       0         0
5 :         ACK ---------->         2005      302
6 :       Pause [   5000ms]         2005                          0
7 :         BYE ---------->         2005      6466      81
8 :         200 <----------         1924      0         0         0
  Call Rate              |    0.000 cps              |   48.555 cps
  Successful call        |        0                  |     1924
  Failed call            |        0                  |     1076
  Average Response Time Repartition 1
             0 ms <= n <         10 ms :        177
            10 ms <= n <         20 ms :        225
            20 ms <= n <         30 ms :         51
            30 ms <= n <         40 ms :         37
            40 ms <= n <         50 ms :         26
            50 ms <= n <        100 ms :         80
           100 ms <= n <        150 ms :         38
           150 ms <= n <        200 ms :         26
                   n >=        200 ms :       1345
178 session(s) - peak 1000, last 5min 1000
```

هنا تغيرت الصورة كلها: 1076 مكالمة فشلت، و927 INVITE انتهت مهلتها Timeout دون رد، وآلاف من إعادات الإرسال، وأغلب الردود الناجحة تجاوزت 200 ملي ثانية، ووصل FreeSWITCH إلى الحد الثاني `max-sessions` بألف جلسة متزامنة، مع أن 100 مكالمة في الثانية لخمس ثوان يجب أن تكون 500 فقط. والسبب أن السيرفر تأخر في الرد، فبقيت المكالمات أطول، فتجمعت حتى امتلأ الحد، ثم بدأ يرفض الجديدة، أي أن التأخير يغذي نفسه. ولاحظ السطر الأخير: 178 جلسة بقيت مفتوحة بعد انتهاء الاختبار، وهي مكالمات أرسل SIPp لها BYE وانتهت مهلته قبل أن يصله الرد، فبقيت عند FreeSWITCH ترسل النغمة إلى لا أحد. وهذه المكالمات اليتيمة لا تنتهي إلا بمهلة انقطاع الصوت `rtp-timeout-sec` في ال Profile، وقيمتها في الإعداد الافتراضي 300 ثانية، أو أن تنهيها بنفسك ب `hupall`، وفي منصة حقيقية تعني هذه المكالمات مكالمات تحسب على حسابك عند المزود.

هل المشكلة في SIP أم في الصوت؟ لنفرض أن السبب هو الصوت الذي يولده FreeSWITCH لكل مكالمة، فإذا أزلناه يجب أن يتحمل السيرفر المعدل نفسه. نعيد الاختبار نفسه على الرقم 9998 الذي لا يرسل صوتاً:

```bash
./load.sh 100 3000 5000 9998
```

والمخرج سوف يكون كما يلي، بعد أن اختصرناه:

```text
cpu during test: 210.49%  mem: 481.6MiB / 31.29GiB
501 total.
0 :      INVITE ---------->         3000      808       0
1 :         100 <----------         3000      8         0         0
4 :         200 <----------  E-RTD1 3000      529       0         0
7 :         BYE ---------->         3000      961       0
8 :         200 <----------         3000      0         0         0
  Call Rate              |    0.000 cps              |   84.289 cps
  Successful call        |        0                  |     3000
  Failed call            |        0                  |        0
```

كل المكالمات الثلاثة آلاف نجحت، و501 مكالمة متزامنة كما يجب، والمعالج عند 210% بدل 368%، ولم تبق أي مكالمة مفتوحة بعد الاختبار. أي أن الذي ينهار أولاً هو معالجة الصوت وليس رسائل SIP، والجدول التالي يجمع النتائج:

| الاختبار                         | المكالمات الناجحة | المكالمات الفاشلة | أعلى عدد متزامن | المعالج أثناء الاختبار |
| -------------------------------- | ----------------- | ----------------- | --------------- | ---------------------- |
| 20 في الثانية مع صوت، الحد 30    | 600               | 0                 | 104             | 122%                   |
| 60 في الثانية مع صوت، الحد 30    | 875               | 925 (503)         | 156             | 185%                   |
| 60 في الثانية مع صوت، الحد 300   | 1800              | 0                 | 300             | 217%                   |
| 100 في الثانية مع صوت، الحد 300  | 1924              | 1076              | 1000 (الحد)     | 368%                   |
| 100 في الثانية دون صوت، الحد 300 | 3000              | 0                 | 501             | 210%                   |

وهذه الأرقام من Docker Desktop على Windows بثماني أنوية، وهي بيئة فيها طبقة افتراضية إضافية، فلا تقم بنقلها إلى سيرفرك كما هي، وإنما خذ منها الطريقة: ابدأ بمعدل منخفض وارفعه خطوة خطوة، وراقب إعادة الإرسال وزمن الرد والمكالمات المتبقية بعد الاختبار وليس عدد الناجح فقط، واختبر بالصوت الحقيقي لأن الاختبار دون صوت يعطيك أرقاماً متفائلة. وفي مركز اتصال حقيقي يضاف إلى ذلك تسجيل المكالمات وتحويل الترميز Transcoding بين Opus وG.711، وكلاهما يستهلك المعالج أكثر من النغمة التي استخدمناها.

وأخيراً، الأمر `fsctl sps` يغير الحد في الذاكرة فقط، وعند إعادة التشغيل يعود إلى 30، كما رأينا عندما أعدنا تشغيل ال Container. لذلك اكتب القيمة في الملف:

```bash
sed -i 's/name="sessions-per-second" value="30"/name="sessions-per-second" value="300"/' \
  conf/autoload_configs/switch.conf.xml
docker restart arc-fs-pbx
docker exec arc-fs-pbx fs_cli -x status | grep "per Sec"
```

والمخرج سوف يكون كما يلي:

```text
0 session(s) per Sec out of max 300, peak 0, last 5min 0
```

## قبل أن تنقل هذا إلى الإنتاج

- **ابدأ من إعداد نظيف:** الإعداد الافتراضي للعرض، وفيه عشرون مستخدماً بكلمة مرور واحدة، وبوابة `example.com`، وعشرات الامتدادات التجريبية، فاحذف ما لا تحتاجه، واجعل لكل تحويلة كلمة مرور خاصة بها.
- **Event Socket:** كلمة مرور طويلة، وقائمة ACL بعناوين تطبيقك فقط، ولا تقم بفتح المنفذ 8021 خارج الشبكة الخاصة أبداً.
- **السياق العام public:** هذا هو السياق الذي يدخله أي متصل من الخارج دون كلمة مرور، فلا تضع فيه امتداداً يتصل بخطوطك الخارجية، والسبب أن هذا هو الطريق المعتاد للاحتيال على المكالمات، وسوف نخصص له مقال تأمين المقسم.
- **الشبكة:** شغل FreeSWITCH بشبكة المضيف `--network host` في الإنتاج كما يوصي [ملف Docker في المستودع](https://github.com/signalwire/freeswitch/blob/v1.11.3/docker/README.md?ref=arabroot.io)، والسبب أن نطاق منافذ RTP الافتراضي من 16384 إلى 32768، ونشر هذا العدد من المنافذ عبر Docker بطيء ومرهق.
- **الحدود:** اضبط `sessions-per-second` و`max-sessions` بحسب اختبار حمل على سيرفرك أنت، ولا تقم بتركها على القيم الافتراضية ولا برفعها دون اختبار.
- **الوقت:** اضبط المنطقة الزمنية في ال Container إذا كانت خطة الاتصال تعتمد على الدوام.
- **أكثر من سيرفر:** عندما يكبر الحمل لا تقم بتكبير سيرفر واحد، وإنما ضع Kamailio أمام عدة سيرفرات FreeSWITCH، وانقل حالة mod\_callcenter إلى قاعدة بيانات مشتركة عبر ODBC، مع ملاحظة أن الوثائق تحذر من أن إعداد الوكلاء في XML يعاد تطبيقه عند كل تشغيل فلا يناسب قاعدة مشتركة بين عدة سيرفرات.

## متى تختار FreeSWITCH ومتى لا تختاره؟

- **اختره** إذا كنت تبني منتجاً تتحكم فيه بالمكالمات من الكود: مركز اتصال بشاشات وكلاء خاصة بك، أو منصة مؤتمرات، أو مكالمات آلية بحجم كبير، أو خدمة اتصالات تبيعها لعملاء كثيرين، وعندك فريق يعرف SIP والبرمجة.
- **لا تختره** إذا كنت تحتاج مقسماً لمكتب أو شركة يديره مسؤول تقنية معلومات من المتصفح، فلا توجد فيه واجهة، وسوف تقضي وقتك في بناء ما يقدمه FreePBX جاهزاً، أو استخدم فوقه واجهة جاهزة مثل [FusionPBX](https://github.com/fusionpbx/fusionpbx?ref=arabroot.io) وهي مشروع مستقل.

وقد قارنا الخيارات كلها بالترخيص والسعر والاستضافة في مقال «3CX أم FreePBX أم FreeSWITCH؟ كيف تختار المقسم PBX».

## مشكلات شائعة وحلولها

| العرض                                                            | السبب                                                      | الحل                                                                        |
| ---------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------- |
| Error Loading module mod\_sofia.so مع Invalid ext-rtp-ip         | طلب STUN في vars.xml لم يصل إلى الإنترنت                   | اكتب العنوان العام أو عنوان السيرفر في external\_rtp\_ip وexternal\_sip\_ip |
| ال Profiles على 127.0.0.1 والهواتف لا تسجل                       | لا يوجد طريق افتراضي، فحسب local\_ip\_v4 عنوان ال Loopback | اضبط local\_ip\_v4 في أول vars.xml                                          |
| كل مكالمة تتأخر عشر ثوان، وفي السجل change the default\_password | كلمة المرور الافتراضية 1234                                | غيرها ثم reloadxml                                                          |
| text/rude-rejection Access Denied, go away.                      | عنوان العميل ليس في ACL ال Event Socket                    | apply-inbound-acl بقائمة فيها عنوانه، ثم reloadacl                          |
| 503 Maximum Calls In Progress وفي السجل Over Session Rate        | تجاوزت sessions-per-second أو max-sessions                 | ارفع الحد بعد اختبار حمل، أو وزع الحمل على عدة سيرفرات                      |
| الامتداد الذي كتبته لا يعمل ويحدث شيء آخر                        | امتداد في الإعداد الافتراضي يطابق الرقم قبله               | اختر أرقاماً لا يستخدمها الإعداد الافتراضي، أو احذف الأمثلة                 |
| البناء يفشل في libks ب Not a valid object name                   | نسخ سطحي \--depth 1                                        | انسخ libks بتاريخها الكامل                                                  |

## إزالة التجربة

نزيل ما أنشأناه بالاسم فقط:

```bash
docker rm -f arc-fs-pbx arc-fs-phone-1001 arc-fs-phone-1002 arc-fs-esl arc-fs-sipp
docker network rm arc-fs-net
docker rmi arc-freeswitch:1.11.3 arc-voip-tools:1
```

## تدريب عملي

- غير استراتيجية الطابور إلى `ring-all` في `callcenter.conf.xml`، ثم أعد تحميل الطابور بالأمر `callcenter_config queue reload support@default`، واتصل بالرقم 7200، وراقب في ESL كم حدث `agent-offering` يظهر لكل مكالمة.
- عدل `esl_watch.py` حتى يحسب لكل وكيل مدة الحديث من الحدثين `bridge-agent-start` و`bridge-agent-end`، ثم قارن النتيجة بعمود `talk_time` في `callcenter_config agent list`.

## الخلاصة

- FreeSWITCH محرك اتصالات بنواة صغيرة ووحدات، صمم ليتحكم فيه تطبيقك عبر Event Socket، ومكانه مراكز الاتصال والمنصات، وليس مقسم المكتب الذي يدار من المتصفح.
- الترخيص MPL 1.1 يسمح ببناء منتج تجاري فوقه، والحزم الجاهزة تحتاج حساباً وتوكن من SignalWire، والبناء من المصدر لا يحتاجه ويأخذ نحو عشرين دقيقة.
- الإعداد الافتراضي للعرض: STUN وعنوان `local_ip_v4` وكلمة المرور `1234` والامتدادات التجريبية كلها تحتاج تعديلاً قبل أي استخدام.
- خطة الاتصال XML توجه، وLua يضيف المنطق، وmod\_callcenter يوزع المكالمات على الوكلاء، وكل خطوة حدث يصل إلى برنامجك، ومنه ترسل أوامر مثل `originate`.
- الحد الافتراضي 30 مكالمة جديدة في الثانية يرفض الزائد ب 503، وعلى جهاز الاختبار تحمل FreeSWITCH 60 مكالمة في الثانية بالصوت دون فشل، وانهار عند 100 بسبب معالجة الصوت، بينما مرت 100 في الثانية دون صوت كاملة.

وفي المقال التالي بإذن الله سوف نضع FreeSWITCH بجانب 3CX وFreePBX ونقارن بينها، ثم ننتقل إلى هواتف المكتب وتطبيقات الجوال.

[![](https://arabroot.io/content/images/2026/10/voip-terms-cover-cover.webp)دليلمصطلحات الاتصالات الهاتفية VoIP للمبتدئينشرح مبسط لمصطلحات الهاتف عبر الإنترنت، وما يمر في الشبكة بين ضغط زر الاتصال ووصول الصوت إلى الطرف الآخر.](https://arabroot.io/articles/%D9%85%D8%B5%D8%B7%D9%84%D8%AD%D8%A7%D8%AA-voip-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/voip-call-path-cover-cover.webp)دليلتتبع مكالمة SIP عبر الشبكة: لماذا يصل الصوت في اتجاه واحد؟نبني مقسماً وهاتفين خلف NAT في تجربة عملية، ونتتبع المكالمة حزمة بحزمة حتى نعرف لماذا يضيع الصوت وكيف نصلحه.](https://arabroot.io/articles/%D8%AA%D8%AA%D8%A8%D8%B9-%D9%85%D9%83%D8%A7%D9%84%D9%85%D8%A9-sip-%D8%B9%D8%A8%D8%B1-%D8%A7%D9%84%D8%B4%D8%A8%D9%83%D8%A9-%D9%88%D8%A7%D9%84%D8%B5%D9%88%D8%AA-%D9%81%D9%8A-%D8%A7%D8%AA%D8%AC%D8%A7%D9%87-%D9%88%D8%A7%D8%AD%D8%AF/)

## سجل التحديثات

- أكتوبر 2026: كتابة الدليل واختباره على FreeSWITCH 1.11.3 مبنياً من المصدر على Debian 13.7، مع libks 2.0.11 وsofia-sip 1.13.18 وspandsp 3.1.1، وbaresip 1.0.0 وSIPp 3.7.2 من حزم Ubuntu 24.04.