نقل لعبة متعددة اللاعبين من C ++ إلى الويب باستخدام Cheerp و WebRTC و Firebase

مقدمة

شركتنا تقنيات الميل يوفر حلولاً لنقل تطبيقات سطح المكتب التقليدية إلى الويب. مترجم C ++ الخاص بنا ابتهج يُنشئ توليفة من WebAssembly و JavaScript ، والتي توفر ملفات تفاعل سهل من المتصفح، وعالية الأداء.

كمثال على استخدامها ، قررنا نقل لعبة متعددة اللاعبين للويب واخترناها Teeworlds. Teeworlds هي لعبة متعددة اللاعبين ثنائية الأبعاد مع مجتمع صغير ولكنه نشط من اللاعبين (بمن فيهم أنا!). إنه صغير من حيث الموارد القابلة للتنزيل ومتطلبات وحدة المعالجة المركزية ووحدة معالجة الرسومات - مرشح مثالي.

نقل لعبة متعددة اللاعبين من C ++ إلى الويب باستخدام Cheerp و WebRTC و Firebase
يعمل في متصفح Teeworlds

قررنا استخدام هذا المشروع للتجربة حلول عامة لنقل كود الشبكة إلى الويب. يتم ذلك عادةً بالطرق التالية:

  • XMLHttpRequest / fetch، إذا كان جزء الشبكة يتكون فقط من طلبات HTTP ، أو
  • WebSockets.

يتطلب كلا الحلين استضافة مكون خادم على جانب الخادم ، ولا يسمح أي منهما باستخدامه كبروتوكول نقل. UDP. هذا مهم للتطبيقات في الوقت الفعلي مثل مؤتمرات الفيديو وبرامج الألعاب لأن تسليم حزم البروتوكول وضمانات الطلب TCP يمكن أن تتداخل مع زمن الوصول المنخفض.

هناك طريقة ثالثة لاستخدام الشبكة من المتصفح: WebRTC.

RTCDataChannel يدعم الإرسال الموثوق به وغير الموثوق (في الحالة الأخيرة ، يحاول استخدام UDP كبروتوكول نقل كلما أمكن ذلك) ، ويمكن استخدامه مع كل من خادم بعيد وبين المتصفحات. هذا يعني أنه يمكننا نقل التطبيق بأكمله إلى المتصفح ، بما في ذلك مكون الخادم!

ومع ذلك ، فإن هذا يأتي مع صعوبة إضافية: قبل أن يتمكن اثنان من أقران WebRTC من التواصل ، يجب أن يقوموا بمصافحة معقدة نسبيًا للاتصال ، الأمر الذي يتطلب كيانات متعددة تابعة لجهات خارجية (خادم إشارات وواحد أو أكثر STUN/منعطف أو دور).

من الناحية المثالية ، نود إنشاء واجهة برمجة تطبيقات للشبكات تستخدم WebRTC داخليًا ، ولكنها أقرب ما يمكن إلى واجهة UDP Sockets ، والتي لا تحتاج إلى إنشاء اتصال.

سيتيح لنا ذلك الاستفادة من WebRTC دون الحاجة إلى كشف التفاصيل المعقدة لرمز التطبيق (الذي أردنا تغييره بأقل قدر ممكن في مشروعنا).

الحد الأدنى من WebRTC

WebRTC عبارة عن مجموعة من واجهات برمجة التطبيقات المتوفرة في المتصفحات لنقل الصوت والفيديو والبيانات العشوائية من نظير إلى نظير.

يتم إنشاء الاتصال بين الأقران (حتى إذا كان هناك NAT على أحد الجانبين أو كلاهما) باستخدام خوادم STUN و / أو TURN من خلال آلية تسمى ICE. يتبادل الأقران معلومات ICE ومعلمات القناة من خلال عرض بروتوكول SDP والإجابة عليه.

