การย้ายเกมแบบผู้เล่นหลายคนจาก C++ ไปยังเว็บด้วย Cheerp, WebRTC และ Firebase

การแนะนำ

บริษัท ของเรา เทคโนโลยีเอนนิ่ง นำเสนอโซลูชันสำหรับการย้ายแอปพลิเคชันเดสก์ท็อปแบบเดิมไปยังเว็บ คอมไพเลอร์ C++ ของเรา กำลังใจ สร้างการรวมกันของ WebAssembly และ JavaScript ซึ่งมีทั้งสองอย่าง การโต้ตอบของเบราว์เซอร์อย่างง่ายและประสิทธิภาพสูง

เพื่อเป็นตัวอย่างในการใช้งาน เราตัดสินใจย้ายเกมที่มีผู้เล่นหลายคนไปยังเว็บและเลือก TeeWorlds. Teeworlds เป็นเกมย้อนยุค 2 มิติที่มีผู้เล่นหลายคนซึ่งมีชุมชนผู้เล่นเล็กๆ แต่กระตือรือร้น (รวมถึงฉันด้วย!) มีขนาดเล็กทั้งในแง่ของทรัพยากรที่ดาวน์โหลดและข้อกำหนดของ CPU และ GPU - เป็นตัวเลือกในอุดมคติ

การย้ายเกมแบบผู้เล่นหลายคนจาก C++ ไปยังเว็บด้วย Cheerp, WebRTC และ Firebase
ทำงานในเบราว์เซอร์ Teeworlds

เราตัดสินใจใช้โปรเจ็กต์นี้เพื่อทดลอง โซลูชันทั่วไปสำหรับการย้ายรหัสเครือข่ายไปยังเว็บ. โดยปกติจะทำด้วยวิธีต่อไปนี้:

  • XMLHttpRequest/ดึงข้อมูลหากส่วนของเครือข่ายประกอบด้วยคำขอ HTTP เท่านั้น หรือ
  • WebSockets.

โซลูชันทั้งสองจำเป็นต้องโฮสต์ส่วนประกอบเซิร์ฟเวอร์บนฝั่งเซิร์ฟเวอร์ และไม่อนุญาตให้ใช้เป็นโปรโตคอลการขนส่ง UDP. นี่เป็นสิ่งสำคัญสำหรับแอปพลิเคชันแบบเรียลไทม์ เช่น ซอฟต์แวร์การประชุมทางวิดีโอและเกม เนื่องจากรับประกันการส่งมอบและลำดับของแพ็กเก็ตโปรโตคอล TCP อาจกลายเป็นอุปสรรคต่อความหน่วงที่ต่ำ

มีวิธีที่สาม - ใช้เครือข่ายจากเบราว์เซอร์: WebRTC.

RTCDataChannel รองรับการส่งข้อมูลทั้งที่เชื่อถือได้และไม่น่าเชื่อถือ (ในกรณีหลังจะพยายามใช้ UDP เป็นโปรโตคอลการขนส่งทุกครั้งที่เป็นไปได้) และสามารถใช้ได้ทั้งกับเซิร์ฟเวอร์ระยะไกลและระหว่างเบราว์เซอร์ ซึ่งหมายความว่าเราสามารถย้ายแอปพลิเคชันทั้งหมดไปยังเบราว์เซอร์ได้ รวมถึงส่วนประกอบของเซิร์ฟเวอร์ด้วย!

อย่างไรก็ตาม สิ่งนี้มาพร้อมกับความยากลำบากเพิ่มเติม: ก่อนที่เพื่อน WebRTC สองคนจะสามารถสื่อสารได้ พวกเขาจำเป็นต้องทำการจับมือที่ค่อนข้างซับซ้อนเพื่อเชื่อมต่อ ซึ่งต้องใช้เอนทิตีบุคคลที่สามหลายรายการ (เซิร์ฟเวอร์การส่งสัญญาณและเซิร์ฟเวอร์หนึ่งเครื่องขึ้นไป งัน/กลับ).

ตามหลักการแล้ว เราต้องการสร้าง API เครือข่ายที่ใช้ WebRTC ภายใน แต่มีความใกล้เคียงกับอินเทอร์เฟซ UDP Sockets ที่ไม่จำเป็นต้องสร้างการเชื่อมต่อมากที่สุด

สิ่งนี้จะทำให้เราสามารถใช้ประโยชน์จาก WebRTC ได้โดยไม่ต้องเปิดเผยรายละเอียดที่ซับซ้อนให้กับโค้ดของแอปพลิเคชัน (ซึ่งเราต้องการเปลี่ยนแปลงให้น้อยที่สุดในโครงการของเรา)

WebRTC ขั้นต่ำ

WebRTC คือชุดของ API ที่มีอยู่ในเบราว์เซอร์ที่ให้การส่งข้อมูลเสียง วิดีโอ และข้อมูลที่กำหนดเองแบบเพียร์ทูเพียร์

การเชื่อมต่อระหว่างเพียร์ถูกสร้างขึ้น (แม้ว่าจะมี NAT อยู่ที่ด้านใดด้านหนึ่งหรือทั้งสองด้านก็ตาม) โดยใช้เซิร์ฟเวอร์ STUN และ/หรือ TURN ผ่านกลไกที่เรียกว่า ICE เพื่อนร่วมงานแลกเปลี่ยนข้อมูล ICE และพารามิเตอร์ช่องสัญญาณผ่านข้อเสนอและคำตอบของโปรโตคอล SDP

