Siirrä moninpeli C++:sta verkkoon Cheerpin, WebRTC:n ja Firebasen avulla

Esittely

yrityksemme Leaning Technologies tarjoaa ratkaisuja perinteisten työpöytäsovellusten siirtämiseen verkkoon. C++-kääntäjämme hurraa luo WebAssemblyn ja JavaScriptin yhdistelmän, joka tarjoaa molemmat yksinkertainen selaimen vuorovaikutus, ja korkea suorituskyky.

Esimerkkinä sen sovelluksesta päätimme siirtää moninpelin verkkoon ja valitsimme Teeworlds. Teeworlds on moninpeli XNUMXD-retropeli, jossa on pieni mutta aktiivinen pelaajayhteisö (mukaan lukien minä!). Se on pieni sekä ladattujen resurssien että CPU- ja GPU-vaatimusten suhteen - ihanteellinen ehdokas.

Siirrä moninpeli C++:sta verkkoon Cheerpin, WebRTC:n ja Firebasen avulla
Toimii Teeworld-selaimessa

Päätimme käyttää tätä projektia kokeiluun yleisiä ratkaisuja verkkokoodin siirtämiseen verkkoon. Tämä tehdään yleensä seuraavilla tavoilla:

  • XMLHttpRequest/fetch, jos verkko-osa koostuu vain HTTP-pyynnöistä, tai
  • WebSockets.

Molemmat ratkaisut edellyttävät palvelinkomponentin isännöintiä palvelinpuolella, eikä kumpikaan salli käyttöä siirtoprotokollana UDP. Tämä on tärkeää reaaliaikaisille sovelluksille, kuten videoneuvotteluohjelmistoille ja peleille, koska se takaa protokollapakettien toimituksen ja järjestyksen TCP voi olla este alhaiselle latenssille.

On kolmas tapa - käytä verkkoa selaimesta: WebRTC.

RTCDataChannel Se tukee sekä luotettavaa että epäluotettavaa lähetystä (jälkimmäisessä tapauksessa se yrittää käyttää UDP:tä siirtoprotokollana aina kun mahdollista), ja sitä voidaan käyttää sekä etäpalvelimen kanssa että selaimien välillä. Tämä tarkoittaa, että voimme siirtää koko sovelluksen selaimeen, mukaan lukien palvelinkomponentin!

Tähän liittyy kuitenkin lisävaikeus: ennen kuin kaksi WebRTC-vertaista voivat kommunikoida, heidän on suoritettava suhteellisen monimutkainen kättely muodostaakseen yhteyden, mikä vaatii useita kolmannen osapuolen kokonaisuuksia (signalointipalvelin ja yksi tai useampi palvelin). TAINNUTTAA/VUORO).

Ihannetapauksessa haluaisimme luoda verkkosovellusliittymän, joka käyttää WebRTC:tä sisäisesti, mutta on mahdollisimman lähellä UDP Sockets -liitäntää, jonka ei tarvitse muodostaa yhteyttä.

Tämä antaa meille mahdollisuuden hyödyntää WebRTC:tä ilman, että meidän tarvitsee paljastaa monimutkaisia ​​yksityiskohtia sovelluskoodille (jota halusimme muuttaa mahdollisimman vähän projektissamme).

Minimi WebRTC

WebRTC on joukko sovellusliittymiä, jotka ovat saatavilla selaimissa ja jotka mahdollistavat äänen, videon ja mielivaltaisen tiedon vertaissiirron.

Yhteys vertaisten välillä muodostetaan (vaikka toisella tai molemmilla puolilla on NAT) STUN- ja/tai TURN-palvelimilla ICE-nimisen mekanismin kautta. Vertailijat vaihtavat ICE-tietoja ja kanavaparametreja SDP-protokollan tarjouksen ja vastauksen kautta.

