የባለብዙ ተጫዋች ጨዋታን ከC++ ወደ ድር በ Cheerp፣ WebRTC እና Firebase በማስተላለፍ ላይ

መግቢያ

የእኛ ኩባንያ ዘንበል ቴክኖሎጂዎች ባህላዊ የዴስክቶፕ መተግበሪያዎችን ወደ ድሩ ለማድረስ መፍትሄዎችን ይሰጣል። የእኛ C++ ማጠናከሪያ አይዞህ ሁለቱንም የሚያቀርበው የዌብአሴምብሊ እና ጃቫስክሪፕት ጥምረት ይፈጥራል ቀላል የአሳሽ መስተጋብር, እና ከፍተኛ አፈፃፀም.

እንደ ትግበራው ምሳሌ፣ ባለብዙ ተጫዋች ጨዋታን ወደ ድሩ ለማድረስ ወስነን መረጥን። ቴዎውስ. Teeworlds ትንሽ ነገር ግን ንቁ የተጫዋቾች ማህበረሰብ (እኔን ጨምሮ!) ያለው ባለብዙ ተጫዋች 2D retro ጨዋታ ነው። በወረዱ ሀብቶች እና በሲፒዩ እና በጂፒዩ መስፈርቶች ረገድ ትንሽ ነው - ተስማሚ እጩ።

የባለብዙ ተጫዋች ጨዋታን ከC++ ወደ ድር በ Cheerp፣ WebRTC እና Firebase በማስተላለፍ ላይ
በTeeworlds አሳሽ ውስጥ በመስራት ላይ

ይህንን ፕሮጀክት ለመሞከር ወስነናል የአውታረ መረብ ኮድ ወደ ድሩ ለማስተላለፍ አጠቃላይ መፍትሄዎች. ይህ ብዙውን ጊዜ በሚከተሉት መንገዶች ይከናወናል.

  • XMLHttpጥያቄ/አምጣየአውታረ መረቡ ክፍል የኤችቲቲፒ ጥያቄዎችን ብቻ ያካተተ ከሆነ ወይም
  • ዌብሳይቶች.

ሁለቱም መፍትሄዎች የአገልጋይ አካልን በአገልጋዩ በኩል ማስተናገድን ይጠይቃሉ, እና እንደ የትራንስፖርት ፕሮቶኮል አንዳቸውም አይፈቀዱም UDP. ይህ እንደ የቪዲዮ ኮንፈረንስ ሶፍትዌሮች እና ጨዋታዎች ላሉ አፕሊኬሽኖች አስፈላጊ ነው፣ ምክንያቱም የፕሮቶኮል እሽጎች መላክ እና ቅደም ተከተል ዋስትና ይሰጣል። TCP ለዝቅተኛ መዘግየት እንቅፋት ሊሆን ይችላል።

ሦስተኛው መንገድ አለ - አውታረ መረቡን ከአሳሹ ይጠቀሙ- WebRTC.

RTCDataChannel አስተማማኝ እና አስተማማኝ ያልሆነ ስርጭትን ይደግፋል (በኋለኛው ሁኔታ ዩዲፒን እንደ ማጓጓዣ ፕሮቶኮል በተቻለ መጠን ለመጠቀም ይሞክራል) እና ሁለቱንም በርቀት አገልጋይ እና በአሳሾች መካከል ሊያገለግል ይችላል። ይህ ማለት የአገልጋዩን አካል ጨምሮ አፕሊኬሽኑን በሙሉ ወደ አሳሹ መላክ እንችላለን ማለት ነው!

ሆኖም ይህ ከተጨማሪ ችግር ጋር አብሮ ይመጣል፡- ሁለት የዌብአርቲሲ እኩዮች ከመገናኘታቸው በፊት ለመገናኘት በአንፃራዊነት ውስብስብ የሆነ የእጅ መጨባበጥ ማድረግ አለባቸው፣ ይህም በርካታ የሶስተኛ ወገን አካላትን ይጠይቃል (ምልክት ሰጪ አገልጋይ እና አንድ ወይም ከዚያ በላይ አገልጋዮች)። STUN/ተራ).

