Cludo gêm aml-chwaraewr o C++ i'r we gyda Cheerp, WebRTC a Firebase

Cyflwyniad

ein cwmni Technolegau sy'n Dysgu yn darparu datrysiadau ar gyfer trosglwyddo cymwysiadau bwrdd gwaith traddodiadol i'r we. Ein casglwr C ++ sirp yn cynhyrchu cyfuniad o WebAssembly a JavaScript, sy'n darparu'r ddau rhyngweithio porwr syml, a pherfformiad uchel.

Fel enghraifft o'i gymhwysiad, fe benderfynon ni borthi gêm aml-chwaraewr i'r we a dewis Teeworlds. Mae Teeworlds yn gêm retro XNUMXD aml-chwaraewr gyda chymuned fach ond gweithgar o chwaraewyr (gan gynnwys fi!). Mae'n fach o ran adnoddau wedi'u llwytho i lawr a gofynion CPU a GPU - ymgeisydd delfrydol.

Cludo gêm aml-chwaraewr o C++ i'r we gyda Cheerp, WebRTC a Firebase
Yn rhedeg yn y porwr Teeworlds

Penderfynasom ddefnyddio'r prosiect hwn i arbrofi ag ef atebion cyffredinol ar gyfer trosglwyddo cod rhwydwaith i'r we. Gwneir hyn fel arfer yn y ffyrdd canlynol:

  • XMLHttpCais/nol, os yw rhan y rhwydwaith yn cynnwys ceisiadau HTTP yn unig, neu
  • WebSocedi.

Mae'r ddau ddatrysiad yn gofyn am gynnal cydran gweinydd ar ochr y gweinydd, ac nid yw'r naill na'r llall yn caniatáu ei ddefnyddio fel protocol trafnidiaeth Cynllun Datblygu Unedol. Mae hyn yn bwysig ar gyfer cymwysiadau amser real fel meddalwedd fideo-gynadledda a gemau, oherwydd mae'n gwarantu danfoniad a threfn pecynnau protocol TCP gall ddod yn rhwystr i hwyrni isel.

Mae yna drydedd ffordd - defnyddiwch y rhwydwaith o'r porwr: WebRTC.

Sianel RTCData Mae'n cefnogi trosglwyddiad dibynadwy ac annibynadwy (yn yr achos olaf mae'n ceisio defnyddio CDU fel protocol cludiant pryd bynnag y bo modd), a gellir ei ddefnyddio gyda gweinydd o bell a rhwng porwyr. Mae hyn yn golygu y gallwn borthi'r rhaglen gyfan i'r porwr, gan gynnwys cydran y gweinydd!

Fodd bynnag, daw hyn ag anhawster ychwanegol: cyn y gall dau gymar WebRTC gyfathrebu, mae angen iddynt berfformio ysgwyd llaw cymharol gymhleth i gysylltu, sy'n gofyn am sawl endid trydydd parti (gweinydd signalau ac un neu fwy o weinyddion STUN/TWRN).

Yn ddelfrydol, hoffem greu API rhwydwaith sy'n defnyddio WebRTC yn fewnol, ond sydd mor agos â phosibl at ryngwyneb Socedi CDU nad oes angen sefydlu cysylltiad.

Bydd hyn yn ein galluogi i fanteisio ar WebRTC heb orfod datgelu manylion cymhleth i'r cod cais (yr oeddem am ei newid cyn lleied â phosibl yn ein prosiect).

Isafswm WebRTC

Mae WebRTC yn set o APIs sydd ar gael mewn porwyr sy'n darparu trosglwyddiad cyfoedion-i-gymar o ddata sain, fideo a mympwyol.

