Cheerp, WebRTC және Firebase көмегімен C++ тілінен вебке көп ойыншы ойынын тасымалдау

Кіріспе

Біздің компания Технологиялар дәстүрлі жұмыс үстелі қолданбаларын интернетке тасымалдау үшін шешімдерді ұсынады. Біздің C++ компиляторы көңілді WebAssembly және JavaScript тіркесімін жасайды, ол екеуін де қамтамасыз етеді шолғыштың қарапайым әрекеттесуі, және жоғары өнімділік.

Оны қолданудың мысалы ретінде біз көп ойыншы ойынын вебке көшіруді шештік және таңдадық Teeworlds. Teeworlds – ойыншылардың шағын, бірақ белсенді қауымдастығы (соның ішінде мен де!) бар көп ойыншы XNUMXD ретро ойыны. Бұл жүктелген ресурстар тұрғысынан да, CPU және GPU талаптары бойынша да кішкентай - тамаша үміткер.

Cheerp, WebRTC және Firebase көмегімен C++ тілінен вебке көп ойыншы ойынын тасымалдау
Teeworlds браузерінде іске қосу

Біз бұл жобаны тәжірибе жасау үшін пайдалануды шештік желі кодын вебке тасымалдаудың жалпы шешімдері. Бұл әдетте келесі жолдармен жүзеге асырылады:

  • XMLHttpRequest/алу, желі бөлігі тек HTTP сұрауларынан тұрса немесе
  • веб-розеткалар.

Екі шешім де сервер жағында сервер компонентін орналастыруды талап етеді және ешқайсысы тасымалдау протоколы ретінде пайдалануға рұқсат бермейді. UDP. Бұл бейнеконференциялық бағдарламалық қамтамасыз ету және ойындар сияқты нақты уақыттағы қолданбалар үшін маңызды, себебі ол хаттамалық пакеттердің жеткізілуіне және тәртібіне кепілдік береді. TCP төмен кідіріске кедергі болуы мүмкін.

Үшінші әдіс бар - браузерден желіні пайдаланыңыз: WebRTC.

RTCDataChannel Ол сенімді де, сенімсіз де жіберуді қолдайды (соңғы жағдайда ол UDP мүмкіндігінше тасымалдау протоколы ретінде пайдалануға тырысады) және қашықтағы сервермен де, браузерлер арасында да пайдалануға болады. Бұл біз бүкіл қолданбаны шолғышқа, соның ішінде сервер құрамдас бөлігіне тасымалдай алатынымызды білдіреді!

Дегенмен, бұл қосымша қиындықпен бірге келеді: екі WebRTC әріптесі байланыса алмас бұрын, олар қосылу үшін салыстырмалы түрде күрделі қол алысуды орындауы керек, ол үшін бірнеше үшінші тарап нысандары (сигнал беру сервері және бір немесе бірнеше серверлер) қажет. ТАҢДАУ/ҰСТАУ).

Ең дұрысы, біз WebRTC іштей пайдаланатын, бірақ қосылымды орнатуды қажет етпейтін UDP Sockets интерфейсіне мүмкіндігінше жақын желілік API жасағымыз келеді.

Бұл бізге WebRTC мүмкіндіктерін қолданбалы кодқа күрделі мәліметтерді көрсетпей-ақ пайдалануға мүмкіндік береді (біз оны жобамызда мүмкіндігінше аз өзгерткіміз келді).

Ең аз WebRTC

WebRTC — аудио, бейне және ерікті деректерді бір-бірден жіберуді қамтамасыз ететін браузерлерде қолжетімді API жиынтығы.

Құрдастардың арасындағы байланыс ICE деп аталатын механизм арқылы STUN және/немесе TURN серверлері арқылы (бір немесе екі жағында NAT болса да) орнатылады. Құрдастары ICE ақпараты мен арна параметрлерін SDP протоколының ұсынысы мен жауабы арқылы алмасады.

Апыр-ай! Бір уақытта қанша аббревиатура? Бұл терминдердің нені білдіретінін қысқаша түсіндірейік:

  • NAT үшін сеанстарды өту утилиталары (ТАҢДАУ) — NAT-ты айналып өту және хостпен тікелей деректер алмасу үшін жұп (IP, порт) алуға арналған протокол. Егер ол өз тапсырмасын орындай алса, онда құрдастар бір-бірімен дербес деректер алмаса алады.
  • NAT айналасындағы релелерді пайдалану арқылы өту (ҰСТАУ) NAT өту үшін де пайдаланылады, бірақ ол мұны екі теңдес үшін де көрінетін прокси арқылы деректерді қайта жіберу арқылы жүзеге асырады. Ол кідіріс қосады және STUN-қа қарағанда жүзеге асыру қымбатырақ (өйткені ол бүкіл байланыс сеансы бойына қолданылады), бірақ кейде бұл жалғыз нұсқа болып табылады.
  • Интерактивті байланыс орнату (ICE) тікелей қосылатын құрдастардан алынған ақпарат, сондай-ақ STUN және TURN серверлерінің кез келген санымен алынған ақпарат негізінде екі теңді қосудың ең жақсы мүмкін әдісін таңдау үшін қолданылады.
  • Сеансты сипаттау протоколы (SDP) қосылым арнасының параметрлерін сипаттауға арналған пішім, мысалы, ICE кандидаттары, мультимедиялық кодектер (аудио/бейне арнасы жағдайында) және т.б... Құрдастардың бірі SDP ұсынысын жібереді, ал екіншісі SDP жауабымен жауап береді. .. Осыдан кейін арна жасалады.