በሐሳብ ደረጃ፣ WebRTCን በውስጥ በኩል የሚጠቀም የአውታረ መረብ ኤፒአይ መፍጠር እንፈልጋለን፣ነገር ግን ግንኙነት መመሥረት ወደማይፈልገው የUDP Sockets በይነገጽ በተቻለ መጠን ቅርብ ነው።

ይህ ውስብስብ ዝርዝሮችን ወደ አፕሊኬሽኑ ኮድ (በፕሮጀክታችን ውስጥ በተቻለ መጠን ትንሽ መለወጥ የፈለግነውን) ሳናጋለጥ የ WebRTC ተጠቃሚ እንድንሆን ያስችለናል.

ዝቅተኛው WebRTC

WebRTC በአሳሾች ውስጥ የሚገኝ የኦዲዮ፣ ቪዲዮ እና የዘፈቀደ ውሂብ ከአቻ ለአቻ ማስተላለፍ የሚያቀርብ የኤፒአይዎች ስብስብ ነው።

በአቻዎች መካከል ያለው ግንኙነት STUN እና/ወይም TURN አገልጋዮችን በመጠቀም ICE በተባለ ዘዴ (በአንዱ ወይም በሁለቱም በኩል NAT ቢኖርም) ይመሰረታል። እኩዮች የ ICE መረጃን እና የሰርጥ መለኪያዎችን በSDP ፕሮቶኮል አቅርቦት እና መልስ ይለዋወጣሉ።

ዋዉ! በአንድ ጊዜ ስንት አህጽሮተ ቃላት? እነዚህ ቃላት ምን ማለት እንደሆኑ ባጭሩ እናብራራ፡-

  • የክፍለ-ጊዜ መሻገሪያ መገልገያዎች ለ NAT (STUN) - NATን ለማለፍ እና ጥንድ ለማግኘት (አይፒ ፣ ወደብ) በቀጥታ ከአስተናጋጁ ጋር ውሂብ ለመለዋወጥ ፕሮቶኮል ። ስራውን መጨረስ ከቻለ እኩዮቹ በተናጥል እርስ በእርስ መረጃ መለዋወጥ ይችላሉ።
  • በ NAT ዙሪያ ቅብብሎሽ በመጠቀም መሻገር (ተራ) ለ NAT መሻገሪያም ጥቅም ላይ ይውላል፣ ነገር ግን ይህንን ተግባራዊ የሚያደርገው ለሁለቱም እኩዮች በሚታይ ተኪ በኩል በማስተላለፍ ነው። መዘግየትን ይጨምራል እና ከ STUN ይልቅ ለመተግበር በጣም ውድ ነው (ምክንያቱም በጠቅላላው የግንኙነት ክፍለ ጊዜ ውስጥ ስለሚተገበር) ፣ ግን አንዳንድ ጊዜ ብቸኛው አማራጭ ነው።
  • በይነተገናኝ ግንኙነት መመስረት (ICE) እኩዮችን በቀጥታ ከማገናኘት በተገኘ መረጃ እና በማንኛውም የ STUN እና TURN አገልጋዮች በተቀበለው መረጃ ላይ በመመስረት ሁለት እኩዮችን የማገናኘት ምርጡን ዘዴ ለመምረጥ ይጠቅማል።
  • የክፍለ ጊዜ መግለጫ ፕሮቶኮል (SDP) የግንኙነት ቻናል መለኪያዎችን የሚገልጽ ቅርጸት ነው ፣ ለምሳሌ ፣ የ ICE እጩዎች ፣ የመልቲሚዲያ ኮዴኮች (በድምጽ / ቪዲዮ ቻናል) ወዘተ ... ከእኩያዎቹ አንዱ የ SDP አቅርቦትን ይልካል ፣ ሁለተኛው ደግሞ በ SDP መልስ ይሰጣል ። . ከዚህ በኋላ ቻናል ይፈጠራል።

እንደዚህ አይነት ግንኙነት ለመፍጠር እኩዮች ከ STUN እና TURN አገልጋዮች የተቀበሉትን መረጃ መሰብሰብ እና እርስ በእርስ መለዋወጥ አለባቸው.