رائع! كم عدد الاختصارات دفعة واحدة. لنوضح بإيجاز ما تعنيه هذه المصطلحات:

  • أدوات اجتياز الجلسة لـ NAT (STUN) - بروتوكول لتجاوز NAT والحصول على زوج (IP ، منفذ) للتواصل مباشرة مع المضيف. إذا تمكن من إكمال مهمته ، فيمكن للأقران تبادل البيانات مع بعضهم البعض بشكل مستقل.
  • الاجتياز باستخدام المرحلات حول NAT (منعطف أو دور) يستخدم أيضًا لتجاوز NAT ، ولكنه يفعل ذلك عن طريق إعادة توجيه البيانات من خلال وكيل مرئي لكلا الزملاء. إنه يضيف زمن انتقال ويكون التنفيذ أكثر تكلفة من STUN (لأنه يتم تطبيقه طوال الجلسة) ، ولكن في بعض الأحيان يكون الخيار الوحيد.
  • مؤسسة الربط التفاعلي (ICE) يستخدم لتحديد أفضل طريقة ممكنة لربط اثنين من الأقران بناءً على المعلومات التي تم الحصول عليها عن طريق الاتصال المباشر مع أقران ، بالإضافة إلى المعلومات التي يتلقاها أي عدد من خوادم STUN و TURN.
  • بروتوكول وصف الجلسة (SDP) - هذا تنسيق لوصف معلمات قناة الاتصال ، على سبيل المثال ، مرشحات ICE ، وبرامج ترميز الوسائط المتعددة (في حالة قناة الصوت / الفيديو) ، وما إلى ذلك. يرسل أحد الأقران عرض SDP ("العرض") ، والثاني يستجيب بإجابة SDP ("استجابة"). بعد ذلك ، يتم إنشاء قناة.

لإنشاء مثل هذا الاتصال ، يحتاج الأقران إلى جمع المعلومات التي يتلقونها من خوادم STUN و TURN وتبادلها مع بعضهم البعض.

تكمن المشكلة في أنهم لا يملكون بعد القدرة على تبادل البيانات مباشرة ، لذلك يجب أن توجد آلية خارج النطاق لتبادل هذه البيانات: خادم إشارات.

يمكن أن يكون خادم الإشارات بسيطًا جدًا ، لأن مهمته الوحيدة هي إعادة توجيه البيانات بين الأقران أثناء مرحلة "المصافحة" (كما هو موضح في الرسم البياني أدناه).

نقل لعبة متعددة اللاعبين من C ++ إلى الويب باستخدام Cheerp و WebRTC و Firebase
تسلسل مصافحة WebRTC المبسط

نظرة عامة على نموذج شبكة Teeworlds

بنية شبكة Teeworlds بسيطة للغاية:

  • مكونات العميل والخادم هما برنامجان مختلفان.
  • يدخل العملاء اللعبة عن طريق الاتصال بواحد من عدة خوادم ، يستضيف كل منها لعبة واحدة فقط في كل مرة.
  • يتم تنفيذ جميع عمليات نقل البيانات في اللعبة من خلال الخادم.
  • يتم استخدام خادم رئيسي خاص لتجميع قائمة بجميع الخوادم العامة التي يتم عرضها في عميل اللعبة.

بفضل استخدام WebRTC لتبادل البيانات ، يمكننا نقل مكون الخادم للعبة إلى المتصفح حيث يوجد العميل. هذا يعطينا فرصة عظيمة ...

تخلص من الخوادم

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

ومع ذلك ، لكي يعمل النظام ، لا يزال يتعين علينا استخدام بنية خارجية:

  • واحد أو أكثر من خوادم STUN: لدينا العديد من الخيارات المجانية للاختيار من بينها.
  • خادم TURN واحد على الأقل: لا توجد خيارات مجانية هنا ، لذا يمكننا إعداد الخدمة الخاصة بنا أو الدفع مقابل الخدمة. لحسن الحظ ، سيكون من الممكن في معظم الأوقات الاتصال عبر خوادم STUN (وتوفير p2p حقيقي) ، ولكن TURN ضروري كإجراء احتياطي.
  • خادم التشوير: على عكس الجانبين الآخرين ، فإن الإشارة ليست موحدة. ما سيكون خادم الإشارات مسؤولاً عنه يعتمد إلى حد ما على التطبيق. في حالتنا ، قبل إنشاء اتصال ، من الضروري تبادل كمية صغيرة من البيانات.
  • خادم Teeworlds الرئيسي: يتم استخدامه من قبل خوادم أخرى للإعلان عن وجوده ومن قبل العملاء للعثور على خوادم عامة. على الرغم من أنه ليس مطلوبًا (يمكن للعملاء دائمًا الاتصال بخادم يعرفونه يدويًا) ، سيكون من الجيد امتلاكه حتى يتمكن اللاعبون من ممارسة الألعاب مع أشخاص عشوائيين.

قررنا استخدام خوادم STUN المجانية من Google ، ونشرنا خادم TURN واحدًا بأنفسنا.

