- Lichess เป็นแพลตฟอร์มหมากรุกโอเพนซอร์สฟรีที่มีผู้เล่นหลายล้านคนทั่วโลก
- ใช้แท็บ Network ของ Chrome DevTools เพื่อติดตามการสื่อสารระหว่างไคลเอนต์กับเซิร์ฟเวอร์
การเชื่อมต่อ WebSocket
- พฤติกรรมเครือข่ายอย่างแรกที่น่าสังเกตคือการเชื่อมต่อ WebSocket ไปยัง URL ลักษณะคล้ายดังนี้:
wss://socket2.lichess.org/play/H5uHz0egyvIA/v6?sri=bt6QzcyOiZg5&v=0
- โปรโตคอล
wssหมายถึงการเชื่อมต่อ WebSocket แบบเข้ารหัสที่ใช้ TLS - WebSocket รองรับการสื่อสารแบบ full-duplex ทำให้ไคลเอนต์และเซิร์ฟเวอร์อัปเดตข้อมูลแบบเรียลไทม์ได้โดยไม่ต้องส่ง HTTP request ซ้ำๆ
ตาของผู้เล่นฝั่งเรา
- เมื่อมีการกระทำ จะมีการแลกเปลี่ยนแพ็กเก็ตข้อมูลดังนี้:
// ส่งเมื่อ 22:51:35.280
{
"t": "move",
"d": {
"u": "d2d4",
"l": 32,
"a": 1
}
}
- ข้อความที่ได้รับจากเซิร์ฟเวอร์:
// รับเมื่อ 22:51:35.312
{
"t": "ack",
"d": 1
}
- เป็นการแจ้งว่าเซิร์ฟเวอร์ได้รับการกระทำของเราแล้ว
// รับเมื่อ 22:51:35.312
{
"t": "move",
"v": 1,
"d": {
"uci": "d2d4",
"san": "d4",
"fen": "rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR",
"ply": 1,
"clock": {
"white": 300,
"black": 300
}
}
}
- ข้อความนี้ให้รายละเอียดของหมากที่เราเดินและสถานะเกมที่อัปเดตแล้ว
ตาของฝ่ายตรงข้าม
- เมื่อฝ่ายตรงข้ามเดินหมาก เราจะได้รับแพ็กเก็ตคล้ายกันจากเซิร์ฟเวอร์:
// รับเมื่อ 22:51:43.489
{
"t": "move",
"v": 2,
"d": {
"uci": "d7d5",
"san": "d5",
"fen": "rnbqkbnr/ppp1pppp/8/3p4/3P4/8/PPP1PPPP/RNBQKBNR",
"ply": 2,
"dests": {
"c2": "c3c4",
"g2": "g3g4"
// รายการหมากที่เดินได้เพิ่มเติม
},
"clock": {
"white": 300,
"black": 300
}
}
}
- พารามิเตอร์
destsแสดงรายการหมากเดินทั้งหมดที่สามารถใช้ได้จากตำแหน่งปัจจุบัน
สถาปัตยกรรมของ Lichess
- ระบบการเล่นแบบเรียลไทม์ของ Lichess ประกอบด้วยบริการหลักสองตัวเป็นหลัก (ทั้งคู่เขียนด้วย Scala):
lila: บริการแกนหลักที่จัดการฟังก์ชันสำคัญ เช่น logic ของเกม สถานะ และการโต้ตอบของผู้ใช้lila-ws: บริการเฉพาะทางด้านการจัดการ WebSocket ที่ทำหน้าที่เป็นสะพานเชื่อมระหว่างไคลเอนต์กับlila
ภาพรวมสถาปัตยกรรม
lila <-> redis <-> lila-ws <-> websocket <-> client
lilaสื่อสารกับlila-wsผ่าน Redis และlila-wsเป็นผู้ดูแลการเชื่อมต่อ WebSocket กับไคลเอนต์
การสื่อสารด้วย Redis Pub/Sub
- event การเดินหมากจะถูก publish ไปยังช่องทาง Redis Pub/Sub ซึ่ง
lilaจะ subscribe เพื่อประมวลผลการเดินหมาก - Redis Pub/Sub ให้การส่งแบบ at-most-once หมายความว่าข้อความอาจสูญหายได้ แต่ช่วยลดการใช้หน่วยความจำ
การทำให้ข้อมูลคงอยู่ถาวรใน MongoDB ในท้ายที่สุด
lilaบันทึกสถานะเกมลงใน MongoDB แต่จะไม่บันทึกทุกการเดินหมากแบบทันที- แต่จะบัฟเฟอร์การเดินหมากไว้และบันทึกเป็นระยะเพื่อลดภาระของฐานข้อมูล
- เมื่อเกิด event สำคัญ สถานะเกมจะถูก flush
การเข้าร่วมเกมที่กำลังดำเนินอยู่
- เมื่อผู้เล่นเชื่อมต่อ จะส่งพารามิเตอร์
vเพื่อบอกระบบว่าตนรู้จักเวอร์ชันล่าสุดของเกมถึงจุดใดแล้ว lila-wsใช้ConcurrentHashMapเพื่อติดตามและจัดการ event ทั้งหมดของเกมที่กำลังดำเนินอยู่
สรุปส่งท้าย
กระบวนการเดินหมากบน Lichess สรุปได้ดังนี้:
- ไคลเอนต์สร้างการเชื่อมต่อ WebSocket กับ
lila-ws - เมื่อผู้เล่นเดินหมาก ไคลเอนต์จะส่ง event การเดินหมากไปยัง
lila-ws lila-wsตอบกลับด้วยackเพื่อยืนยันว่าได้รับการเดินหมากแล้ว- event การเดินหมากถูก publish ไปยังช่องทาง Redis Pub/Sub และ
lilaเป็นผู้ประมวลผล lilaรับการเดินหมาก อัปเดตสถานะเกม และบันทึกลง MongoDB ในท้ายที่สุด จากนั้นสถานะเกมที่อัปเดตแล้วจะถูกส่งกลับไปยังไคลเอนต์ผ่านlila-ws- ไคลเอนต์ได้รับสถานะเกมที่อัปเดตแล้วซึ่งสะท้อนการเดินหมากใหม่และการเปลี่ยนแปลงของเกม
ความเห็นของ GN⁺
- โพสต์นี้พาไปดูสถาปัตยกรรมแบ็กเอนด์และกระบวนการที่ทำให้การเล่นแบบเรียลไทม์บน lichess.org ซึ่งเป็นแพลตฟอร์มหมากรุกโอเพนซอร์สยอดนิยมเกิดขึ้นได้อย่างละเอียด
- เนื้อหาแนะนำองค์ประกอบทางเทคนิคสำคัญที่ควรพิจารณาเมื่อต้องสร้างเว็บแอปแบบเรียลไทม์ เช่น การสื่อสารแบบเรียลไทม์ด้วย WebSocket การส่งข้อความที่ขยายระบบได้ด้วย Redis Pub/Sub และการบันทึกข้อมูลถาวรด้วย MongoDB
- สถาปัตยกรรมของ Lichess เหมาะมากกับเกมหลายผู้เล่นแบบเรียลไทม์ แต่รูปแบบและเทคนิคคล้ายกันก็สามารถนำไปใช้กับเว็บแอปเรียลไทม์ประเภทอื่นได้ด้วย เช่น แชต เครื่องมือทำงานร่วมกัน และฟีดโซเชียลมีเดีย
- ฟีเจอร์เรียลไทม์ช่วยยกระดับประสบการณ์ผู้ใช้และการโต้ตอบได้ แต่ก็มาพร้อมความท้าทายทางเทคนิคเฉพาะตัว เช่น scalability, reliability และ data consistency โดยโพสต์นี้เสนอแนวทางรับมือกับความท้าทายเหล่านี้
- ตัวอย่างโปรเจกต์โอเพนซอร์สที่ใช้เทคโนโลยีสแตกคล้ายกัน ได้แก่ Socket.IO (เฟรมเวิร์กสำหรับแอปเรียลไทม์บน Node.js) และ RethinkDB (ฐานข้อมูล NoSQL ที่เหมาะกับเว็บแอปเรียลไทม์)
- การวิเคราะห์ในโพสต์นี้ไม่ได้ตรวจดูซอร์สโค้ดของ Lichess โดยตรง จึงอาจมีความแตกต่างจากการติดตั้งใช้งานจริง แต่แนวคิดพื้นฐานและรูปแบบสถาปัตยกรรมที่อธิบายไว้ยังคงใช้ได้
- ในการออกแบบระบบเรียลไทม์ ควรพิจารณาอย่างรอบคอบว่าการส่งแบบ at-most-once (อาจเกิดการสูญหายของข้อความ) หรือ at-least-once (อาจเกิดข้อความซ้ำ) แบบใดเหมาะสมกว่า ทั้งนี้ขึ้นอยู่กับความต้องการและ trade-off ของแอปพลิเคชัน
1 ความคิดเห็น
ความเห็นจาก Hacker News
มีข้อร้องเรียนเกี่ยวกับโครงสร้างเวลาใน Chess.com ดูเหมือนว่าเซิร์ฟเวอร์จะเป็นฝ่ายติดตามเวลา จึงเหมือนไม่คำนึงถึงเวลาในการส่งข้อมูลและค่าหน่วง โดยเฉพาะเวลาที่เล่นเกมจับเวลาบนไคลเอนต์มือถือซึ่งทำให้ไม่สะดวกมาก
Lichess เลือกแนวทางแบบ StackOverflow และใช้เซิร์ฟเวอร์ที่ทรงพลัง
การคำนวณการเดินฝั่งเซิร์ฟเวอร์ช่วยรับประกันความสม่ำเสมอ และเพิ่มประสิทธิภาพการทำงานของไคลเอนต์ที่มีพลังประมวลผลหรือพลังงานจำกัด
ยังอธิบายไม่เพียงพอว่าจัดการกับข้อความสูญหายในช่อง Redis pub/sub อย่างไร
พารามิเตอร์ "l" อาจหมายถึงค่าหน่วงที่เซิร์ฟเวอร์สังเกตได้
น่าแปลกใจที่เซิร์ฟเวอร์จะไล่รายการการเดินถัดไปที่ถูกกฎหมายทั้งหมดแล้วส่งมา
มีคำถามเกี่ยวกับวิธีปกป้องเว็บซ็อกเก็ตเซิร์ฟเวอร์
สงสัยว่าเหตุใดโปรโตคอลจึงต้องมี ack
FEN เข้ารหัสเฉพาะสถานะของกระดาน ไม่รวมสถานะของเกม