Cheerp، WebRTC اور Firebase کے ساتھ ملٹی پلیئر گیم کو C++ سے ویب پر پورٹ کرنا

تعارف

ہماری کمپنی جھکاؤ والی ٹیکنالوجیز روایتی ڈیسک ٹاپ ایپلی کیشنز کو ویب پر پورٹ کرنے کے لیے حل فراہم کرتا ہے۔ ہمارا C++ مرتب کرنے والا خوش ہونا WebAssembly اور JavaScript کا ایک مجموعہ تیار کرتا ہے، جو دونوں فراہم کرتا ہے۔ سادہ براؤزر تعامل، اور اعلی کارکردگی.

اس کی درخواست کی ایک مثال کے طور پر، ہم نے ایک ملٹی پلیئر گیم کو ویب پر پورٹ کرنے کا فیصلہ کیا اور انتخاب کیا۔ Teeworlds. Teeworlds ایک ملٹی پلیئر 2D ریٹرو گیم ہے جس میں کھلاڑیوں کی ایک چھوٹی لیکن فعال کمیونٹی ہے (بشمول میں!) یہ ڈاؤن لوڈ کردہ وسائل اور CPU اور GPU کی ضروریات کے لحاظ سے چھوٹا ہے - ایک مثالی امیدوار۔

Cheerp، WebRTC اور Firebase کے ساتھ ملٹی پلیئر گیم کو C++ سے ویب پر پورٹ کرنا
Teeworlds براؤزر میں چل رہا ہے۔

ہم نے تجربہ کرنے کے لیے اس پروجیکٹ کو استعمال کرنے کا فیصلہ کیا۔ نیٹ ورک کوڈ کو ویب پر پورٹ کرنے کے عمومی حل. یہ عام طور پر مندرجہ ذیل طریقوں سے کیا جاتا ہے:

  • XMLHttpRequest/ftch، اگر نیٹ ورک کا حصہ صرف HTTP درخواستوں پر مشتمل ہے، یا
  • ویب ساکٹس۔.

دونوں حلوں کے لیے سرور کی طرف سرور کے جزو کی میزبانی کی ضرورت ہوتی ہے، اور نہ ہی ٹرانسپورٹ پروٹوکول کے طور پر استعمال کی اجازت دیتا ہے۔ UDP. یہ ریئل ٹائم ایپلی کیشنز جیسے ویڈیو کانفرنسنگ سوفٹ ویئر اور گیمز کے لیے اہم ہے، کیونکہ یہ پروٹوکول پیکٹ کی ترسیل اور آرڈر کی ضمانت دیتا ہے۔ ٹی سی پی کم تاخیر میں رکاوٹ بن سکتی ہے۔

ایک تیسرا طریقہ ہے - براؤزر سے نیٹ ورک استعمال کریں: WebRTC.

RTCDataChannel یہ قابل اعتماد اور ناقابل اعتبار ٹرانسمیشن دونوں کو سپورٹ کرتا ہے (بعد میں جب بھی ممکن ہو UDP کو بطور ٹرانسپورٹ پروٹوکول استعمال کرنے کی کوشش کرتا ہے)، اور اسے ریموٹ سرور اور براؤزرز کے درمیان استعمال کیا جا سکتا ہے۔ اس کا مطلب ہے کہ ہم سرور کے اجزاء سمیت پوری ایپلیکیشن کو براؤزر میں پورٹ کر سکتے ہیں!

تاہم، یہ ایک اضافی دشواری کے ساتھ آتا ہے: اس سے پہلے کہ WebRTC کے دو ساتھی بات چیت کر سکیں، انہیں جڑنے کے لیے نسبتاً پیچیدہ مصافحہ کرنے کی ضرورت ہوتی ہے، جس کے لیے کئی فریق ثالث اداروں (ایک سگنلنگ سرور اور ایک یا زیادہ سرورز) کی ضرورت ہوتی ہے۔ اسٹن/ٹرن).