Mae'r cysylltiad rhwng cyfoedion wedi'i sefydlu (hyd yn oed os oes NAT ar un ochr neu'r ddwy ochr) gan ddefnyddio gweinyddwyr STUN a/neu TURN trwy fecanwaith o'r enw ICE. Mae cyfoedion yn cyfnewid gwybodaeth ICE a pharamedrau sianel trwy gynnig ac ateb y protocol CDY.

Waw! Sawl talfyriad ar yr un pryd? Gadewch i ni esbonio'n fyr ystyr y termau hyn:

  • Cyfleustodau Traversal Sesiwn ar gyfer NAT (STUN) - protocol ar gyfer osgoi NAT a chael pâr (IP, porthladd) ar gyfer cyfnewid data yn uniongyrchol â'r gwesteiwr. Os yw'n llwyddo i gwblhau ei dasg, yna gall cyfoedion gyfnewid data yn annibynnol â'i gilydd.
  • Traversal Defnyddio Releiau o amgylch NAT (TWRN) yn cael ei ddefnyddio hefyd ar gyfer tramwyo NAT, ond mae'n gweithredu hyn trwy anfon data ymlaen trwy ddirprwy sy'n weladwy i'r ddau gymar. Mae'n ychwanegu hwyrni ac mae'n ddrutach i'w weithredu na STUN (oherwydd ei fod yn cael ei gymhwyso trwy gydol y sesiwn gyfathrebu gyfan), ond weithiau dyma'r unig opsiwn.
  • Sefydliad Cysylltedd Rhyngweithiol (ICE) a ddefnyddir i ddewis y dull gorau posibl o gysylltu dau gymar yn seiliedig ar wybodaeth a gafwyd o gysylltu cymheiriaid yn uniongyrchol, yn ogystal â gwybodaeth a dderbyniwyd gan unrhyw nifer o weinyddion STUN a TURN.
  • Protocol Disgrifiad o'r Sesiwn (CDY) yn fformat ar gyfer disgrifio paramedrau sianel cysylltiad, er enghraifft, ymgeiswyr ICE, codecau amlgyfrwng (yn achos sianel sain/fideo), ac ati... Mae un o'r cymheiriaid yn anfon Cynnig SDP, ac mae'r ail yn ymateb gydag Ateb SDP . . Ar ôl hyn, mae sianel yn cael ei chreu.

Er mwyn creu cysylltiad o'r fath, mae angen i gyfoedion gasglu'r wybodaeth a gânt gan y gweinyddwyr STUN a TURN a'i chyfnewid â'i gilydd.

Y broblem yw nad oes ganddynt y gallu i gyfathrebu'n uniongyrchol eto, felly mae'n rhaid i fecanwaith y tu allan i'r band fodoli i gyfnewid y data hwn: gweinydd signalau.

Gall gweinydd signalau fod yn syml iawn oherwydd ei unig waith yw anfon data ymlaen rhwng cyfoedion yn y cyfnod ysgwyd llaw (fel y dangosir yn y diagram isod).

Cludo gêm aml-chwaraewr o C++ i'r we gyda Cheerp, WebRTC a Firebase
Diagram dilyniant ysgwyd llaw WebRTC wedi'i symleiddio

Trosolwg Model Rhwydwaith Teeworlds

Mae pensaernïaeth rhwydwaith Teeworlds yn syml iawn:

  • Mae'r cydrannau cleient a gweinydd yn ddwy raglen wahanol.
  • Mae cleientiaid yn mynd i mewn i'r gêm trwy gysylltu ag un o nifer o weinyddion, gyda phob un ohonynt yn cynnal un gêm yn unig ar y tro.
  • Mae'r holl drosglwyddo data yn y gêm yn cael ei wneud trwy'r gweinydd.
  • Defnyddir gweinydd meistr arbennig i gasglu rhestr o'r holl weinyddion cyhoeddus sy'n cael eu harddangos yn y cleient gêm.

Diolch i'r defnydd o WebRTC ar gyfer cyfnewid data, gallwn drosglwyddo elfen gweinydd y gêm i'r porwr lle mae'r cleient wedi'i leoli. Mae hyn yn rhoi cyfle gwych i ni...

Cael gwared ar weinyddion

Mae gan ddiffyg rhesymeg gweinydd fantais braf: gallwn ddefnyddio'r rhaglen gyfan fel cynnwys statig ar Github Pages neu ar ein caledwedd ein hunain y tu ôl i Cloudflare, gan sicrhau lawrlwythiadau cyflym a uptime uchel am ddim. Mewn gwirionedd, gallwn anghofio amdanynt, ac os ydym yn ffodus a bod y gêm yn dod yn boblogaidd, yna ni fydd yn rhaid moderneiddio'r seilwaith.

Fodd bynnag, er mwyn i'r system weithio, mae'n rhaid i ni ddefnyddio pensaernïaeth allanol o hyd:

  • Un neu fwy o weinyddion STUN: Mae gennym ni sawl opsiwn am ddim i ddewis ohonynt.
  • O leiaf un gweinydd TURN: nid oes opsiynau am ddim yma, felly gallwn naill ai sefydlu ein gweinydd ein hunain neu dalu am y gwasanaeth. Yn ffodus, y rhan fwyaf o'r amser gellir sefydlu'r cysylltiad trwy weinyddion STUN (a darparu p2p go iawn), ond mae angen TURN fel opsiwn wrth gefn.
  • Gweinydd Signalau: Yn wahanol i'r ddwy agwedd arall, nid yw signalau wedi'u safoni. Mae'r hyn y bydd y gweinydd signalau yn gyfrifol amdano mewn gwirionedd yn dibynnu rhywfaint ar y cymhwysiad. Yn ein hachos ni, cyn sefydlu cysylltiad, mae angen cyfnewid ychydig bach o ddata.
  • Teeworlds Master Server: Fe'i defnyddir gan weinyddion eraill i hysbysebu eu bodolaeth a chan gleientiaid i ddod o hyd i weinyddion cyhoeddus. Er nad yw'n ofynnol (gall cleientiaid bob amser gysylltu â gweinydd y maent yn gwybod amdano â llaw), byddai'n braf cael fel y gall chwaraewyr gymryd rhan mewn gemau gyda phobl ar hap.

Fe wnaethom benderfynu defnyddio gweinyddwyr STUN rhad ac am ddim Google, a defnyddio un gweinydd TURN ein hunain.

Am y ddau bwynt olaf a ddefnyddiwyd gennym Firebase:

  • Gweithredir prif weinydd Teeworlds yn syml iawn: fel rhestr o wrthrychau sy'n cynnwys gwybodaeth (enw, IP, map, modd, ...) pob gweinydd gweithredol. Mae gweinyddwyr yn cyhoeddi ac yn diweddaru eu gwrthrych eu hunain, ac mae cleientiaid yn cymryd y rhestr gyfan ac yn ei harddangos i'r chwaraewr. Rydym hefyd yn arddangos y rhestr ar y dudalen gartref fel HTML fel y gall chwaraewyr glicio ar y gweinydd a chael eu cludo'n syth i'r gêm.
  • Mae cysylltiad agos rhwng arwyddo a gweithredu ein socedi, a ddisgrifir yn yr adran nesaf.

Cludo gêm aml-chwaraewr o C++ i'r we gyda Cheerp, WebRTC a Firebase
Rhestr o weinyddion y tu mewn i'r gêm ac ar yr hafan

Gweithredu socedi

Rydym am greu API sydd mor agos at Posix UDP Sockets â phosibl i leihau nifer y newidiadau sydd eu hangen.

Rydym hefyd am weithredu'r isafswm angenrheidiol ar gyfer y cyfnewid data symlaf dros y rhwydwaith.

Er enghraifft, nid oes angen llwybro go iawn arnom: mae pob cyfoedion ar yr un "LAN rhithwir" sy'n gysylltiedig ag enghraifft benodol o gronfa ddata Firebase.

Felly, nid oes angen cyfeiriadau IP unigryw arnom: mae gwerthoedd allweddol Firebase unigryw (yn debyg i enwau parth) yn ddigonol i adnabod cyfoedion yn unigryw, ac mae pob cymar yn lleol yn aseinio cyfeiriadau IP "ffug" i bob allwedd y mae angen ei gyfieithu. Mae hyn yn dileu'n llwyr yr angen am aseiniad cyfeiriad IP byd-eang, sy'n dasg nad yw'n ddibwys.

Dyma'r API lleiaf y mae angen i ni ei weithredu:

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

Mae'r API yn syml ac yn debyg i'r Posix Sockets API, ond mae ganddo ychydig o wahaniaethau pwysig: logio galwadau yn ôl, aseinio IPs lleol, a chysylltiadau diog.

Cofrestru Galwadau'n Ôl

Hyd yn oed os yw'r rhaglen wreiddiol yn defnyddio I/O nad yw'n rhwystro, rhaid ail-ffactorio'r cod i redeg mewn porwr gwe.

Y rheswm am hyn yw bod y ddolen digwyddiad yn y porwr wedi'i chuddio o'r rhaglen (boed yn JavaScript neu WebAssembly).

Yn yr amgylchedd brodorol gallwn ysgrifennu cod fel hyn

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

Os yw dolen y digwyddiad wedi'i chuddio i ni, yna mae angen i ni ei throi'n rhywbeth fel hyn:

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

Aseiniad IP lleol

Nid yw'r IDau nod yn ein "rhwydwaith" yn gyfeiriadau IP, ond yn allweddi Firebase (maen nhw'n llinynnau sy'n edrych fel hyn: -LmEC50PYZLCiCP-vqde ).

Mae hyn yn gyfleus oherwydd nid oes angen mecanwaith arnom ar gyfer aseinio IPs a gwirio eu unigrywiaeth (yn ogystal â'u gwaredu ar ôl i'r cleient ddatgysylltu), ond yn aml mae angen nodi cyfoedion yn ôl gwerth rhifol.

Dyma'r union beth y defnyddir y swyddogaethau ar ei gyfer. resolve и reverseResolve: Mae'r cais rywsut yn derbyn gwerth llinyn yr allwedd (trwy fewnbwn defnyddiwr neu drwy'r gweinydd meistr), a gall ei drosi i gyfeiriad IP ar gyfer defnydd mewnol. Mae gweddill yr API hefyd yn derbyn y gwerth hwn yn hytrach na llinyn ar gyfer symlrwydd.

Mae hyn yn debyg i chwilio DNS, ond yn cael ei berfformio'n lleol ar y cleient.

Hynny yw, ni ellir rhannu cyfeiriadau IP rhwng gwahanol gleientiaid, ac os oes angen rhyw fath o ddynodwr byd-eang, bydd yn rhaid ei gynhyrchu mewn ffordd wahanol.

Cysylltiad diog

Nid oes angen cysylltiad ar y CDU, ond fel y gwelsom, mae WebRTC yn gofyn am broses gysylltu hir cyn y gall ddechrau trosglwyddo data rhwng dau gymar.

Os ydym am ddarparu'r un lefel o dynnu, (sendto/recvfrom gyda chyfoedion mympwyol heb gysylltiad blaenorol), yna rhaid iddynt berfformio cysylltiad “diog” (oedi) y tu mewn i'r API.

Dyma beth sy'n digwydd yn ystod cyfathrebu arferol rhwng y “gweinyddwr” a'r “cleient” wrth ddefnyddio CDU, a beth ddylai ein llyfrgell ei wneud:

  • Galwadau gweinydd bind()i ddweud wrth y system weithredu ei fod am dderbyn pecynnau ar y porthladd penodedig.

Yn lle hynny, byddwn yn cyhoeddi porthladd agored i Firebase o dan allwedd y gweinydd ac yn gwrando am ddigwyddiadau yn ei is-goeden.

  • Galwadau gweinydd recvfrom(), gan dderbyn pecynnau sy'n dod o unrhyw westeiwr ar y porthladd hwn.

Yn ein hachos ni, mae angen inni wirio'r ciw o becynnau sy'n dod i mewn a anfonwyd i'r porthladd hwn.

Mae gan bob porthladd ei giw ei hun, ac rydym yn ychwanegu'r porthladdoedd ffynhonnell a chyrchfan i ddechrau'r datagramau WebRTC fel ein bod yn gwybod at ba giw i anfon ymlaen pan fydd pecyn newydd yn cyrraedd.

Nid yw'r alwad yn blocio, felly os nad oes pecynnau, byddwn yn dychwelyd -1 ac yn gosod errno=EWOULDBLOCK.

  • Mae'r cleient yn derbyn yr IP a phorthladd y gweinydd trwy rai dulliau allanol, a galwadau sendto(). Mae hyn hefyd yn gwneud galwad fewnol. bind(), felly dilynol recvfrom() yn derbyn yr ymateb heb weithredu rhwymiad penodol.

Yn ein hachos ni, mae'r cleient yn derbyn yr allwedd llinyn yn allanol ac yn defnyddio'r swyddogaeth resolve() i gael cyfeiriad IP.

Ar y pwynt hwn, rydym yn cychwyn ysgwyd llaw WebRTC os nad yw'r ddau gymar wedi'u cysylltu â'i gilydd eto. Mae cysylltiadau â phorthladdoedd gwahanol o'r un cyfoedion yn defnyddio'r un WebRTC DataChannel.

Rydym hefyd yn perfformio'n anuniongyrchol bind()fel y gall y gweinydd ailgysylltu yn y nesaf sendto() rhag ofn iddo gau am ryw reswm.

Mae'r gweinydd yn cael ei hysbysu o gysylltiad y cleient pan fydd y cleient yn ysgrifennu ei gynnig SDP o dan y wybodaeth porthladd gweinydd yn Firebase, ac mae'r gweinydd yn ymateb gyda'i ymateb yno.

Mae'r diagram isod yn dangos enghraifft o lif neges ar gyfer cynllun soced a throsglwyddiad y neges gyntaf o'r cleient i'r gweinydd:

Cludo gêm aml-chwaraewr o C++ i'r we gyda Cheerp, WebRTC a Firebase
Diagram cyflawn o'r cyfnod cysylltiad rhwng cleient a gweinydd

Casgliad

Os ydych chi wedi darllen hyd yma, mae'n debyg bod gennych chi ddiddordeb mewn gweld y theori ar waith. Gellir chwarae'r gêm ymlaen teeworlds.leaningtech.com, rhowch gynnig arni!


Paru cyfeillgar rhwng cydweithwyr

Mae cod llyfrgell y rhwydwaith ar gael am ddim yn Github. Ymunwch â'r sgwrs ar ein sianel yn Gitter!

Ffynhonnell: hab.com

Ychwanegu sylw