ችግሩ እስካሁን በቀጥታ የመግባቢያ ችሎታ ስለሌላቸው ይህን ውሂብ ለመለዋወጥ ከባንዱ ውጪ የሆነ ዘዴ መኖር አለበት፡ ምልክት ሰጪ አገልጋይ።

የምልክት ሰጪ አገልጋይ በጣም ቀላል ሊሆን ይችላል ምክንያቱም ስራው በመጨባበጥ ሂደት (ከታች ባለው ስእል እንደሚታየው) በእኩዮች መካከል ያለውን መረጃ ማስተላለፍ ብቻ ነው።

የባለብዙ ተጫዋች ጨዋታን ከC++ ወደ ድር በ Cheerp፣ WebRTC እና Firebase በማስተላለፍ ላይ
ቀለል ያለ WebRTC የመጨባበጥ ቅደም ተከተል ንድፍ

Teeworlds አውታረ መረብ ሞዴል አጠቃላይ እይታ

Teeworlds አውታረ መረብ አርክቴክቸር በጣም ቀላል ነው፡-

  • ደንበኛው እና የአገልጋይ አካላት ሁለት የተለያዩ ፕሮግራሞች ናቸው.
  • ደንበኞች ወደ ጨዋታው የሚገቡት ከበርካታ አገልጋዮች ወደ አንዱ በማገናኘት ሲሆን እያንዳንዳቸው በአንድ ጊዜ አንድ ጨዋታ ብቻ ያስተናግዳሉ።
  • በጨዋታው ውስጥ ሁሉም የውሂብ ማስተላለፍ በአገልጋዩ በኩል ይካሄዳል.
  • ልዩ ማስተር አገልጋይ በጨዋታ ደንበኛ ውስጥ የሚታዩትን ሁሉንም የህዝብ አገልጋዮች ዝርዝር ለመሰብሰብ ይጠቅማል።

ለመረጃ ልውውጥ WebRTC አጠቃቀም ምስጋና ይግባውና የጨዋታውን የአገልጋይ አካል ደንበኛው ወደሚገኝበት አሳሽ ማስተላለፍ እንችላለን። ይህ ትልቅ እድል ይሰጠናል ...

አገልጋዮችን ያስወግዱ

የአገልጋይ አመክንዮ አለመኖር ጥሩ ጠቀሜታ አለው፡ አፕሊኬሽኑን በሙሉ እንደ ቋሚ ይዘት በ Github Pages ወይም በራሳችን ሃርድዌር ከ Cloudflare ጀርባ ማሰማራት እንችላለን፣ በዚህም ፈጣን መውረዶችን እና ከፍተኛ የስራ ጊዜን በነጻ ማረጋገጥ እንችላለን። እንደውም እነርሱን ልንረሳቸው እንችላለን እና እድለኞች ከሆንን እና ጨዋታው ተወዳጅ ከሆነ መሰረተ ልማቱ ዘመናዊ መሆን የለበትም።

ነገር ግን፣ ስርዓቱ እንዲሰራ፣ አሁንም ውጫዊ አርክቴክቸርን መጠቀም አለብን፡-

  • አንድ ወይም ከዚያ በላይ የ STUN አገልጋዮች፡ የምንመርጣቸው ብዙ ነጻ አማራጮች አሉን።
  • ቢያንስ አንድ የ TURN አገልጋይ፡ እዚህ ምንም ነጻ አማራጮች ስለሌሉ የራሳችንን ማዋቀር ወይም ለአገልግሎቱ መክፈል እንችላለን። እንደ እድል ሆኖ፣ ብዙ ጊዜ ግንኙነቱ በSTUN አገልጋዮች በኩል ሊመሰረት ይችላል (እና እውነተኛ p2p ያቅርቡ)፣ ነገር ግን TURN እንደ ውድቀት አማራጭ ያስፈልጋል።
  • የምልክት ሰጪ አገልጋይ፡ ከሌሎቹ ሁለት ገጽታዎች በተለየ መልኩ ምልክት መስጠት ደረጃውን የጠበቀ አይደለም። የምልክት ሰጪው አገልጋዩ ተጠያቂ የሚሆነው በመተግበሪያው ላይ በመጠኑ ይወሰናል። በእኛ ሁኔታ, ግንኙነት ከመመሥረትዎ በፊት, አነስተኛ መጠን ያለው መረጃ መለዋወጥ አስፈላጊ ነው.
  • Teeworlds ማስተር አገልጋይ፡- ህልውናቸውን ለማስተዋወቅ እና በደንበኞች የህዝብ አገልጋዮችን ለማግኘት በሌሎች አገልጋዮች ጥቅም ላይ ይውላል። አስፈላጊ ባይሆንም (ደንበኞች ሁል ጊዜ በእጅ ከሚያውቁት አገልጋይ ጋር መገናኘት ይችላሉ) ፣ ተጫዋቾች በዘፈቀደ ሰዎች ውስጥ ባሉ ጨዋታዎች ውስጥ እንዲሳተፉ ማድረጉ ጥሩ ነው።