مثالی طور پر، ہم ایک ایسا نیٹ ورک API بنانا چاہیں گے جو WebRTC کو اندرونی طور پر استعمال کرے، لیکن UDP Sockets کے انٹرفیس کے جتنا ممکن ہو اس کے قریب ہو جس کو کنکشن قائم کرنے کی ضرورت نہیں ہے۔

یہ ہمیں ایپلیکیشن کوڈ پر پیچیدہ تفصیلات ظاہر کیے بغیر WebRTC کا فائدہ اٹھانے کی اجازت دے گا (جسے ہم اپنے پروجیکٹ میں کم سے کم تبدیل کرنا چاہتے تھے)۔

کم از کم WebRTC

WebRTC براؤزرز میں دستیاب APIs کا ایک سیٹ ہے جو آڈیو، ویڈیو اور صوابدیدی ڈیٹا کی پیئر ٹو پیئر ٹرانسمیشن فراہم کرتا ہے۔

ICE نامی میکانزم کے ذریعے STUN اور/یا ٹرن سرورز کا استعمال کرتے ہوئے ساتھیوں کے درمیان رابطہ قائم ہوتا ہے (چاہے ایک یا دونوں طرف NAT ہو)۔ ساتھی SDP پروٹوکول کی پیشکش اور جواب کے ذریعے ICE معلومات اور چینل کے پیرامیٹرز کا تبادلہ کرتے ہیں۔

زبردست! ایک وقت میں کتنے مخففات؟ آئیے مختصراً بتاتے ہیں کہ ان اصطلاحات کا کیا مطلب ہے:

  • NAT کے لیے سیشن ٹراورسل یوٹیلیٹیز (اسٹن) - NAT کو نظرانداز کرنے اور میزبان کے ساتھ براہ راست ڈیٹا کے تبادلے کے لیے ایک جوڑا (IP، پورٹ) حاصل کرنے کے لیے ایک پروٹوکول۔ اگر وہ اپنا کام مکمل کرنے کا انتظام کرتا ہے، تو ساتھی آزادانہ طور پر ایک دوسرے کے ساتھ ڈیٹا کا تبادلہ کر سکتے ہیں۔
  • NAT کے ارد گرد ریلے کا استعمال کرتے ہوئے ٹراورسل (ٹرن) NAT ٹراورسل کے لیے بھی استعمال ہوتا ہے، لیکن یہ ایک پراکسی کے ذریعے ڈیٹا کو آگے بھیج کر اس کو لاگو کرتا ہے جو دونوں ہم عمروں کو نظر آتا ہے۔ یہ تاخیر کا اضافہ کرتا ہے اور STUN کے مقابلے میں لاگو کرنا زیادہ مہنگا ہے (کیونکہ یہ پورے مواصلاتی سیشن میں لاگو ہوتا ہے)، لیکن بعض اوقات یہ واحد آپشن ہوتا ہے۔
  • انٹرایکٹو کنیکٹیویٹی اسٹیبلشمنٹ (ICE) براہ راست جوڑنے والے ساتھیوں سے حاصل کردہ معلومات کے ساتھ ساتھ STUN اور ٹرن سرورز کی کسی بھی تعداد سے موصول ہونے والی معلومات کی بنیاد پر دو ساتھیوں کو جوڑنے کا بہترین ممکنہ طریقہ منتخب کرنے کے لیے استعمال کیا جاتا ہے۔
  • سیشن کی تفصیل پروٹوکول (ایس ڈی پی) کنکشن چینل کے پیرامیٹرز کو بیان کرنے کا ایک فارمیٹ ہے، مثال کے طور پر، ICE امیدوار، ملٹی میڈیا کوڈیکس (آڈیو/ویڈیو چینل کی صورت میں)، وغیرہ... ساتھیوں میں سے ایک SDP پیشکش بھیجتا ہے، اور دوسرا SDP جواب کے ساتھ جواب دیتا ہے۔ . اس کے بعد ایک چینل بنتا ہے۔