Vau! Kuinka monta lyhennettä kerralla? Selvitetään lyhyesti, mitä nämä termit tarkoittavat:

  • Session Traversal -apuohjelmat NAT:lle (TAINNUTTAA) — protokolla NAT:n ohittamiseksi ja parin (IP, portin) hankkimiseksi tietojen vaihtamiseksi suoraan isäntäkoneen kanssa. Jos hän onnistuu suorittamaan tehtävänsä, vertaiset voivat vaihtaa tietoja itsenäisesti keskenään.
  • Läpikulku releillä NAT:n ympärillä (VUORO) käytetään myös NAT-läpikulkuun, mutta se toteuttaa tämän välittämällä tiedot välityspalvelimen kautta, joka näkyy molemmille vertaisversioille. Se lisää viivettä ja on kalliimpaa toteuttaa kuin STUN (koska sitä käytetään koko viestintäistunnon ajan), mutta joskus se on ainoa vaihtoehto.
  • Interaktiivisen yhteyden perustaminen (ICE) käytetään valitsemaan paras mahdollinen tapa yhdistää kaksi vertaista suoraan yhdistäviltä yrityksiltä saatujen tietojen sekä minkä tahansa STUN- ja TURN-palvelimien vastaanottaman tiedon perusteella.
  • Istunnon kuvausprotokolla (SDP) on muoto, jolla kuvataan yhteyskanavan parametreja, esimerkiksi ICE-ehdokkaita, multimediakoodekkeja (ääni-/videokanavan tapauksessa) jne... Yksi vertaisista lähettää SDP-tarjouksen ja toinen vastaa SDP-vastauksella . Tämän jälkeen luodaan kanava.

Tällaisen yhteyden luomiseksi vertaiskäyttäjien on kerättävä STUN- ja TURN-palvelimista saamansa tiedot ja vaihdettava ne keskenään.

Ongelmana on, että niillä ei vielä ole kykyä kommunikoida suoraan, joten näiden tietojen vaihtamiseksi on oltava kaistan ulkopuolinen mekanismi: signalointipalvelin.

Signalointipalvelin voi olla hyvin yksinkertainen, koska sen ainoa tehtävä on välittää tietoa vertaisten välillä kättelyvaiheessa (kuten alla olevassa kaaviossa näkyy).

Siirrä moninpeli C++:sta verkkoon Cheerpin, WebRTC:n ja Firebasen avulla
Yksinkertaistettu WebRTC-kättelyjärjestyskaavio

Teeworlds-verkostomallin yleiskatsaus

Teeworlds-verkkoarkkitehtuuri on hyvin yksinkertainen:

  • Asiakas- ja palvelinkomponentit ovat kaksi eri ohjelmaa.
  • Asiakkaat pääsevät peliin muodostamalla yhteyden yhteen useista palvelimista, joista jokainen isännöi vain yhtä peliä kerrallaan.
  • Kaikki tiedonsiirto pelissä tapahtuu palvelimen kautta.
  • Erityistä pääpalvelinta käytetään keräämään luettelo kaikista julkisista palvelimista, jotka näkyvät peliohjelmassa.

WebRTC:n käytön ansiosta tiedonvaihtoon voimme siirtää pelin palvelinkomponentin selaimeen, jossa asiakas sijaitsee. Tämä antaa meille loistavan mahdollisuuden...

Päästä eroon palvelimista

Palvelinlogiikan puutteella on hieno etu: voimme ottaa koko sovelluksen käyttöön staattisena sisältönä Github Pagesilla tai omalla laitteistollamme Cloudflaren takana, mikä takaa nopeat lataukset ja korkean käytettävyyden ilmaiseksi. Itse asiassa voimme unohtaa ne, ja jos olemme onnekkaita ja pelistä tulee suosittu, infrastruktuuria ei tarvitse modernisoida.

Kuitenkin, jotta järjestelmä toimisi, meidän on silti käytettävä ulkoista arkkitehtuuria:

  • Yksi tai useampi STUN-palvelin: Valittavana on useita ilmaisia ​​vaihtoehtoja.
  • Vähintään yksi TURN-palvelin: täällä ei ole ilmaisia ​​vaihtoehtoja, joten voimme joko perustaa oman tai maksaa palvelun. Onneksi suurimman osan ajasta yhteys voidaan muodostaa STUN-palvelimien kautta (ja tarjota todellinen p2p), mutta TURN tarvitaan varavaihtoehtona.
  • Signalointipalvelin: Toisin kuin kaksi muuta aspektia, signalointia ei ole standardoitu. Se, mistä signalointipalvelin todella on vastuussa, riippuu jonkin verran sovelluksesta. Meidän tapauksessamme ennen yhteyden muodostamista on tarpeen vaihtaa pieni määrä tietoa.
  • Teeworlds Master Server: Muut palvelimet käyttävät sitä mainostaakseen olemassaoloaan ja asiakkaat löytääkseen julkisia palvelimia. Vaikka se ei ole pakollista (asiakkaat voivat aina muodostaa yhteyden palvelimeen, josta he tietävät manuaalisesti), olisi mukava saada, jotta pelaajat voivat osallistua peleihin satunnaisten ihmisten kanssa.

Päätimme käyttää Googlen ilmaisia ​​STUN-palvelimia ja otimme yhden TURN-palvelimen käyttöön itse.

Käytimme kaksi viimeistä pistettä Firebase:

  • Teeworlds-pääpalvelin on toteutettu hyvin yksinkertaisesti: luettelona objekteista, jotka sisältävät kunkin aktiivisen palvelimen tiedot (nimi, IP, kartta, tila, ...). Palvelimet julkaisevat ja päivittävät oman objektinsa, ja asiakkaat ottavat koko luettelon ja näyttävät sen soittimelle. Näytämme luettelon myös kotisivulla HTML-muodossa, jotta pelaajat voivat yksinkertaisesti klikata palvelinta ja siirtyä suoraan peliin.
  • Signalointi liittyy läheisesti socket-toteutukseen, joka kuvataan seuraavassa osiossa.

Siirrä moninpeli C++:sta verkkoon Cheerpin, WebRTC:n ja Firebasen avulla
Luettelo palvelimista pelin sisällä ja kotisivulla

Pistorasioiden toteutus

Haluamme luoda API:n, joka on mahdollisimman lähellä Posix UDP Socketsia, minimoidaksemme tarvittavien muutosten määrän.

Haluamme myös toteuttaa tarvittavan minimin yksinkertaisimpaan tiedonvaihtoon verkon yli.

Emme esimerkiksi tarvitse todellista reititystä: kaikki kumppanit ovat samassa "virtuaalisessa lähiverkossa", joka liittyy tiettyyn Firebase-tietokanta-instanssiin.

Siksi emme tarvitse yksilöllisiä IP-osoitteita: yksilölliset Firebase-avainarvot (samanlaiset kuin verkkotunnusten nimet) riittävät vertaisten yksilöimiseen, ja jokainen vertaiskumppani määrittää paikallisesti "väärennetyt" IP-osoitteet kullekin avaimelle, joka on käännettävä. Tämä poistaa kokonaan maailmanlaajuisen IP-osoitteen määrittämisen tarpeen, mikä ei ole triviaali tehtävä.

Tässä on vähimmäissovellusliittymä, joka meidän on otettava käyttöön:

// 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 on yksinkertainen ja samanlainen kuin Posix Sockets API, mutta sillä on muutamia tärkeitä eroja: takaisinsoittojen kirjaaminen, paikallisten IP-osoitteiden määrittäminen ja laiskoja yhteyksiä.

Takaisinsoittojen rekisteröinti

Vaikka alkuperäinen ohjelma käyttää estostamatonta I/O:ta, koodi on muutettava, jotta se toimisi verkkoselaimessa.

Syynä tähän on se, että selaimen tapahtumasilmukka on piilotettu ohjelmalta (oli se sitten JavaScript tai WebAssembly).

Alkuperäisessä ympäristössä voimme kirjoittaa koodia näin

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

Jos tapahtumasilmukka on meille piilotettu, meidän on muutettava se joksikin tältä:

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

Paikallinen IP-määritys

"Verkkomme" solmutunnukset eivät ole IP-osoitteita, vaan Firebase-avaimia (ne ovat merkkijonoja, jotka näyttävät tältä: -LmEC50PYZLCiCP-vqde ).

Tämä on kätevää, koska emme tarvitse mekanismia IP-osoitteiden määrittämiseen ja niiden ainutlaatuisuuden tarkistamiseen (sekä niiden hävittämiseen, kun asiakas katkaisee yhteyden), mutta usein on tarpeen tunnistaa vertaisarvot numeerisen arvon perusteella.