የGoogle ነፃ STUN አገልጋዮችን ለመጠቀም ወስነናል፣ እና አንድ TURN አገልጋይ እራሳችንን አሰማርተናል።

ለመጨረሻዎቹ ሁለት ነጥቦች ተጠቀምን። Firebase:

  • የTeeworlds ማስተር አገልጋይ በጣም ቀላል ነው የሚተገበረው፡ እንደ እያንዳንዱ ንቁ አገልጋይ መረጃ (ስም፣ አይፒ፣ ካርታ፣ ሁነታ፣ ...) የያዙ ነገሮች ዝርዝር ነው። አገልጋዮች የራሳቸውን ነገር ያትሙ እና ያዘምኑታል፣ እና ደንበኞች ሙሉውን ዝርዝር ወስደው ለተጫዋቹ ያሳያሉ። ተጫዋቾቹ በቀላሉ አገልጋዩን ጠቅ በማድረግ በቀጥታ ወደ ጨዋታው እንዲወሰዱ ዝርዝሩን በመነሻ ገጹ ላይ እንደ HTML እናሳያለን።
  • ምልክት ማድረጊያ በሚቀጥለው ክፍል ከተገለጸው የሶኬት አተገባበር ጋር በቅርበት የተያያዘ ነው።

የባለብዙ ተጫዋች ጨዋታን ከC++ ወደ ድር በ Cheerp፣ WebRTC እና Firebase በማስተላለፍ ላይ
በጨዋታው ውስጥ እና በመነሻ ገጽ ላይ ያሉ የአገልጋዮች ዝርዝር

ሶኬቶችን መተግበር

የሚፈለጉትን ለውጦች ብዛት ለመቀነስ በተቻለ መጠን ወደ Posix UDP Sockets ቅርብ የሆነ ኤፒአይ መፍጠር እንፈልጋለን።

እንዲሁም በአውታረ መረቡ ላይ በጣም ቀላል ለሆነ የመረጃ ልውውጥ የሚፈለገውን አስፈላጊውን ዝቅተኛ መተግበር እንፈልጋለን።

ለምሳሌ፣ እውነተኛ ማዘዋወር አያስፈልገንም፡ ሁሉም አቻዎች ከተለየ የFirebase ዳታቤዝ ምሳሌ ጋር በተገናኘ ተመሳሳይ "ምናባዊ LAN" ላይ ናቸው።

ስለዚህ፣ ልዩ የአይፒ አድራሻዎችን አንፈልግም፡ ልዩ የFirebase ቁልፍ እሴቶች (ከጎራ ስሞች ጋር ተመሳሳይ) እኩዮችን በልዩ ሁኔታ ለመለየት በቂ ናቸው፣ እና እያንዳንዱ እኩያ በየአካባቢው መተርጎም ለሚያስፈልገው እያንዳንዱ ቁልፍ “የውሸት” IP አድራሻዎችን ይመድባል። ይህ ዓለም አቀፋዊ የአይፒ አድራሻ ምደባ አስፈላጊነትን ሙሉ በሙሉ ያስወግዳል ፣ ይህም ቀላል ያልሆነ ተግባር ነው።