بالنسبة للعنصرين الأخيرين ، استخدمناها Firebase:

  • يتم تنفيذ خادم Teeworlds الرئيسي بكل بساطة: كقائمة من الكائنات تحتوي على معلومات (الاسم ، IP ، الخريطة ، الوضع ، ...) لكل خادم نشط. تقوم الخوادم بنشر وتحديث الكائن الخاص بهم ، ويأخذ العملاء القائمة بأكملها ويعرضونها على المشغل. نعرض أيضًا القائمة بتنسيق HTML على الصفحة الرئيسية بحيث يمكن للاعبين ببساطة النقر فوق الخادم والانتقال مباشرةً إلى اللعبة.
  • يرتبط التشوير ارتباطًا وثيقًا بتنفيذ المقبس الخاص بنا ، الموضح في القسم التالي.

نقل لعبة متعددة اللاعبين من C ++ إلى الويب باستخدام Cheerp و WebRTC و Firebase
قائمة الخوادم داخل اللعبة وعلى الصفحة الرئيسية

تنفيذ المقبس

نريد إنشاء واجهة برمجة تطبيقات قريبة قدر الإمكان من Posix UDP Sockets لتقليل عدد التغييرات المطلوبة.

نريد أيضًا تنفيذ الحد الأدنى الضروري المطلوب لأبسط تبادل للبيانات عبر الشبكة.

على سبيل المثال ، لا نحتاج إلى توجيه حقيقي: جميع الأقران على نفس "الشبكة المحلية الافتراضية" المرتبطة بطبعة قاعدة بيانات Firebase معينة.

لذلك ، لا نحتاج إلى عناوين IP فريدة: لتحديد الأقران بشكل فريد ، يكفي استخدام قيم مفاتيح Firebase الفريدة (على غرار أسماء المجال) ، ويقوم كل نظير محليًا بتعيين عناوين IP "وهمية" لكل مفتاح يحتاج إلى مترجم. هذا يحفظنا تمامًا من الاضطرار إلى تعيين عناوين IP عالميًا ، وهي ليست مهمة تافهة.

هذا هو الحد الأدنى من واجهة برمجة التطبيقات التي نحتاج إلى تنفيذها:

// Create and destroy a socket
int socket();
int close(int fd);
// Bind a socket to a port, and publish it on Firebase
int bind(int fd, AddrInfo* addr);
// Send a packet. This lazily create a WebRTC connection to the 
// peer when necessary
int sendto(int fd, uint8_t* buf, int len, const AddrInfo* addr);
// Receive the packets destined to this socket
int recvfrom(int fd, uint8_t* buf, int len, AddrInfo* addr);
// Be notified when new packets arrived
int recvCallback(Callback cb);
// Obtain a local ip address for this peer key
uint32_t resolve(client::String* key);
// Get the peer key for this ip
String* reverseResolve(uint32_t addr);
// Get the local peer key
String* local_key();
// Initialize the library with the given Firebase database and 
// WebRTc connection options
void init(client::FirebaseConfig* fb, client::RTCConfiguration* ice);

واجهة برمجة التطبيقات بسيطة ومماثلة لواجهة Posix Sockets API ، ولكن مع بعض الاختلافات المهمة: تسجيل رد الاتصال وتخصيص IP المحلي والاتصال البطيء.

تسجيل عمليات الاسترجاعات

حتى إذا كان البرنامج الأصلي يستخدم الإدخال / الإخراج غير المحظور ، فيجب إعادة صياغة الكود ليعمل في مستعرض ويب.

والسبب في ذلك هو إخفاء حلقة الأحداث في المتصفح عن البرنامج (سواء أكان ذلك JavaScript أو WebAssembly).

في بيئة محلية ، يمكننا كتابة كود مثل هذا

while(running) {
  select(...); // wait for I/O events
  while(true) {
    int r = readfrom(...); // try to read
    if (r < 0 && errno == EWOULDBLOCK) // no more data available
      break;
    ...
  }
  ...
}

إذا كانت حلقة الحدث مخفية بالنسبة لنا ، فسنحتاج إلى تحويلها إلى شيء مثل هذا:

auto cb = []() { // this will be called when new data is available
  while(true) {
    int r = readfrom(...); // try to read
    if (r < 0 && errno == EWOULDBLOCK) // no more data available
      break;
    ...
  }
  ...
};
recvCallback(cb); // register the callback

تعيين IP المحلي

معرفات العقدة في "شبكتنا" ليست عناوين IP ، ولكنها مفاتيح Firebase (هذه سلاسل تبدو كالتالي: -LmEC50PYZLCiCP-vqde ).

هذا مناسب لأننا لسنا بحاجة إلى آلية لتعيين عناوين IP والتحقق مما إذا كانت فريدة (والتخلص منها بعد قطع اتصال العميل) ، ولكن غالبًا ما يكون من الضروري تحديد الأقران من خلال القيمة الرقمية.

