Cheerp, WebRTC болон Firebase ашиглан C++-ээс олон тоглогчтой тоглоомыг вэб рүү шилжүүлэх

Танилцуулга

манай компани Налуу технологиуд уламжлалт ширээний програмуудыг вэб рүү шилжүүлэх шийдлүүдийг санал болгодог. Манай C++ хөрвүүлэгч уйлах WebAssembly болон JavaScript-ийн хослолыг үүсгэдэг бөгөөд энэ нь хоёуланг нь хангадаг энгийн хөтөчийн харилцан үйлчлэл, өндөр гүйцэтгэлтэй.

Үүний хэрэглээний жишээ болгон бид олон тоглогчийн тоглоомыг вэб рүү оруулахаар шийдэж, сонгосон Teeworlds. Teeworlds бол жижиг боловч идэвхтэй тоглогчдын нийгэмлэгтэй (намайг оруулаад!) олон тоглогчтой 2D чимэг тоглоом юм. Энэ нь татаж авсан нөөц болон CPU болон GPU-ийн шаардлагын хувьд жижиг бөгөөд хамгийн тохиромжтой нэр дэвшигч юм.

Cheerp, WebRTC болон Firebase ашиглан C++-ээс олон тоглогчтой тоглоомыг вэб рүү шилжүүлэх
Teeworlds хөтөч дээр ажиллаж байна

Бид энэ төслийг ашиглан туршилт хийхээр шийдсэн сүлжээний кодыг вэб рүү шилжүүлэх ерөнхий шийдлүүд. Үүнийг ихэвчлэн дараах байдлаар хийдэг.

  • XMLHttpRequest/tatch, хэрэв сүлжээний хэсэг нь зөвхөн 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 серверээс хүлээн авсан мэдээлэлд үндэслэн хоёр үе тэнгийнхэнийг холбох хамгийн сайн аргыг сонгоход ашигладаг.
  • Сессийг тайлбарлах протокол (RDS) нь холболтын сувгийн параметрүүдийг тайлбарлах формат юм, жишээлбэл, ICE нэр дэвшигчид, мультимедиа кодлогч (аудио/видео сувгийн хувьд) гэх мэт... Үе тэнгийнхний нэг нь SDP санал илгээж, хоёр дахь нь SDP хариултаар хариулдаг. .. Үүний дараа суваг үүсгэгддэг.

Ийм холболт үүсгэхийн тулд үе тэнгийнхэн нь STUN болон TURN серверүүдээс хүлээн авсан мэдээллээ цуглуулж, өөр хоорондоо солилцох хэрэгтэй.

Асуудал нь тэд шууд харилцах чадваргүй байгаа тул энэ өгөгдлийг солилцохын тулд зурвасаас гадуурх механизм байх ёстой: дохионы сервер.

Дохиоллын сервер нь маш энгийн байж болно, учир нь түүний цорын ганц ажил нь гар барих үе шатанд үе тэнгийнхний хооронд өгөгдөл дамжуулах явдал юм (доорх диаграммд үзүүлсэн шиг).

Cheerp, WebRTC болон Firebase ашиглан C++-ээс олон тоглогчтой тоглоомыг вэб рүү шилжүүлэх
Хялбаршуулсан WebRTC гар барих дарааллын диаграм

Teeworlds сүлжээний загварын тойм

Teeworlds сүлжээний архитектур нь маш энгийн:

  • Үйлчлүүлэгч болон серверийн бүрэлдэхүүн хэсэг нь хоёр өөр програм юм.
  • Үйлчлүүлэгчид хэд хэдэн серверийн аль нэгэнд холбогдож тоглоомд ордог бөгөөд тус бүр нь нэг удаад зөвхөн нэг тоглоомыг зохион байгуулдаг.
  • Тоглоомын бүх өгөгдөл дамжуулах нь серверээр дамждаг.
  • Тусгай мастер серверийг тоглоомын клиент дээр харуулсан бүх нийтийн серверүүдийн жагсаалтыг цуглуулахад ашигладаг.

Мэдээлэл солилцоход WebRTC ашигласны ачаар бид тоглоомын серверийн бүрэлдэхүүн хэсгийг үйлчлүүлэгчийн байрладаг хөтөч рүү шилжүүлэх боломжтой. Энэ нь бидэнд маш том боломжийг олгож байна...

Серверүүдээс салах

Серверийн логик дутагдалтай байгаа нь сайн давуу талтай: бид програмыг бүхэлд нь Github хуудас эсвэл Cloudflare-ийн ард байрлах өөрийн техник хангамж дээр статик контент болгон байрлуулж, хурдан татаж авах, өндөр ажиллах хугацааг үнэ төлбөргүй хийх боломжтой. Үнэн хэрэгтээ бид тэднийг мартаж болно, хэрэв бид аз таарч, тоглоом алдартай болвол дэд бүтцийг шинэчлэх шаардлагагүй болно.

