Memindahkan permainan berbilang pemain daripada C++ ke web dengan Cheerp, WebRTC dan Firebase

Pengenalan

syarikat kita Teknologi Bersandar menyediakan penyelesaian untuk mengalihkan aplikasi desktop tradisional ke web. Pengkompil C++ kami sorak menjana gabungan WebAssembly dan JavaScript, yang menyediakan kedua-duanya interaksi pelayar mudah, dan prestasi tinggi.

Sebagai contoh aplikasinya, kami memutuskan untuk memindahkan permainan berbilang pemain ke web dan memilih Teeworlds. Teeworlds ialah permainan retro 2D berbilang pemain dengan komuniti pemain yang kecil tetapi aktif (termasuk saya!). Ia kecil dari segi sumber yang dimuat turun dan keperluan CPU dan GPU - calon yang ideal.

Memindahkan permainan berbilang pemain daripada C++ ke web dengan Cheerp, WebRTC dan Firebase
Berjalan dalam pelayar Teeworlds

Kami memutuskan untuk menggunakan projek ini untuk mencuba penyelesaian umum untuk mengalihkan kod rangkaian ke web. Ini biasanya dilakukan dengan cara berikut:

  • XMLHttpRequest/fetch, jika bahagian rangkaian hanya terdiri daripada permintaan HTTP, atau
  • Soket Web.

Kedua-dua penyelesaian memerlukan pengehosan komponen pelayan di bahagian pelayan, dan kedua-duanya tidak membenarkan untuk digunakan sebagai protokol pengangkutan UDP. Ini penting untuk aplikasi masa nyata seperti perisian dan permainan persidangan video, kerana ia menjamin penghantaran dan susunan paket protokol TCP mungkin menjadi penghalang kepada kependaman rendah.

Terdapat cara ketiga - gunakan rangkaian dari penyemak imbas: WebRTC.

RTCDataChannel Ia menyokong kedua-dua penghantaran yang boleh dipercayai dan tidak boleh dipercayai (dalam kes kedua ia cuba menggunakan UDP sebagai protokol pengangkutan apabila boleh), dan boleh digunakan dengan pelayan jauh dan antara penyemak imbas. Ini bermakna kita boleh mengalihkan keseluruhan aplikasi ke penyemak imbas, termasuk komponen pelayan!

Walau bagaimanapun, ini datang dengan kesukaran tambahan: sebelum dua rakan WebRTC boleh berkomunikasi, mereka perlu melakukan jabat tangan yang agak kompleks untuk menyambung, yang memerlukan beberapa entiti pihak ketiga (pelayan isyarat dan satu atau lebih pelayan MENYEBABKAN PENGSAN/TUKAR).

Sebaik-baiknya, kami ingin mencipta API rangkaian yang menggunakan WebRTC secara dalaman, tetapi sedekat mungkin dengan antara muka Soket UDP yang tidak perlu membuat sambungan.

Ini akan membolehkan kami memanfaatkan WebRTC tanpa perlu mendedahkan butiran kompleks kepada kod aplikasi (yang kami mahu ubah sesedikit mungkin dalam projek kami).

WebRTC minimum

WebRTC ialah satu set API yang tersedia dalam penyemak imbas yang menyediakan penghantaran audio, video dan data arbitrari rakan ke rakan.

Sambungan antara rakan sebaya diwujudkan (walaupun terdapat NAT pada satu atau kedua-dua belah pihak) menggunakan pelayan STUN dan/atau TURN melalui mekanisme yang dipanggil ICE. Rakan sebaya bertukar maklumat ICE dan parameter saluran melalui tawaran dan jawapan protokol SDP.

Wah! Berapa banyak singkatan pada satu masa? Mari kita terangkan secara ringkas apa maksud istilah ini:

  • Utiliti Traversal Sesi untuk NAT (MENYEBABKAN PENGSAN) β€” protokol untuk memintas NAT dan mendapatkan pasangan (IP, port) untuk bertukar-tukar data terus dengan hos. Jika dia berjaya menyelesaikan tugasnya, rakan sebaya boleh bertukar-tukar data secara bebas antara satu sama lain.
  • Traversal Menggunakan Geganti di sekitar NAT (TUKAR) juga digunakan untuk traversal NAT, tetapi ia melaksanakan ini dengan memajukan data melalui proksi yang boleh dilihat oleh kedua-dua rakan sebaya. Ia menambah kependaman dan lebih mahal untuk dilaksanakan daripada STUN (kerana ia digunakan sepanjang keseluruhan sesi komunikasi), tetapi kadangkala ia adalah satu-satunya pilihan.
  • Penubuhan Ketersambungan Interaktif (ICE) digunakan untuk memilih kaedah terbaik untuk menyambungkan dua rakan sebaya berdasarkan maklumat yang diperoleh daripada menyambungkan rakan sebaya secara langsung, serta maklumat yang diterima oleh mana-mana bilangan pelayan STUN dan TURN.
  • Protokol Penerangan Sesi (SDP) ialah format untuk menerangkan parameter saluran sambungan, contohnya, calon ICE, codec multimedia (dalam kes saluran audio/video), dsb... Salah seorang rakan sebaya menghantar Tawaran SDP, dan yang kedua membalas dengan Jawapan SDP . Selepas ini, saluran dibuat.

Untuk membuat sambungan sedemikian, rakan sebaya perlu mengumpul maklumat yang mereka terima daripada pelayan STUN dan TURN dan menukarnya antara satu sama lain.

Masalahnya ialah mereka belum mempunyai keupayaan untuk berkomunikasi secara langsung, jadi mekanisme luar jalur mesti wujud untuk menukar data ini: pelayan isyarat.

Pelayan isyarat boleh menjadi sangat mudah kerana tugasnya hanya untuk memajukan data antara rakan sebaya dalam fasa jabat tangan (seperti ditunjukkan dalam rajah di bawah).

Memindahkan permainan berbilang pemain daripada C++ ke web dengan Cheerp, WebRTC dan Firebase
Gambarajah jujukan jabat tangan WebRTC yang dipermudahkan

Gambaran Keseluruhan Model Rangkaian Teeworlds

Seni bina rangkaian Teeworlds sangat mudah:

  • Komponen klien dan pelayan adalah dua program yang berbeza.
  • Pelanggan memasuki permainan dengan menyambung ke salah satu daripada beberapa pelayan, yang setiap satunya menganjurkan satu permainan pada satu masa.
  • Semua pemindahan data dalam permainan dijalankan melalui pelayan.
  • Pelayan induk khas digunakan untuk mengumpul senarai semua pelayan awam yang dipaparkan dalam klien permainan.

Terima kasih kepada penggunaan WebRTC untuk pertukaran data, kami boleh memindahkan komponen pelayan permainan ke penyemak imbas di mana pelanggan berada. Ini memberi kita peluang besar...

Buang pelayan

Kekurangan logik pelayan mempunyai kelebihan yang bagus: kami boleh menggunakan keseluruhan aplikasi sebagai kandungan statik pada Halaman Github atau pada perkakasan kami sendiri di belakang Cloudflare, sekali gus memastikan muat turun pantas dan masa aktif yang tinggi secara percuma. Malah, kita boleh melupakan mereka, dan jika kita bernasib baik dan permainan menjadi popular, maka infrastruktur tidak perlu dimodenkan.

Walau bagaimanapun, untuk sistem berfungsi, kita masih perlu menggunakan seni bina luaran:

  • Satu atau lebih pelayan STUN: Kami mempunyai beberapa pilihan percuma untuk dipilih.
  • Sekurang-kurangnya satu pelayan TURN: tiada pilihan percuma di sini, jadi kami boleh menyediakan sendiri atau membayar perkhidmatan tersebut. Mujurlah, kebanyakan masa sambungan boleh diwujudkan melalui pelayan STUN (dan menyediakan p2p benar), tetapi TURN diperlukan sebagai pilihan sandaran.
  • Pelayan Isyarat: Tidak seperti dua aspek lain, isyarat tidak diseragamkan. Perkara yang sebenarnya akan bertanggungjawab oleh pelayan isyarat bergantung pada aplikasi. Dalam kes kami, sebelum membuat sambungan, adalah perlu untuk menukar sejumlah kecil data.
  • Pelayan Induk Teeworlds: Ia digunakan oleh pelayan lain untuk mengiklankan kewujudan mereka dan oleh pelanggan untuk mencari pelayan awam. Walaupun ia tidak diperlukan (pelanggan sentiasa boleh menyambung ke pelayan yang mereka ketahui secara manual), adalah bagus untuk mempunyai supaya pemain boleh mengambil bahagian dalam permainan dengan orang rawak.

Kami memutuskan untuk menggunakan pelayan STUN percuma Google dan menggunakan satu pelayan TURN sendiri.

Untuk dua mata terakhir yang kami gunakan Firebase:

  • Pelayan induk Teeworlds dilaksanakan dengan sangat mudah: sebagai senarai objek yang mengandungi maklumat (nama, IP, peta, mod, ...) bagi setiap pelayan aktif. Pelayan menerbitkan dan mengemas kini objek mereka sendiri, dan pelanggan mengambil keseluruhan senarai dan memaparkannya kepada pemain. Kami juga memaparkan senarai di halaman utama sebagai HTML supaya pemain hanya boleh mengklik pada pelayan dan dibawa terus ke permainan.
  • Isyarat berkait rapat dengan pelaksanaan soket kami, diterangkan dalam bahagian seterusnya.

Memindahkan permainan berbilang pemain daripada C++ ke web dengan Cheerp, WebRTC dan Firebase
Senarai pelayan di dalam permainan dan di halaman utama

Pelaksanaan soket

Kami ingin mencipta API yang sehampir mungkin dengan Soket UDP Posix untuk meminimumkan bilangan perubahan yang diperlukan.

Kami juga ingin melaksanakan minimum yang diperlukan untuk pertukaran data paling mudah melalui rangkaian.

Sebagai contoh, kami tidak memerlukan penghalaan sebenar: semua rakan sebaya berada pada "LAN maya" yang sama yang dikaitkan dengan tika pangkalan data Firebase tertentu.

Oleh itu, kami tidak memerlukan alamat IP unik: nilai kunci Firebase yang unik (serupa dengan nama domain) mencukupi untuk mengenal pasti rakan sebaya secara unik dan setiap rakan setara secara setempat memberikan alamat IP "palsu" kepada setiap kunci yang perlu diterjemahkan. Ini menghapuskan sepenuhnya keperluan untuk penetapan alamat IP global, yang merupakan tugas yang tidak remeh.

Berikut ialah API minimum yang perlu kami laksanakan:

// 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 ini mudah dan serupa dengan Posix Sockets API, tetapi mempunyai beberapa perbezaan penting: log panggilan balik, menetapkan IP tempatan dan sambungan malas.

Mendaftar Panggilan Balik

Walaupun program asal menggunakan I/O yang tidak menyekat, kod mesti difaktorkan semula untuk dijalankan dalam pelayar web.

Sebabnya ialah gelung acara dalam penyemak imbas disembunyikan daripada program (sama ada JavaScript atau WebAssembly).

Dalam persekitaran asli kita boleh menulis kod seperti ini

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;
    ...
  }
  ...
}

Jika gelung acara disembunyikan kepada kita, maka kita perlu mengubahnya menjadi seperti ini:

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

Tugasan IP tempatan

ID nod dalam "rangkaian" kami bukan alamat IP, tetapi kunci Firebase (ia adalah rentetan yang kelihatan seperti ini: -LmEC50PYZLCiCP-vqde ).

Ini mudah kerana kami tidak memerlukan mekanisme untuk menetapkan IP dan menyemak keunikannya (serta melupuskannya selepas pelanggan memutuskan sambungan), tetapi selalunya perlu untuk mengenal pasti rakan sebaya dengan nilai angka.