هذا هو الغرض من الوظائف. resolve и reverseResolve: يتلقى التطبيق بطريقة ما قيمة سلسلة المفتاح (عبر إدخال المستخدم أو عبر الخادم الرئيسي) ويمكن حلها إلى عنوان IP للاستخدام الداخلي. تتلقى بقية واجهة برمجة التطبيقات أيضًا هذه القيمة بدلاً من سلسلة من أجل البساطة.

هذا مشابه لبحث DNS ، يتم إجراؤه محليًا فقط على العميل.

وهذا يعني أنه لا يمكن مشاركة عناوين IP بين عملاء مختلفين ، وإذا كانت هناك حاجة إلى نوع من المعرف العالمي ، فيجب عندئذٍ إنشاؤه بطريقة مختلفة.

انضمام كسول

لا يحتاج UDP إلى اتصال ، ولكن كما رأينا ، يتطلب WebRTC عملية اتصال طويلة قبل أن يتمكن من بدء نقل البيانات بين نظيرين.

إذا أردنا تقديم نفس المستوى من التجريد ، (sendto/recvfrom مع أقران تعسفيين بدون اتصال مسبق) ، ثم يجب عليهم إجراء اتصال "كسول" (متأخر) داخل واجهة برمجة التطبيقات.

إليك ما يحدث في الاتصال العادي بين "الخادم" و "العميل" في حالة استخدام UDP ، وما يجب أن تفعله مكتبتنا:

  • مكالمات الخادم bind()لإخبار نظام التشغيل أنه يريد استقبال الحزم على المنفذ المحدد.

بدلاً من ذلك ، سننشر منفذًا مفتوحًا إلى Firebase أسفل مفتاح الخادم ونستمع إلى الأحداث في شجرته الفرعية.

  • مكالمات الخادم recvfrom()، قبول الحزم من أي مضيف على هذا المنفذ.

في حالتنا ، نحتاج إلى التحقق من قائمة الانتظار الواردة للحزم المرسلة إلى هذا المنفذ.

يحتوي كل منفذ على قائمة انتظار خاصة به ، ونضيف منافذ المصدر والوجهة إلى بداية مخططات بيانات WebRTC حتى نعرف قائمة الانتظار التي يجب إعادة التوجيه إليها عند وصول حزمة جديدة.

المكالمة غير محظورة ، لذلك إذا لم تكن هناك حزم ، فسنرجع ببساطة -1 ونضبط errno=EWOULDBLOCK.

  • يتلقى العميل بواسطة بعض الوسائل الخارجية عنوان IP ومنفذ الخادم والمكالمات sendto(). كما تقوم بإجراء مكالمة داخلية bind()، لذلك في اليوم التالي recvfrom() سيتلقى الاستجابة بدون تنفيذ الربط بشكل صريح.

في حالتنا ، يتلقى العميل خارجيًا مفتاح سلسلة ويستخدم الوظيفة resolve() للحصول على عنوان IP.

في هذه المرحلة ، نبدأ مصافحة WebRTC إذا لم يكن الزميلان متصلين بالفعل ببعضهما البعض. تستخدم الاتصالات بمنافذ مختلفة لنفس النظير نفس قناة WebRTC DataChannel.

نحن أيضا نفعل بشكل غير مباشر bind()حتى يتمكن الخادم من إعادة الاتصال بعد ذلك sendto() في حالة إغلاقه لسبب ما.

يتم إخطار الخادم باتصال العميل عندما يكتب العميل عرض SDP الخاص به ضمن معلومات منفذ الخادم في Firebase ، ويستجيب الخادم باستجابته هناك أيضًا.

يوضح الرسم البياني أدناه مثالاً لتدفق الرسائل لنظام المقبس والرسالة الأولى من العميل إلى الخادم:

نقل لعبة متعددة اللاعبين من C ++ إلى الويب باستخدام Cheerp و WebRTC و Firebase
رسم تخطيطي كامل لمرحلة الاتصال بين العميل والخادم

اختتام

إذا كنت قد قرأت هذا الحد ، فقد تكون مهتمًا برؤية النظرية قيد التنفيذ. يمكن لعب اللعبة Teeworlds.leaningtech.com، جربها!


مباراة ودية بين الزملاء

يتوفر رمز مكتبة الشبكة مجانًا على جيثب. انضم إلى المحادثة على قناتنا مشبكة!

المصدر: www.habr.com

إضافة تعليق