ایسا کنکشن بنانے کے لیے، ساتھیوں کو STUN اور ٹرن سرورز سے موصول ہونے والی معلومات کو اکٹھا کرنے اور ایک دوسرے کے ساتھ اس کا تبادلہ کرنے کی ضرورت ہے۔

مسئلہ یہ ہے کہ ان کے پاس ابھی تک براہ راست بات چیت کرنے کی صلاحیت نہیں ہے، لہذا اس ڈیٹا کے تبادلے کے لیے ایک آؤٹ آف بینڈ میکانزم موجود ہونا چاہیے: ایک سگنلنگ سرور۔

سگنلنگ سرور بہت آسان ہو سکتا ہے کیونکہ اس کا واحد کام ہینڈ شیک مرحلے میں ساتھیوں کے درمیان ڈیٹا کو آگے بڑھانا ہے (جیسا کہ نیچے دی گئی تصویر میں دکھایا گیا ہے)۔

Cheerp، WebRTC اور Firebase کے ساتھ ملٹی پلیئر گیم کو C++ سے ویب پر پورٹ کرنا
آسان WebRTC ہینڈ شیک ترتیب خاکہ

Teeworlds نیٹ ورک ماڈل کا جائزہ

Teeworlds نیٹ ورک فن تعمیر بہت آسان ہے:

  • کلائنٹ اور سرور کے اجزاء دو مختلف پروگرام ہیں۔
  • کلائنٹ کئی سرورز میں سے ایک سے جڑ کر گیم میں داخل ہوتے ہیں، جن میں سے ہر ایک ایک وقت میں صرف ایک گیم کی میزبانی کرتا ہے۔
  • گیم میں تمام ڈیٹا کی منتقلی سرور کے ذریعے کی جاتی ہے۔
  • گیم کلائنٹ میں دکھائے جانے والے تمام پبلک سرورز کی فہرست جمع کرنے کے لیے ایک خاص ماسٹر سرور استعمال کیا جاتا ہے۔

ڈیٹا ایکسچینج کے لیے WebRTC کے استعمال کی بدولت، ہم گیم کے سرور جزو کو اس براؤزر میں منتقل کر سکتے ہیں جہاں کلائنٹ موجود ہے۔ یہ ہمیں ایک بہترین موقع فراہم کرتا ہے...

سرورز سے چھٹکارا حاصل کریں۔

سرور لاجک کی کمی کا ایک اچھا فائدہ ہے: ہم مکمل ایپلیکیشن کو جامد مواد کے طور پر Github Pages پر یا Cloudflare کے پیچھے اپنے ہارڈ ویئر پر لگا سکتے ہیں، اس طرح مفت میں تیز ڈاؤن لوڈز اور اعلی اپ ٹائم کو یقینی بنایا جا سکتا ہے۔ درحقیقت، ہم ان کے بارے میں بھول سکتے ہیں، اور اگر ہم خوش قسمت ہیں اور کھیل مقبول ہو جاتا ہے، تو بنیادی ڈھانچے کو جدید بنانے کی ضرورت نہیں ہوگی.