Мұндай қосылымды құру үшін әріптестер STUN және TURN серверлерінен алатын ақпаратты жинап, бір-бірімен алмасуы керек.

Мәселе мынада, олардың әлі тікелей байланысу мүмкіндігі жоқ, сондықтан бұл деректермен алмасу үшін диапазоннан тыс механизм болуы керек: сигнал беру сервері.

Сигнал беру сервері өте қарапайым болуы мүмкін, себебі оның жалғыз жұмысы қол алысу фазасындағы әріптестер арасында деректерді жіберу болып табылады (төмендегі диаграммада көрсетілгендей).

Cheerp, WebRTC және Firebase көмегімен C++ тілінен вебке көп ойыншы ойынын тасымалдау
Жеңілдетілген WebRTC қол алысу реттілігі диаграммасы

Teeworlds желі үлгісіне шолу

Teeworlds желісінің архитектурасы өте қарапайым:

  • Клиент пен сервер құрамдастары екі түрлі бағдарлама.
  • Клиенттер ойынға әрқайсысында бір уақытта тек бір ойынды қабылдайтын бірнеше серверлердің біріне қосылу арқылы кіреді.
  • Ойындағы барлық деректерді беру сервер арқылы жүзеге асырылады.
  • Арнайы негізгі сервер ойын клиентінде көрсетілетін барлық жалпы серверлердің тізімін жинау үшін пайдаланылады.

Деректер алмасу үшін WebRTC пайдаланудың арқасында біз ойынның серверлік құрамдас бөлігін клиент орналасқан браузерге тасымалдай аламыз. Бұл бізге үлкен мүмкіндік береді...

Серверлерден құтылыңыз

Сервер логикасының жоқтығы жақсы артықшылыққа ие: біз бүкіл қолданбаны Github беттерінде немесе Cloudflare артындағы өз аппараттық құралымызда статикалық мазмұн ретінде орналастыра аламыз, осылайша жылдам жүктеп алуды және жоғары жұмыс уақытын тегін қамтамасыз ете аламыз. Шындығында, біз оларды ұмыта аламыз, ал егер сәті түсіп, ойын танымал болса, онда инфрақұрылымды жаңғыртудың қажеті жоқ.

Дегенмен, жүйе жұмыс істеуі үшін біз әлі де сыртқы архитектураны пайдалануымыз керек:

  • Бір немесе бірнеше STUN серверлері: Бізде таңдауға болатын бірнеше тегін опциялар бар.
  • Кем дегенде бір TURN сервері: мұнда тегін опциялар жоқ, сондықтан біз өзімізді орната аламыз немесе қызмет үшін төлей аламыз. Бақытымызға орай, көп жағдайда қосылымды STUN серверлері арқылы орнатуға болады (және шынайы p2p қамтамасыз етеді), бірақ TURN қалпына келтіру опциясы ретінде қажет.
  • Сигнал беру сервері: Басқа екі аспектіден айырмашылығы, сигнал беру стандартталмаған. Сигнал беру сервері не үшін жауапты болады, бұл қолданбаға байланысты. Біздің жағдайда, қосылымды орнатпас бұрын, деректердің аз мөлшерін алмасу қажет.
  • Teeworlds негізгі сервері: Оны басқа серверлер өздерінің бар екенін жариялау үшін, ал клиенттер жалпыға ортақ серверлерді табу үшін пайдаланады. Бұл талап етілмесе де (клиенттер әрқашан өздері білетін серверге қолмен қосыла алады), ойыншылар кездейсоқ адамдармен ойындарға қатыса алатындай болғаны жақсы болар еді.

Біз Google-дың тегін STUN серверлерін пайдалануды шештік және бір TURN серверін өзіміз орналастырдық.

Соңғы екі нүкте үшін біз қолдандық Firebase:

  • Teeworlds негізгі сервері өте қарапайым түрде жүзеге асырылады: әрбір белсенді сервердің ақпараты (аты, IP, карта, режим, ...) бар нысандар тізімі ретінде. Серверлер өз нысанын жариялайды және жаңартады, ал клиенттер тізімді толығымен алып, оны ойнатқышқа көрсетеді. Сондай-ақ біз тізімді басты бетте HTML түрінде көрсетеміз, осылайша ойыншылар жай ғана серверді басып, ойынға тікелей өтуі мүмкін.
  • Сигнал беру келесі бөлімде сипатталған розеткаларды іске асырумен тығыз байланысты.