ተግባራዊ ለማድረግ የሚያስፈልገን አነስተኛው ኤፒአይ ይኸውና፡

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

ኤፒአይ ቀላል እና ከPosix Sockets API ጋር ተመሳሳይ ነው፣ ግን ጥቂት አስፈላጊ ልዩነቶች አሉት። መልሶ ጥሪዎችን መመዝገብ፣ የአካባቢ አይፒዎችን መመደብ እና ሰነፍ ግንኙነቶች.

መልሶ ጥሪዎችን በመመዝገብ ላይ

ምንም እንኳን ዋናው ፕሮግራም የማይከለክለው I/Oን ቢጠቀምም, ኮዱ በድር አሳሽ ውስጥ እንዲሰራ እንደገና መፈጠር አለበት.

ይህ የሆነበት ምክንያት በአሳሹ ውስጥ ያለው የዝግጅቱ ዑደት ከፕሮግራሙ (ጃቫ ስክሪፕት ወይም ዌብአሴምሊ ሊሆን ይችላል) ተደብቋል።

በአገሬው አካባቢ እንደዚህ አይነት ኮድ መጻፍ እንችላለን

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

የአካባቢ አይፒ ምደባ

በእኛ "መረብ" ውስጥ ያሉት የመስቀለኛ መንገድ መታወቂያዎች የአይ ፒ አድራሻዎች አይደሉም፣ ግን የFirebase ቁልፎች ናቸው (እነሱ ይህን የሚመስሉ ሕብረቁምፊዎች ናቸው፡- -LmEC50PYZLCiCP-vqde ).

ይህ ምቹ ነው ምክንያቱም አይፒዎችን ለመመደብ እና ልዩነታቸውን ለመፈተሽ (እንዲሁም ደንበኛው ከተቋረጠ በኋላ እነሱን ለማስወገድ) ዘዴ አያስፈልገንም ፣ ግን ብዙውን ጊዜ አቻዎችን በቁጥር እሴት መለየት ያስፈልጋል።

ይህ በትክክል ተግባሮቹ ጥቅም ላይ የሚውሉት ነው. resolve и reverseResolve: አፕሊኬሽኑ እንደምንም የቁልፉን string እሴት ይቀበላል (በተጠቃሚ ግብዓት ወይም በዋናው አገልጋይ) ፣ እና ለውስጣዊ አገልግሎት ወደ IP አድራሻ ሊለውጠው ይችላል። የተቀረው ኤፒአይ እንዲሁ ለቀላልነት ከሕብረቁምፊ ይልቅ ይህንን እሴት ይቀበላል።

ይህ ከዲ ኤን ኤስ ፍለጋ ጋር ተመሳሳይ ነው፣ ነገር ግን በደንበኛው ላይ በአካባቢው ይከናወናል።

ማለትም የአይ ፒ አድራሻዎችን በተለያዩ ደንበኞች መካከል ማጋራት አይቻልም፣ እና አንድ አይነት አለምአቀፍ መለያ ካስፈለገ በተለየ መንገድ መፈጠር አለበት።

ሰነፍ ግንኙነት

ዩዲፒ ግንኙነት አይፈልግም ነገር ግን እንዳየነው WebRTC በሁለት እኩዮች መካከል ውሂብ ማስተላለፍ ከመጀመሩ በፊት ረጅም የግንኙነት ሂደት ይፈልጋል።

ተመሳሳይ የአብስትራክሽን ደረጃ ማቅረብ ከፈለግን፣ (sendto/recvfrom ያለቅድመ ግንኙነት በዘፈቀደ እኩዮች)፣ ከዚያ በኤፒአይ ውስጥ “ሰነፍ” (የዘገየ) ግንኙነት ማከናወን አለባቸው።

ዩዲፒን ሲጠቀሙ በ‹አገልጋዩ› እና በ‹ደንበኛው› መካከል በተለመደው ግንኙነት ወቅት የሚሆነው እና የእኛ ቤተ-መጽሐፍት ምን ማድረግ እንዳለበት ይህ ነው።

  • የአገልጋይ ጥሪዎች bind()በተጠቀሰው ወደብ ላይ ፓኬቶችን መቀበል እንደሚፈልግ ለስርዓተ ክወናው መንገር.