تاہم، نظام کے کام کرنے کے لیے، ہمیں اب بھی ایک بیرونی فن تعمیر کا استعمال کرنا ہوگا:

  • ایک یا زیادہ STUN سرورز: ہمارے پاس انتخاب کرنے کے لیے کئی مفت اختیارات ہیں۔
  • کم از کم ایک ٹرن سرور: یہاں کوئی مفت اختیارات نہیں ہیں، لہذا ہم یا تو اپنا سیٹ اپ کرسکتے ہیں یا سروس کے لیے ادائیگی کرسکتے ہیں۔ خوش قسمتی سے، زیادہ تر وقت کنکشن STUN سرورز کے ذریعے قائم کیا جا سکتا ہے (اور صحیح p2p فراہم کرتے ہیں)، لیکن فال بیک آپشن کے طور پر ٹرن کی ضرورت ہے۔
  • سگنلنگ سرور: دیگر دو پہلوؤں کے برعکس، سگنلنگ معیاری نہیں ہے۔ سگنلنگ سرور اصل میں کس چیز کے لیے ذمہ دار ہو گا اس کا انحصار کسی حد تک درخواست پر ہے۔ ہمارے معاملے میں، کنکشن قائم کرنے سے پہلے، تھوڑی مقدار میں ڈیٹا کا تبادلہ کرنا ضروری ہے۔
  • Teeworlds Master Server: اسے دوسرے سرورز اپنے وجود کی تشہیر کے لیے اور کلائنٹس کے ذریعے عوامی سرورز تلاش کرنے کے لیے استعمال کرتے ہیں۔ اگرچہ اس کی ضرورت نہیں ہے (کلائنٹ ہمیشہ اس سرور سے جڑ سکتے ہیں جس کے بارے میں وہ دستی طور پر جانتے ہیں)، یہ اچھا ہو گا کہ کھلاڑی بے ترتیب لوگوں کے ساتھ گیمز میں حصہ لے سکیں۔

ہم نے گوگل کے مفت STUN سرورز استعمال کرنے کا فیصلہ کیا، اور خود ایک ٹرن سرور تعینات کیا۔

آخری دو پوائنٹس کے لیے ہم نے استعمال کیا۔ فائر بیس:

  • Teeworlds ماسٹر سرور کو بہت آسانی سے لاگو کیا گیا ہے: ہر ایک فعال سرور کی معلومات (نام، IP، نقشہ، موڈ، ...) پر مشتمل اشیاء کی فہرست کے طور پر۔ سرورز اپنے آبجیکٹ کو شائع اور اپ ڈیٹ کرتے ہیں، اور کلائنٹ پوری فہرست لیتے ہیں اور اسے پلیئر پر ڈسپلے کرتے ہیں۔ ہم ہوم پیج پر فہرست کو HTML کے طور پر بھی ڈسپلے کرتے ہیں تاکہ کھلاڑی آسانی سے سرور پر کلک کر سکیں اور سیدھے گیم پر لے جا سکیں۔
  • سگنلنگ کا ہمارے ساکٹ کے نفاذ سے گہرا تعلق ہے، جسے اگلے حصے میں بیان کیا گیا ہے۔

Cheerp، WebRTC اور Firebase کے ساتھ ملٹی پلیئر گیم کو C++ سے ویب پر پورٹ کرنا
گیم کے اندر اور ہوم پیج پر سرورز کی فہرست

ساکٹ کا نفاذ

ہم ایک ایسا API بنانا چاہتے ہیں جو Posix UDP Sockets کے زیادہ سے زیادہ قریب ہو تاکہ ضروری تبدیلیوں کی تعداد کو کم سے کم کیا جا سکے۔

ہم نیٹ ورک پر آسان ترین ڈیٹا ایکسچینج کے لیے ضروری کم از کم لاگو کرنا بھی چاہتے ہیں۔

مثال کے طور پر، ہمیں حقیقی روٹنگ کی ضرورت نہیں ہے: تمام ہم عمر ایک ہی "ورچوئل LAN" پر ہیں جو ایک مخصوص Firebase ڈیٹا بیس مثال سے وابستہ ہیں۔

لہذا، ہمیں منفرد IP پتوں کی ضرورت نہیں ہے: منفرد Firebase کلیدی اقدار (ڈومین ناموں سے ملتی جلتی) ساتھیوں کی منفرد شناخت کرنے کے لیے کافی ہیں، اور ہر ہم مرتبہ مقامی طور پر ہر کلید کو "جعلی" IP پتے تفویض کرتا ہے جس کا ترجمہ کرنے کی ضرورت ہے۔ یہ عالمی IP ایڈریس تفویض کی ضرورت کو مکمل طور پر ختم کر دیتا ہے، جو کہ ایک غیر معمولی کام ہے۔