Inilah sebenarnya fungsi yang digunakan. resolve ΠΈ reverseResolve: Aplikasi entah bagaimana menerima nilai rentetan kunci (melalui input pengguna atau melalui pelayan induk), dan boleh menukarnya kepada alamat IP untuk kegunaan dalaman. Selebihnya API juga menerima nilai ini dan bukannya rentetan untuk kesederhanaan.

Ini serupa dengan carian DNS, tetapi dilakukan secara tempatan pada klien.

Iaitu, alamat IP tidak boleh dikongsi antara pelanggan yang berbeza, dan jika beberapa jenis pengecam global diperlukan, ia perlu dijana dengan cara yang berbeza.

Sambungan malas

UDP tidak memerlukan sambungan, tetapi seperti yang telah kita lihat, WebRTC memerlukan proses sambungan yang panjang sebelum ia boleh mula memindahkan data antara dua rakan sebaya.

Jika kita ingin menyediakan tahap abstraksi yang sama, (sendto/recvfrom dengan rakan sebaya tanpa sambungan terlebih dahulu), maka mereka mesti melakukan sambungan "malas" (tertunda) di dalam API.

Inilah yang berlaku semasa komunikasi biasa antara "pelayan" dan "pelanggan" apabila menggunakan UDP, dan perkara yang perlu dilakukan oleh perpustakaan kami:

  • Panggilan pelayan bind()untuk memberitahu sistem pengendalian bahawa ia ingin menerima paket pada port yang ditentukan.

Sebaliknya, kami akan menerbitkan port terbuka ke Firebase di bawah kunci pelayan dan mendengar acara dalam subpokoknya.

  • Panggilan pelayan recvfrom(), menerima paket yang datang daripada mana-mana hos pada port ini.

Dalam kes kami, kami perlu menyemak baris gilir masuk paket yang dihantar ke port ini.

Setiap port mempunyai baris gilir sendiri, dan kami menambah port sumber dan destinasi pada permulaan datagram WebRTC supaya kami tahu baris gilir mana yang hendak dimajukan apabila paket baharu tiba.

Panggilan tidak menyekat, jadi jika tiada paket, kami hanya kembali -1 dan tetapkan errno=EWOULDBLOCK.

  • Pelanggan menerima IP dan port pelayan melalui beberapa cara luaran, dan panggilan sendto(). Ini juga membuat panggilan dalaman. bind(), oleh itu seterusnya recvfrom() akan menerima respons tanpa melaksanakan bind secara eksplisit.

Dalam kes kami, pelanggan secara luaran menerima kunci rentetan dan menggunakan fungsi tersebut resolve() untuk mendapatkan alamat IP.

Pada ketika ini, kami memulakan jabat tangan WebRTC jika kedua-dua rakan sebaya belum bersambung antara satu sama lain. Sambungan ke port berbeza dari rakan sebaya yang sama menggunakan Saluran Data WebRTC yang sama.

Kami juga melakukan secara tidak langsung bind()supaya pelayan boleh menyambung semula pada seterusnya sendto() sekiranya ia ditutup atas sebab tertentu.

Pelayan dimaklumkan tentang sambungan pelanggan apabila pelanggan menulis tawaran SDPnya di bawah maklumat port pelayan dalam Firebase dan pelayan bertindak balas dengan responsnya di sana.

Rajah di bawah menunjukkan contoh aliran mesej untuk skema soket dan penghantaran mesej pertama daripada klien ke pelayan:

Memindahkan permainan berbilang pemain daripada C++ ke web dengan Cheerp, WebRTC dan Firebase
Gambar rajah lengkap fasa sambungan antara klien dan pelayan

Kesimpulan

Jika anda telah membaca sejauh ini, anda mungkin berminat untuk melihat teori itu dalam tindakan. Permainan ini boleh dimainkan teeworlds.leaningtech.com, cuba ia!


Perlawanan persahabatan antara rakan sekerja

Kod perpustakaan rangkaian tersedia secara percuma di Github. Sertai perbualan di saluran kami di Gitter!

Sumber: www.habr.com

Tambah komen