ይልቁንስ ክፍት ወደብ ወደ ፋየርቤዝ በአገልጋይ ቁልፍ ስር እናተም እና በንዑስ ዛፉ ውስጥ ያሉትን ክስተቶች እናዳምጣለን።

  • የአገልጋይ ጥሪዎች recvfrom()በዚህ ወደብ ላይ ከማንኛውም አስተናጋጅ የሚመጡ እሽጎችን መቀበል።

በእኛ ሁኔታ ወደዚህ ወደብ የሚላኩትን የፓኬቶች ወረፋ ማረጋገጥ አለብን።

እያንዳንዱ ወደብ የራሱ ወረፋ አለው፣ እና ምንጩን እና መድረሻውን ወደቦች በ WebRTC ዳታግራም መጀመሪያ ላይ እንጨምራለን አዲስ ፓኬት ሲመጣ የትኛውን ወረፋ ማስተላለፍ እንዳለብን ለማወቅ።

ጥሪው አይከለከልም, ስለዚህ ምንም እሽጎች ከሌሉ በቀላሉ -1 ተመልሰን እናዘጋጃለን errno=EWOULDBLOCK.

  • ደንበኛው የአገልጋዩን አይፒ እና ወደብ በአንዳንድ ውጫዊ መንገዶች ይቀበላል እና ይደውላል sendto(). ይህ ደግሞ የውስጥ ጥሪ ያደርጋል። bind(), ስለዚህ ተከታይ recvfrom() ማሰርን በግልፅ ሳያስፈጽም ምላሹን ይቀበላል።

በእኛ ሁኔታ, ደንበኛው በውጫዊ መንገድ የሕብረቁምፊ ቁልፉን ይቀበላል እና ተግባሩን ይጠቀማል resolve() የአይፒ አድራሻ ለማግኘት.

በዚህ ጊዜ፣ ሁለቱ እኩዮች ገና እርስበርስ ካልተገናኙ የWebRTC መጨባበጥን እንጀምራለን። ከተመሳሳይ አቻ ወደቦች ጋር ያሉ ግንኙነቶች ተመሳሳዩን WebRTC DataChannel ይጠቀማሉ።

በተዘዋዋሪም እንሰራለን። bind()በሚቀጥለው ጊዜ አገልጋዩ እንደገና እንዲገናኝ sendto() በሆነ ምክንያት ቢዘጋ።

ደንበኛው የ SDP አቅርቦቱን በFirebase ውስጥ ባለው የአገልጋይ ወደብ መረጃ ስር ሲጽፍ አገልጋዩ የደንበኛውን ግንኙነት ያሳውቃል እና አገልጋዩ በዚያ ምላሽ ይሰጣል።

ከዚህ በታች ያለው ንድፍ ለሶኬት እቅድ የመልእክት ፍሰት እና የመጀመሪያውን መልእክት ከደንበኛው ወደ አገልጋዩ ለማስተላለፍ ምሳሌ ያሳያል።

የባለብዙ ተጫዋች ጨዋታን ከC++ ወደ ድር በ Cheerp፣ WebRTC እና Firebase በማስተላለፍ ላይ
በደንበኛው እና በአገልጋይ መካከል ያለው የግንኙነት ደረጃ የተሟላ ንድፍ

መደምደሚያ

ይህን እስካሁን ካነበብክ፣ ንድፈ ሃሳቡን በተግባር ለማየት ትፈልግ ይሆናል። ጨዋታው በ ላይ መጫወት ይችላል። teeworlds.leaningtech.com, ሞክረው!


በባልደረባዎች መካከል የወዳጅነት ግጥሚያ

የአውታረ መረብ ቤተመፃህፍት ኮድ በ ላይ በነጻ ይገኛል። የፊልሙ. ውይይቱን በቻናላችን ይቀላቀሉ Gitter!

ምንጭ: hab.com

አስተያየት ያክሉ