ว้าว! มีตัวย่อกี่ตัวในคราวเดียว? เรามาอธิบายสั้นๆ ว่าคำเหล่านี้หมายถึงอะไร:

  • ยูทิลิตี้การข้ามเซสชันสำหรับ NAT (งัน) — โปรโตคอลสำหรับการเลี่ยงผ่าน NAT และรับคู่ (IP, พอร์ต) เพื่อแลกเปลี่ยนข้อมูลโดยตรงกับโฮสต์ หากเขาจัดการงานของเขาให้สำเร็จ เพื่อนร่วมงานก็สามารถแลกเปลี่ยนข้อมูลระหว่างกันได้อย่างอิสระ
  • การสำรวจโดยใช้รีเลย์รอบ 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 Master Server: ถูกใช้โดยเซิร์ฟเวอร์อื่นเพื่อโฆษณาการมีอยู่ของพวกเขาและโดยไคลเอนต์เพื่อค้นหาเซิร์ฟเวอร์สาธารณะ แม้ว่าจะไม่จำเป็น (ไคลเอนต์สามารถเชื่อมต่อกับเซิร์ฟเวอร์ที่พวกเขารู้จักด้วยตนเองได้ตลอดเวลา) แต่ก็คงจะดีถ้ามีเพื่อให้ผู้เล่นสามารถเข้าร่วมในเกมกับผู้คนแบบสุ่มได้

เราตัดสินใจใช้เซิร์ฟเวอร์ STUN ฟรีของ Google และปรับใช้เซิร์ฟเวอร์ TURN หนึ่งเซิร์ฟเวอร์ด้วยตัวเราเอง

สำหรับสองจุดสุดท้ายที่เราใช้ Firebase:

  • เซิร์ฟเวอร์หลักของ Teeworlds ได้รับการปรับใช้อย่างง่ายดาย: เป็นรายการออบเจ็กต์ที่มีข้อมูล (ชื่อ, IP, แผนที่, โหมด, ...) ของแต่ละเซิร์ฟเวอร์ที่ใช้งานอยู่ เซิร์ฟเวอร์เผยแพร่และอัปเดตออบเจ็กต์ของตนเอง และไคลเอนต์จะนำรายการทั้งหมดไปแสดงให้ผู้เล่นเห็น นอกจากนี้เรายังแสดงรายการบนหน้าแรกเป็น HTML เพื่อให้ผู้เล่นสามารถคลิกบนเซิร์ฟเวอร์และเข้าสู่เกมได้ทันที
  • การส่งสัญญาณมีความเกี่ยวข้องอย่างใกล้ชิดกับการใช้งานซ็อกเก็ตของเรา ตามที่อธิบายไว้ในส่วนถัดไป

การย้ายเกมแบบผู้เล่นหลายคนจาก C++ ไปยังเว็บด้วย Cheerp, WebRTC และ Firebase
รายชื่อเซิร์ฟเวอร์ภายในเกมและในหน้าแรก

การติดตั้งซ็อกเก็ต

เราต้องการสร้าง API ที่ใกล้เคียงกับ Posix UDP Sockets มากที่สุดเพื่อลดจำนวนการเปลี่ยนแปลงที่จำเป็น

นอกจากนี้เรายังต้องการใช้ขั้นต่ำที่จำเป็นสำหรับการแลกเปลี่ยนข้อมูลที่ง่ายที่สุดผ่านเครือข่าย

ตัวอย่างเช่น เราไม่ต้องการการกำหนดเส้นทางจริง: เพียร์ทั้งหมดอยู่บน "LAN เสมือน" เดียวกันซึ่งเชื่อมโยงกับอินสแตนซ์ฐานข้อมูล 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 ในเครื่อง และการเชื่อมต่อแบบ Lazy.

การลงทะเบียนการโทรกลับ

แม้ว่าโปรแกรมดั้งเดิมจะใช้ 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;
    ...
  }
  ...
}

หากเราซ่อน Event Loop ไว้ เราต้องเปลี่ยนมันให้เป็นดังนี้:

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 และตรวจสอบความเป็นเอกลักษณ์ (รวมถึงการกำจัด 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() ในกรณีที่ปิดด้วยเหตุผลบางประการ

เซิร์ฟเวอร์จะได้รับแจ้งการเชื่อมต่อของลูกค้าเมื่อไคลเอนต์เขียนข้อเสนอ SDP ภายใต้ข้อมูลพอร์ตเซิร์ฟเวอร์ใน Firebase และเซิร์ฟเวอร์ตอบสนองด้วยการตอบสนองที่นั่น

แผนภาพด้านล่างแสดงตัวอย่างการไหลของข้อความสำหรับรูปแบบซ็อกเก็ตและการส่งข้อความแรกจากไคลเอนต์ไปยังเซิร์ฟเวอร์:

การย้ายเกมแบบผู้เล่นหลายคนจาก C++ ไปยังเว็บด้วย Cheerp, WebRTC และ Firebase
แผนภาพที่สมบูรณ์ของขั้นตอนการเชื่อมต่อระหว่างไคลเอนต์และเซิร์ฟเวอร์

ข้อสรุป

หากคุณอ่านมาไกลขนาดนี้ คุณอาจสนใจที่จะเห็นทฤษฎีนี้นำไปใช้จริง เกมสามารถเล่นได้ teeworlds.leaningtech.com, ลองมัน!


การแข่งขันกระชับมิตรระหว่างเพื่อนร่วมงาน

รหัสห้องสมุดเครือข่ายมีอิสระที่ Github. ร่วมสนทนาในช่องของเราได้ที่ ตะแกรง!

ที่มา: will.com

เพิ่มความคิดเห็น