Cheerp, WebRTC және Firebase көмегімен C++ тілінен вебке көп ойыншы ойынын тасымалдау
Ойын ішіндегі және басты бетте серверлердің тізімі

Розеткаларды жүзеге асыру

Қажетті өзгерістер санын азайту үшін Posix UDP ұяларына мүмкіндігінше жақын API жасағымыз келеді.

Біз сондай-ақ желі арқылы ең қарапайым деректер алмасу үшін қажетті қажетті минимумды іске асырғымыз келеді.

Мысалы, бізге нақты маршруттау қажет емес: барлық әріптестер арнайы 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 интерфейсіне ұқсас, бірақ бірнеше маңызды айырмашылықтары бар: кері қоңырауларды тіркеу, жергілікті 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 мекенжайына түрлендіре алады. API қалған бөлігі де қарапайымдылық үшін жолдың орнына осы мәнді алады.

Бұл DNS іздеуіне ұқсас, бірақ клиентте жергілікті түрде орындалады.

Яғни, IP мекенжайларын әртүрлі клиенттер арасында ортақ пайдалану мүмкін емес және қандай да бір ғаламдық идентификатор қажет болса, оны басқа жолмен жасау керек болады.

Жалқау байланыс

UDP қосылымды қажет етпейді, бірақ біз көргеніміздей, WebRTC екі құрдас арасында деректерді тасымалдауды бастамас бұрын ұзақ қосылу процесін қажет етеді.

Егер біз абстракцияның бірдей деңгейін қамтамасыз еткіміз келсе, (sendto/recvfrom алдын ала қосылымсыз ерікті әріптестермен), содан кейін олар API ішінде «жалқау» (кешіктірілген) қосылымды орындауы керек.

Бұл UDP пайдалану кезінде «сервер» мен «клиент» арасындағы қалыпты байланыс кезінде орын алады және біздің кітапхана не істеу керек:

  • Сервер қоңыраулары bind()операциялық жүйеге көрсетілген портта пакеттерді алғысы келетінін айту.

Оның орнына біз сервер кілті астында Firebase-ге ашық портты жариялаймыз және оның ішкі ағашындағы оқиғаларды тыңдаймыз.

  • Сервер қоңыраулары recvfrom(), осы порттағы кез келген хосттан келетін пакеттерді қабылдау.

Біздің жағдайда осы портқа жіберілген пакеттердің кіріс кезегін тексеру керек.

Әрбір порттың өз кезегі бар және біз жаңа пакет келгенде қай кезекті бағыттау керектігін білу үшін WebRTC датаграммаларының басына бастапқы және тағайындау порттарын қосамыз.

Қоңырау бұғатталмайды, сондықтан пакеттер болмаса, біз жай ғана -1 қайтарамыз және орнатамыз errno=EWOULDBLOCK.

  • Клиент сервердің IP және портын қандай да бір сыртқы құралдармен алады және қоңыраулар жасайды sendto(). Бұл сонымен қатар ішкі қоңырауды жасайды. bind(), сондықтан кейінгі recvfrom() байлауды нақты орындамай жауап алады.

Біздің жағдайда клиент жол кілтін сырттан алады және функцияны пайдаланады resolve() IP мекенжайын алу үшін.

Осы кезде екі құрдас бір-бірімен әлі қосылмаған болса, WebRTC қол алысуын бастаймыз. Бір құрдастың әртүрлі порттарына қосылымдар бірдей WebRTC DataChannel пайдаланады.

Біз де жанама түрде орындаймыз bind()сервер келесіде қайта қосылуы үшін sendto() қандай да бір себептермен жабылған жағдайда.

Клиент Firebase жүйесіндегі сервер порты ақпаратының астына SDP ұсынысын жазғанда сервер клиенттің қосылымы туралы хабардар етіледі және сервер сол жерде өз жауабымен жауап береді.

Төмендегі диаграмма розетка схемасы үшін хабарлама ағынының мысалын және клиенттен серверге жіберілген бірінші хабарламаны көрсетеді:

Cheerp, WebRTC және Firebase көмегімен C++ тілінен вебке көп ойыншы ойынын тасымалдау
Клиент пен сервер арасындағы қосылу кезеңінің толық диаграммасы

қорытынды

Егер сіз осы уақытқа дейін оқысаңыз, сіз теорияны іс жүзінде көргіңіз келетін шығар. Ойынды ойнауға болады teeworlds.leaningtech.com, көріңіз!


Әріптестер арасындағы жолдастық кездесу

Желілік кітапхана коды мына жерден тегін қол жетімді GitHub. Біздің арнадағы сұхбатқа мына мекен-жай бойынша қосылыңыз Гиттер!

Ақпарат көзі: www.habr.com

пікір қалдыру