Juuri tähän toimintoja käytetään. resolve и reverseResolve: Sovellus vastaanottaa jotenkin avaimen merkkijonoarvon (käyttäjän syötteen tai pääpalvelimen kautta) ja voi muuntaa sen IP-osoitteeksi sisäistä käyttöä varten. Myös muu API vastaanottaa tämän arvon merkkijonon sijaan yksinkertaisuuden vuoksi.

Tämä on samanlainen kuin DNS-haku, mutta se suoritetaan paikallisesti asiakkaalle.

Eli IP-osoitteita ei voi jakaa eri asiakkaiden kesken, ja jos tarvitaan jonkinlainen globaali tunniste, se on generoitava eri tavalla.

Laiska yhteys

UDP ei tarvitse yhteyttä, mutta kuten olemme nähneet, WebRTC vaatii pitkän yhteysprosessin ennen kuin se voi aloittaa tiedonsiirron kahden vertaisohjelman välillä.

Jos haluamme tarjota saman tason abstraktiota, (sendto/recvfrom mielivaltaisten kumppanien kanssa ilman aiempaa yhteyttä), heidän on muodostettava "laiska" (viivästetty) yhteys API:n sisällä.

Näin tapahtuu normaalin viestinnän aikana "palvelimen" ja "asiakkaan" välillä UDP:tä käytettäessä, ja mitä kirjastomme tulisi tehdä:

  • Palvelimen puhelut bind()kertoa käyttöjärjestelmälle, että se haluaa vastaanottaa paketteja määritettyyn porttiin.

Sen sijaan julkaisemme avoimen portin Firebaseen palvelinavaimen alla ja kuuntelemme tapahtumia sen alipuussa.

  • Palvelimen puhelut recvfrom(), joka hyväksyy paketit, jotka tulevat mistä tahansa tämän portin isännästä.

Meidän tapauksessamme meidän on tarkistettava tähän porttiin lähetettyjen pakettien saapuva jono.

Jokaisella portilla on oma jononsa, ja lisäämme lähde- ja kohdeportit WebRTC-datagrammien alkuun, jotta tiedämme, mihin jonoon välitetään, kun uusi paketti saapuu.

Puhelu ei estä, joten jos paketteja ei ole, palaamme yksinkertaisesti -1 ja asetamme errno=EWOULDBLOCK.

  • Asiakas vastaanottaa palvelimen IP:n ja portin jollain ulkoisella tavalla ja soittaa sendto(). Tämä tekee myös sisäisen puhelun. bind(), joten myöhemmin recvfrom() vastaanottaa vastauksen suorittamatta nimenomaisesti sidontaa.

Meidän tapauksessamme asiakas vastaanottaa ulkoisesti merkkijonoavaimen ja käyttää toimintoa resolve() saadaksesi IP-osoitteen.

Tässä vaiheessa aloitamme WebRTC-kättelyn, jos kaksi vertaista eivät ole vielä yhteydessä toisiinsa. Yhteydet saman vertaisohjelman eri portteihin käyttävät samaa WebRTC DataChannelia.

Toimimme myös epäsuorasti bind()jotta palvelin voi muodostaa yhteyden uudelleen seuraavassa sendto() jos se jostain syystä sulkeutuu.

Palvelimelle ilmoitetaan asiakkaan yhteydestä, kun asiakas kirjoittaa SDP-tarjouksensa Firebasen palvelimen porttitietojen alle, ja palvelin vastaa vastauksellaan siellä.

Alla olevassa kaaviossa on esimerkki socket-mallin viestikulusta ja ensimmäisen viestin lähettämisestä asiakkaalta palvelimelle:

Siirrä moninpeli C++:sta verkkoon Cheerpin, WebRTC:n ja Firebasen avulla
Täydellinen kaavio asiakkaan ja palvelimen välisestä yhteysvaiheesta

Johtopäätös

Jos olet lukenut tähän asti, olet todennäköisesti kiinnostunut näkemään teorian käytännössä. Peliä voi pelata teeworlds.leaningtech.com, Kokeile!


Ystävällinen ottelu kollegoiden välillä

Verkkokirjaston koodi on vapaasti saatavilla osoitteessa Github. Liity keskusteluun kanavallamme osoitteessa gitter!

Lähde: will.com

Lisää kommentti