Гэсэн хэдий ч системийг ажиллуулахын тулд бид гадаад архитектурыг ашиглах шаардлагатай хэвээр байна:

  • Нэг буюу хэд хэдэн STUN сервер: Бид сонгох боломжтой хэд хэдэн үнэгүй сонголттой.
  • Дор хаяж нэг TURN сервер: энд үнэгүй сонголт байхгүй тул бид өөрсдөө тохируулах эсвэл үйлчилгээний төлбөрийг төлөх боломжтой. Аз болоход ихэнх тохиолдолд холболтыг STUN серверүүдээр (мөн жинхэнэ p2p-ээр хангадаг) холбож болох боловч TURN нь нөөц сонголт болгон шаардлагатай байдаг.
  • Дохионы сервер: Бусад хоёр талаас ялгаатай нь дохиолол нь стандартчилагдаагүй. Сигналын сервер яг юуг хариуцах вэ гэдэг нь тухайн програмаас тодорхой хэмжээгээр шалтгаална. Манай тохиолдолд холболт үүсгэхийн өмнө бага хэмжээний өгөгдөл солилцох шаардлагатай байдаг.
  • Teeworlds Мастер Сервер: Үүнийг бусад серверүүд өөрсдийн оршин тогтнолыг сурталчлах, үйлчлүүлэгчид нийтийн сервер хайхад ашигладаг. Энэ нь шаардлагагүй ч (үйлчлүүлэгчид өөрсдийн мэддэг серверт үргэлж гараар холбогдож болно), тоглогчид санамсаргүй хүмүүстэй тоглоомонд оролцох боломжтой байх нь сайхан байх болно.

Бид Google-ийн үнэ төлбөргүй STUN серверүүдийг ашиглахаар шийдэж, нэг TURN серверийг өөрсдөө байршуулсан.

Сүүлийн хоёр цэгийг бид ашигласан Функц:

  • Teeworlds мастер серверийг маш энгийнээр хэрэгжүүлдэг: идэвхтэй сервер бүрийн мэдээлэл (нэр, IP, газрын зураг, горим, ...) агуулсан объектуудын жагсаалт хэлбэрээр. Серверүүд өөрсдийн объектыг нийтэлж, шинэчилдэг бөгөөд үйлчлүүлэгчид жагсаалтыг бүхэлд нь авч, тоглуулагчид харуулдаг. Мөн бид үндсэн хуудсан дээрх жагсаалтыг HTML хэлбэрээр харуулдаг тул тоглогчид сервер дээр дарж шууд тоглоом руу орох боломжтой.
  • Дохио нь бидний дараагийн хэсэгт тайлбарласан залгууруудын хэрэгжилттэй нягт холбоотой.

Cheerp, WebRTC болон Firebase ашиглан C++-ээс олон тоглогчтой тоглоомыг вэб рүү шилжүүлэх
Тоглоом болон нүүр хуудсан дээрх серверүүдийн жагсаалт

Сокетуудын хэрэгжилт

Бид шаардлагатай өөрчлөлтүүдийн тоог багасгахын тулд Posix UDP Sockets-тай аль болох ойрхон API үүсгэхийг хүсч байна.

Мөн бид сүлжээгээр хамгийн энгийн өгөгдөл солилцоход шаардагдах доод хэмжээг хэрэгжүүлэхийг хүсч байна.

Жишээлбэл, бидэнд жинхэнэ чиглүүлэлт хэрэггүй: бүх үе тэнгийнхэн Firebase мэдээллийн сангийн тодорхой жишээтэй холбоотой ижил "виртуал LAN" дээр байдаг.

Тиймээс бидэнд өвөрмөц 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 оноох, залхуу холболтууд.

Буцах дуудлагыг бүртгэж байна

Анхны програм нь блоклохгүй 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 ).

Энэ нь бидэнд тохиромжтой, учир нь бидэнд IP-г оноож, тэдгээрийн өвөрмөц байдлыг шалгах механизм хэрэггүй (мөн үйлчлүүлэгч салгасны дараа тэдгээрийг устгах), гэхдээ үе тэнгийнхнээ тоон утгаараа тодорхойлох шаардлагатай байдаг.

Функцуудыг яг ийм зорилгоор ашигладаг. resolve и reverseResolve: Аппликешн нь ямар нэгэн байдлаар түлхүүрийн утгыг (хэрэглэгчийн оролт эсвэл мастер серверээр дамжуулан) хүлээн авч, дотоод хэрэгцээнд зориулж IP хаяг руу хөрвүүлэх боломжтой. Бусад API нь хялбаршуулах үүднээс мөрийн оронд энэ утгыг хүлээн авдаг.