یہ کم از کم API ہے جسے ہمیں لاگو کرنے کی ضرورت ہے:

// 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);

API سادہ اور Posix Sockets API سے ملتا جلتا ہے، لیکن اس میں کچھ اہم اختلافات ہیں: کال بیکس کو لاگ کرنا، مقامی IPs تفویض کرنا، اور سست روابط.

کال بیکس کو رجسٹر کرنا

یہاں تک کہ اگر اصل پروگرام نان بلاکنگ I/O استعمال کرتا ہے، کوڈ کو ویب براؤزر میں چلانے کے لیے ری فیکٹر ہونا چاہیے۔

اس کی وجہ یہ ہے کہ براؤزر میں ایونٹ لوپ پروگرام سے پوشیدہ ہے (وہ 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 تفویض

ہمارے "نیٹ ورک" میں نوڈ ID IP ایڈریس نہیں ہیں، بلکہ Firebase کیز ہیں (وہ تاریں ہیں جو اس طرح نظر آتی ہیں: -LmEC50PYZLCiCP-vqde ).

یہ آسان ہے کیونکہ ہمیں آئی پی کو تفویض کرنے اور ان کی انفرادیت کو جانچنے کے لیے کسی طریقہ کار کی ضرورت نہیں ہے (نیز کلائنٹ کے منقطع ہونے کے بعد ان کو ٹھکانے لگانے کے لیے)، لیکن اکثر عددی قدر کے حساب سے ہم عمر افراد کی شناخت کرنا ضروری ہوتا ہے۔

یہ بالکل وہی ہے جس کے لئے افعال استعمال ہوتے ہیں۔ resolve и reverseResolve: ایپلیکیشن کسی نہ کسی طرح کلید کی سٹرنگ ویلیو (صارف کے ان پٹ کے ذریعے یا ماسٹر سرور کے ذریعے) وصول کرتی ہے، اور اسے اندرونی استعمال کے لیے IP ایڈریس میں تبدیل کر سکتی ہے۔ باقی API کو بھی سادگی کے لیے سٹرنگ کے بجائے یہ قدر ملتی ہے۔

یہ DNS تلاش کی طرح ہے، لیکن کلائنٹ پر مقامی طور پر انجام دیا جاتا ہے۔

یعنی آئی پی ایڈریسز کو مختلف کلائنٹس کے درمیان شیئر نہیں کیا جا سکتا، اور اگر کسی قسم کے عالمی شناخت کنندہ کی ضرورت ہو تو اسے مختلف طریقے سے تیار کرنا پڑے گا۔

سست کنکشن

UDP کو کنکشن کی ضرورت نہیں ہے، لیکن جیسا کہ ہم نے دیکھا ہے، WebRTC کو دو ہم عمروں کے درمیان ڈیٹا کی منتقلی شروع کرنے سے پہلے ایک طویل کنکشن کے عمل کی ضرورت ہوتی ہے۔

اگر ہم تجرید کی ایک ہی سطح فراہم کرنا چاہتے ہیں، (sendto/recvfrom بغیر کسی پیشگی کنکشن کے صوابدیدی ساتھیوں کے ساتھ)، پھر انہیں API کے اندر ایک "سست" (تاخیر) کنکشن انجام دینا ہوگا۔

UDP استعمال کرتے وقت "سرور" اور "کلائنٹ" کے درمیان معمول کے رابطے کے دوران یہی ہوتا ہے، اور ہماری لائبریری کو کیا کرنا چاہیے:

  • سرور کالز bind()آپریٹنگ سسٹم کو بتانا کہ وہ مخصوص پورٹ پر پیکٹ وصول کرنا چاہتا ہے۔

اس کے بجائے، ہم سرور کی کے تحت فائربیس پر ایک کھلا پورٹ شائع کریں گے اور اس کے ذیلی درخت میں ہونے والے واقعات سنیں گے۔

  • سرور کالز recvfrom()اس پورٹ پر کسی بھی میزبان سے آنے والے پیکٹوں کو قبول کرنا۔

ہمارے معاملے میں، ہمیں اس بندرگاہ پر بھیجے گئے پیکٹوں کی آنے والی قطار کو چیک کرنے کی ضرورت ہے۔

ہر بندرگاہ کی اپنی قطار ہوتی ہے، اور ہم WebRTC ڈیٹاگرام کے آغاز میں ماخذ اور منزل کی بندرگاہوں کو شامل کرتے ہیں تاکہ ہم جان سکیں کہ نیا پیکٹ آنے پر کس قطار کو آگے بڑھانا ہے۔

کال غیر مسدود ہے، لہذا اگر کوئی پیکٹ نہیں ہیں، تو ہم صرف -1 واپس کرتے ہیں اور سیٹ کرتے ہیں۔ errno=EWOULDBLOCK.

  • کلائنٹ سرور کا IP اور پورٹ کچھ بیرونی ذرائع سے وصول کرتا ہے اور کال کرتا ہے۔ sendto(). یہ اندرونی کال بھی کرتا ہے۔ bind()اس لیے بعد میں recvfrom() واضح طور پر پابند کیے بغیر جواب موصول ہوگا۔

ہمارے معاملے میں، کلائنٹ بیرونی طور پر سٹرنگ کلید حاصل کرتا ہے اور فنکشن کا استعمال کرتا ہے۔ resolve() آئی پی ایڈریس حاصل کرنے کے لیے۔

اس مقام پر، ہم WebRTC ہینڈ شیک شروع کرتے ہیں اگر دونوں ہم عمر افراد ابھی تک ایک دوسرے سے جڑے نہیں ہیں۔ ایک ہی ہم مرتبہ کے مختلف پورٹس سے کنکشن ایک ہی WebRTC DataChannel کا استعمال کرتے ہیں۔

ہم بھی بالواسطہ کارکردگی کا مظاہرہ کرتے ہیں۔ bind()تاکہ سرور اگلے میں دوبارہ رابطہ کر سکے۔ sendto() کسی وجہ سے بند ہونے کی صورت میں۔

سرور کو کلائنٹ کے کنکشن کے بارے میں مطلع کیا جاتا ہے جب کلائنٹ Firebase میں سرور پورٹ کی معلومات کے تحت اپنی SDP پیشکش لکھتا ہے، اور سرور وہاں اپنے جواب کے ساتھ جواب دیتا ہے۔

نیچے دیا گیا خاکہ ساکٹ اسکیم کے لیے پیغام کے بہاؤ اور کلائنٹ سے سرور تک پہلے پیغام کی ترسیل کی ایک مثال دکھاتا ہے:

Cheerp، WebRTC اور Firebase کے ساتھ ملٹی پلیئر گیم کو C++ سے ویب پر پورٹ کرنا
کلائنٹ اور سرور کے درمیان کنکشن کے مرحلے کا مکمل خاکہ

حاصل يہ ہوا

اگر آپ نے ابھی تک پڑھا ہے تو، آپ شاید نظریہ کو عملی طور پر دیکھنے میں دلچسپی رکھتے ہیں۔ پر کھیل کھیلا جا سکتا ہے۔ teeworlds.leaningtech.com، کوشش کرو!


ساتھیوں کے درمیان دوستانہ میچ

نیٹ ورک لائبریری کوڈ مفت میں دستیاب ہے۔ Github کے. ہمارے چینل پر گفتگو میں شامل ہوں۔ گرڈ!

ماخذ: www.habr.com

نیا تبصرہ شامل کریں