Энэ нь DNS хайлттай төстэй боловч үйлчлүүлэгч дээр локал байдлаар хийгддэг.

Өөрөөр хэлбэл, IP хаягийг өөр өөр үйлчлүүлэгчдийн хооронд хуваалцах боломжгүй бөгөөд хэрэв ямар нэгэн төрлийн глобал танигч шаардлагатай бол түүнийг өөр аргаар үүсгэх шаардлагатай болно.

Залхуу холболт

UDP-д холболт шаардлагагүй, гэхдээ бидний харж байгаагаар WebRTC нь хоёр үе тэнгийн хооронд өгөгдөл дамжуулахаас өмнө урт холболтын процесс шаарддаг.

Хэрэв бид ижил түвшний хийсвэрлэлийг өгөхийг хүсвэл, (sendto/recvfrom Урьдчилан холболтгүй дурын үе тэнгийнхэнтэй), дараа нь тэд API дотор "залхуу" (саатсан) холболт хийх ёстой.

UDP ашиглах үед "сервер" болон "үйлчлүүлэгч" хоорондын ердийн харилцааны үед ийм зүйл тохиолддог бөгөөд манай номын сан юу хийх ёстой вэ:

  • Серверийн дуудлага bind()үйлдлийн системд заасан порт дээр пакет хүлээн авахыг хүсч байгаагаа хэлэх.

Үүний оронд бид серверийн түлхүүрийн доор Firebase-д нээлттэй портыг нийтэлж, түүний дэд мод дахь үйл явдлыг сонсох болно.

  • Серверийн дуудлага recvfrom(), энэ порт дээрх дурын хостоос ирж буй пакетуудыг хүлээн авах.

Манай тохиолдолд бид энэ порт руу илгээсэн пакетуудын ирж буй дарааллыг шалгах хэрэгтэй.

Порт бүр өөрийн гэсэн дараалалтай бөгөөд бид шинэ пакет ирэх үед аль дараалал руу шилжүүлэхийг мэдэхийн тулд WebRTC датаграммын эхэнд эх сурвалж болон очих портуудыг нэмдэг.

Дуудлага нь блоклохгүй тул хэрэв пакет байхгүй бол бид зүгээр л -1 гэж буцаагаад тохируулна errno=EWOULDBLOCK.

  • Үйлчлүүлэгч серверийн IP болон портыг гадны ямар нэгэн аргаар хүлээн авч, дуудлага хийдэг sendto(). Энэ нь мөн дотоод дуудлага хийдэг. bind(), тиймээс дараа нь recvfrom() холбоосыг тодорхой гүйцэтгэхгүйгээр хариуг хүлээн авах болно.

Манай тохиолдолд үйлчлүүлэгч гаднаас string түлхүүрийг хүлээн авч функцийг ашигладаг resolve() IP хаяг авахын тулд.

Энэ үед бид хоёр үе тэнгийнхэн хоорондоо хараахан холбогдоогүй бол WebRTC-н гар барилтыг эхлүүлнэ. Нэг үе тэнгийн өөр портуудтай холбогдох нь ижил WebRTC DataChannel ашигладаг.

Бид мөн шууд бусаар гүйцэтгэдэг bind()Ингэснээр сервер дараагийн үед дахин холбогдох боломжтой болно sendto() ямар нэг шалтгаанаар хаагдсан тохиолдолд.

Үйлчлүүлэгч Firebase дахь серверийн портын мэдээллийн доор SDP саналаа бичих үед серверт үйлчлүүлэгчийн холболтын талаар мэдэгдэх бөгөөд сервер нь тэнд хариу өгөх болно.

Доорх диаграмм нь залгуурын схемийн мессежийн урсгал болон үйлчлүүлэгчээс сервер рүү эхний мессежийг дамжуулах жишээг харуулж байна.

Cheerp, WebRTC болон Firebase ашиглан C++-ээс олон тоглогчтой тоглоомыг вэб рүү шилжүүлэх
Үйлчлүүлэгч болон серверийн хоорондох холболтын үе шатны иж бүрэн диаграм

дүгнэлт

Хэрэв та энэ хүртэл уншсан бол онолыг бодитоор харах сонирхолтой байх магадлалтай. Тоглоомыг цааш нь тоглох боломжтой teeworlds.leaningtech.com, оролдоод үз!


Хамт ажиллагсдын нөхөрсөг тоглолт

Сүлжээний номын сангийн кодыг эндээс үнэгүй авах боломжтой Github. Манай суваг дээрх ярилцлагад нэгдээрэй Гайт!

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх