วันนี้เมื่อ Node.js เต็มไปด้วยการแกว่งเราสามารถใช้มันเพื่อทำทุกสิ่งได้ เมื่อไม่นานมานี้โฮสต์เข้าร่วมในงาน Geek Song ในช่วงเหตุการณ์นี้เราตั้งใจที่จะสร้างเกมที่อนุญาตให้ "คนหัวลง" สื่อสารได้มากขึ้น ฟังก์ชั่นหลักคือการโต้ตอบแบบหลายบุคคลแบบเรียลไทม์ของแนวคิด LAN Party การแข่งขัน Geeks เป็นเพียง 36 ชั่วโมงที่น่าเวทนาอย่างน่าสงสารซึ่งต้องใช้ทุกอย่างให้รวดเร็วและรวดเร็ว ภายใต้หลักฐานดังกล่าวการเตรียมการครั้งแรกดูเหมือนจะเป็น "ธรรมชาติ" เล็กน้อย โซลูชันแอปพลิเคชันข้ามแพลตฟอร์มเราเลือก Node-Webkit ซึ่งง่ายพอและตรงตามข้อกำหนดของเรา
ตามข้อกำหนดการพัฒนาของเราสามารถดำเนินการแยกต่างหากตามโมดูล บทความนี้อธิบายถึงกระบวนการของการพัฒนา spaceroom (กรอบเกมหลายคนแบบเรียลไทม์ของเรา) รวมถึงชุดของการสำรวจและความพยายามรวมถึงวิธีแก้ปัญหาสำหรับข้อ จำกัด บางประการเกี่ยวกับ node.js และแพลตฟอร์ม webkit เองและข้อเสนอของโซลูชัน
เริ่มต้น
สเปซ
ในตอนแรกการออกแบบของ Spaceroom นั้นมีความต้องการอย่างแน่นอน เราหวังว่าเฟรมเวิร์กนี้จะให้ฟังก์ชั่นพื้นฐานต่อไปนี้:
สามารถแยกแยะกลุ่มผู้ใช้ตามห้อง (หรือช่อง)
สามารถรับคำแนะนำจากผู้ใช้ในกลุ่มคอลเลกชัน
เมื่อจับคู่ระหว่างลูกค้าแต่ละรายข้อมูลเกมสามารถถ่ายทอดได้อย่างถูกต้องตามช่วงเวลาที่กำหนด
สามารถกำจัดผลกระทบของความล่าช้าของเครือข่ายให้มากที่สุด
แน่นอนในระยะต่อมาของการเข้ารหัสเราได้จัดทำฟังก์ชั่นเพิ่มเติมเกี่ยวกับการทำงานมากขึ้นรวมถึงการหยุดเกมสร้างตัวเลขสุ่มที่สอดคล้องกันระหว่างลูกค้าแต่ละราย ฯลฯ (แน่นอนว่าสิ่งเหล่านี้สามารถนำไปใช้ได้ด้วยตัวเองตามข้อกำหนดและไม่จำเป็นต้องใช้ spaceroom ซึ่งเป็นกรอบการทำงาน
APIs
Spaceroom แบ่งออกเป็นสองส่วน: ด้านหน้าและด้านหลัง งานที่เซิร์ฟเวอร์ต้องการรวมถึงการบำรุงรักษารายการห้องและให้ฟังก์ชั่นการสร้างและเข้าร่วมห้อง APIs ลูกค้าของเรามีลักษณะเช่นนี้:
spaceroom.connect (ที่อยู่การโทรกลับ) เชื่อมต่อกับเซิร์ฟเวอร์
spaceroom.creaturoom (โทรกลับ) สร้างห้อง
spaceroom.oinroom (roomid) เข้าร่วมห้องพัก
spaceroom.on (กิจกรรมการโทรกลับ) ฟังกิจกรรม
-
หลังจากไคลเอนต์เชื่อมต่อกับเซิร์ฟเวอร์จะได้รับกิจกรรมต่าง ๆ ตัวอย่างเช่นผู้ใช้ในห้องอาจได้รับกิจกรรมที่ผู้เล่นใหม่เข้าร่วมหรือเหตุการณ์ที่เกมเริ่มต้น เราให้ "วงจรชีวิต" ซึ่งจะอยู่ในหนึ่งในรัฐต่อไปนี้ได้ตลอดเวลา:
คุณสามารถรับสถานะปัจจุบันของลูกค้าผ่าน spaceroom.state
การใช้เฟรมเวิร์กฝั่งเซิร์ฟเวอร์นั้นค่อนข้างง่าย หากคุณใช้ไฟล์กำหนดค่าเริ่มต้นคุณสามารถเรียกใช้เฟรมเวิร์กฝั่งเซิร์ฟเวอร์ได้โดยตรง เรามีข้อกำหนดพื้นฐาน: รหัสเซิร์ฟเวอร์สามารถทำงานได้โดยตรงในไคลเอนต์โดยไม่จำเป็นต้องใช้เซิร์ฟเวอร์แยกต่างหาก ผู้เล่นที่เล่น PS หรือ PSP ควรรู้ว่าฉันกำลังพูดถึงอะไร แน่นอนว่ามันยอดเยี่ยมในการทำงานบนเซิร์ฟเวอร์พิเศษ
การใช้งานรหัสเชิงตรรกะนั้นสั้น ๆ ที่นี่ Spaceroom รุ่นแรกเสร็จสิ้นฟังก์ชั่นของเซิร์ฟเวอร์ซ็อกเก็ตซึ่งเก็บรักษารายการห้องพักรวมถึงสถานะของห้องและการสื่อสารเวลาของเกมที่สอดคล้องกับแต่ละห้อง (คอลเลกชันคำแนะนำการออกอากาศถัง ฯลฯ ) สำหรับการใช้งานเฉพาะโปรดดูที่ซอร์สโค้ด
อัลกอริทึมแบบซิงโครนัส
ดังนั้นเราจะทำให้สิ่งต่าง ๆ ปรากฏขึ้นระหว่างลูกค้าแต่ละรายที่สอดคล้องกันแบบเรียลไทม์ได้อย่างไร?
สิ่งนี้ฟังดูน่าสนใจ คิดอย่างรอบคอบเราต้องการเซิร์ฟเวอร์อะไรเพื่อช่วยเราส่งมอบ? โดยธรรมชาติคุณจะนึกถึงสิ่งที่อาจทำให้เกิดความไม่สอดคล้องกันทางตรรกะระหว่างลูกค้าต่างๆ: คำแนะนำของผู้ใช้ เนื่องจากรหัสที่เกี่ยวข้องกับลอจิกของเกมนั้นเหมือนกันเนื่องจากเงื่อนไขเดียวกันรหัสจะทำงานผลลัพธ์เดียวกัน ความแตกต่างเพียงอย่างเดียวคือคำสั่งผู้เล่นที่ได้รับในระหว่างเกม ถูกต้องเราต้องการวิธีการซิงโครไนซ์คำแนะนำเหล่านี้ หากลูกค้าทั้งหมดสามารถได้รับคำสั่งเดียวกันลูกค้าทั้งหมดสามารถมีผลการดำเนินการเดียวกันในทางทฤษฎี
อัลกอริทึมการซิงโครไนซ์ของเกมออนไลน์เป็นสถานการณ์ที่แปลกและใช้งานได้ทุกประเภท Spaceroom ใช้อัลกอริทึมการซิงโครไนซ์คล้ายกับแนวคิดของการล็อคเฟรม เราแบ่งไทม์ไลน์ออกเป็นช่วงเวลาทีละหนึ่งและแต่ละช่วงเวลาเรียกว่าถัง Bucket ใช้ในการโหลดคำแนะนำและได้รับการดูแลโดยฝั่งเซิร์ฟเวอร์ ในตอนท้ายของแต่ละช่วงเวลาของถังเซิร์ฟเวอร์จะออกอากาศถังไปยังไคลเอนต์ทั้งหมด หลังจากลูกค้าได้รับถังคำแนะนำจะถูกดึงออกมาจากนั้นดำเนินการหลังจากการตรวจสอบ
เพื่อลดผลกระทบของการหน่วงเวลาของเครือข่ายแต่ละคำสั่งที่ได้รับจากเซิร์ฟเวอร์จากไคลเอนต์จะถูกส่งไปยังถังที่เกี่ยวข้องตามอัลกอริทึมที่แน่นอนและขั้นตอนต่อไปนี้จะปฏิบัติตามโดยเฉพาะ:
ให้ order_start เป็นเวลาของคำสั่งที่ดำเนินการโดยคำสั่งและ t คือเวลาเริ่มต้นของถังที่ order_start ตั้งอยู่
หาก t + delay_time <= เวลาเริ่มต้นของถังที่กำลังรวบรวมคำสั่งส่งคำสั่งไปยังถังที่กำลังรวบรวมคำสั่งอยู่ในขณะนี้มิฉะนั้นดำเนินการต่อขั้นตอนที่ 3 ดำเนินการต่อ
ส่งคำสั่งไปยังถังที่สอดคล้องกันของ t + delay_time
ในกรณีที่ Delay_time เป็นเวลาหน่วงของเซิร์ฟเวอร์ที่ตกลงกันไว้ซึ่งสามารถใช้เป็นความล่าช้าเฉลี่ยระหว่างลูกค้า ค่าเริ่มต้นใน spaceroom คือ 80 และค่าเริ่มต้นของความยาวของถังคือ 48 ในตอนท้ายของแต่ละช่วงเวลาที่ฝากข้อมูลเซิร์ฟเวอร์ออกอากาศถังนี้ไปยังไคลเอนต์ทั้งหมดและเริ่มรับคำแนะนำสำหรับถังถัดไป ไคลเอนต์ควบคุมข้อผิดพลาดเวลาภายในช่วงที่ยอมรับได้เมื่อทำการจับคู่ในตรรกะโดยอัตโนมัติตามช่วงเวลาของถังที่ได้รับ
ซึ่งหมายความว่าภายใต้สถานการณ์ปกติไคลเอนต์จะได้รับถังที่ส่งจากเซิร์ฟเวอร์ทุก 48ms เมื่อเวลาที่ต้องประมวลผลถังลูกค้าจะจัดการได้ตามนั้น สมมติว่าไคลเอนต์ FPS = 60 จะได้รับถังทุก ๆ 3 เฟรมหรือมากกว่านั้นและตรรกะจะได้รับการปรับปรุงตามที่เก็บข้อมูลนี้ หากไม่ได้รับถังหลังจากเวลาหมดอายุเนื่องจากความผันผวนของเครือข่ายลูกค้าจะหยุดตรรกะของเกมและรอ ในเวลาภายในถังสามารถอัปเดตตรรกะได้โดยใช้วิธี LERP
ในกรณีของ delay_time = 80, bucket_size = 48 คำสั่งใดจะล่าช้าโดยการดำเนินการอย่างน้อย 96ms เปลี่ยนพารามิเตอร์ทั้งสองนี้ตัวอย่างเช่นในกรณีของ delay_time = 60, bucket_size = 32, คำสั่งทั้งสองจะล่าช้าอย่างน้อย 64ms
เหตุการณ์เลือดที่เกิดจากการจับเวลา
เมื่อมองไปที่สิ่งทั้งหมดกรอบของเราจะต้องมีตัวจับเวลาที่แม่นยำเมื่อมันทำงาน ดำเนินการออกอากาศของถังภายใต้ช่วงเวลาคงที่ แน่นอนว่าก่อนอื่นเราคิดถึงการใช้ setInterval () แต่วินาทีต่อไปเราก็ตระหนักว่าแนวคิดนี้ไม่น่าเชื่อถือเพียงใด: Naughty setInterval () ดูเหมือนจะมีข้อผิดพลาดร้ายแรงมาก และสิ่งที่แย่มากคือทุกข้อผิดพลาดจะสะสมทำให้เกิดผลร้ายแรงมากขึ้น
ดังนั้นเราจึงนึกถึงการใช้ settimeout () เพื่อทำให้ตรรกะของเรามีความเสถียรประมาณรอบช่วงเวลาที่กำหนดโดยแก้ไขเวลาของการมาถึงครั้งต่อไปแบบไดนามิก ตัวอย่างเช่นครั้งนี้ settimeout () น้อยกว่าที่คาดไว้ 5ms ดังนั้นเราจะปล่อยให้มันล่วงหน้า 5ms ก่อนเวลาในครั้งต่อไป อย่างไรก็ตามผลการทดสอบไม่เป็นที่น่าพอใจและนี่ไม่ได้สง่างามพอไม่ว่าคุณจะมองอย่างไร
ดังนั้นเราต้องเปลี่ยนความคิดของเรา เป็นไปได้ไหมที่จะทำให้ Settimeout () หมดอายุโดยเร็วที่สุดเท่าที่จะเป็นไปได้จากนั้นเราตรวจสอบว่าเวลาปัจจุบันถึงเวลาเป้าหมายหรือไม่ ตัวอย่างเช่นในลูปของเราโดยใช้ settimeout (โทรกลับ 1) เพื่อตรวจสอบเวลาซึ่งดูเหมือนจะเป็นความคิดที่ดี
ตัวจับเวลาที่น่าผิดหวัง
เราเขียนรหัสชิ้นหนึ่งเพื่อทดสอบความคิดของเราทันทีและผลลัพธ์ก็น่าผิดหวัง ในเวอร์ชันที่มีเสถียรภาพล่าสุดของ node.js (v0.10.32) และแพลตฟอร์ม Windows ให้เรียกใช้รหัสชิ้นนี้:
การคัดลอกรหัสมีดังนี้:
var sum = 0, count = 0;
ฟังก์ชั่นทดสอบ () {
var now = date.now ();
settimeout (function () {
var diff = date.now () - ตอนนี้;
sum += diff;
นับ ++;
ทดสอบ();
-
-
ทดสอบ();
หลังจากระยะเวลาหนึ่งให้ป้อนผลรวม/นับในคอนโซลและคุณสามารถเห็นผลลัพธ์คล้ายกับ:
การคัดลอกรหัสมีดังนี้:
> ผลรวม / นับ
15.624555160142348
อะไร?! ฉันต้องการช่วงเวลา 1ms แต่คุณบอกฉันว่าช่วงเวลาเฉลี่ยที่แท้จริงคือ 15.625ms! ภาพนี้สวยเกินไป เราทำการทดสอบแบบเดียวกันกับ Mac และผลลัพธ์คือ 1.4ms ดังนั้นเราจึงสับสน: นี่คืออะไร? ถ้าฉันเป็นแฟนแอปเปิลฉันอาจสรุปได้ว่าหน้าต่างนั้นขยะเกินไปและยอมแพ้กับหน้าต่าง โชคดีที่ฉันเป็นวิศวกรส่วนหน้าอย่างเข้มงวดดังนั้นฉันจึงเริ่มคิดถึงหมายเลขนี้ต่อไป
เดี๋ยวก่อนทำไมหมายเลขนี้ถึงคุ้นเคย? ตัวเลขนี้จะคล้ายกับช่วงเวลาตัวจับเวลาสูงสุดภายใต้ Windows หรือไม่? ฉันดาวน์โหลดนาฬิกาทันทีเพื่อทดสอบและหลังจากเรียกใช้คอนโซลฉันได้ผลลัพธ์ต่อไปนี้:
การคัดลอกรหัสมีดังนี้:
ช่วงเวลาตัวจับเวลาสูงสุด: 15.625 ms
ช่วงเวลาเทียมขั้นต่ำ: 0.500 มิลลิวินาที
ช่วงเวลาจับเวลาปัจจุบัน: 1.001 ms
แน่นอน ดูที่คู่มือ node.js เราสามารถดูคำอธิบายของ settimout เช่นนี้:
ความล่าช้าที่เกิดขึ้นจริงขึ้นอยู่กับปัจจัยภายนอกเช่นการจับเวลาของระบบปฏิบัติการและภาระของระบบ
อย่างไรก็ตามผลการทดสอบแสดงให้เห็นว่าความล่าช้าที่เกิดขึ้นจริงนี้คือช่วงเวลาตัวจับเวลาสูงสุด (โปรดทราบว่าช่วงเวลาตัวจับเวลาปัจจุบันของระบบมีเพียง 1.001ms) ซึ่งไม่สามารถยอมรับได้ในกรณีใด ๆ ความอยากรู้อยากเห็นที่แข็งแกร่งทำให้เรามองผ่านซอร์สโค้ดของ node.js เพื่อให้ได้เห็นความจริง
บั๊กใน node.js
ฉันเชื่อว่าพวกคุณส่วนใหญ่และฉันมีความเข้าใจบางอย่างเกี่ยวกับกลไกการวนซ้ำของ Node.js เมื่อดูที่ซอร์สโค้ดของการใช้งานตัวจับเวลาเราสามารถเข้าใจหลักการการใช้งานของตัวจับเวลาได้อย่างคร่าว ๆ เริ่มต้นด้วยห่วงหลักของ Event Loop:
การคัดลอกรหัสมีดังนี้:
ในขณะที่ (r! = 0 && loop-> stop_flag == 0) {
/* อัปเดตเวลาทั่วโลก*/
UV_UPDATE_TIME (ลูป);
/* ตรวจสอบว่าตัวจับเวลาหมดอายุและเรียกใช้การโทรกลับตัวจับเวลาที่เกี่ยวข้อง*//
uv_process_timers (วน);
/* โทรหาโทรกลับไม่ได้ใช้งานหากไม่มีอะไรทำ -
if (loop-> pending_reqs_tail == null &&
loop-> endgame_handles == null) {
/* ป้องกันไม่ให้ลูปเหตุการณ์ออกจาก*/
UV_IDLE_INVOKE (วน);
-
UV_PROCESS_REQS (ลูป);
UV_PROCESS_ENDGAMES (ลูป);
uv_prepare_invoke (วน);
/* รวบรวมกิจกรรม io*/
(*โพล) (วนลูป, ลูป-> idle_handles == null &&
loop-> pending_reqs_tail == null &&
loop-> endgame_handles == null &&
! loop-> stop_flag &&
(loop-> active_handles> 0 ||
! ngx_queue_empty (& loop-> active_reqs)) &&
! (โหมด & uv_run_nowait));
/* setimmediate () ฯลฯ*/
UV_CHECK_INVOKE (วน);
r = uv__loop_alive (ลูป);
if (โหมด & (uv_run_once | uv_run_nowait)))
หยุดพัก;
-
ซอร์สโค้ดของฟังก์ชัน UV_UPDATE_TIME มีดังนี้: (https://github.com/joyent/libuv/blob/v0.10/src/win/timer.c))
การคัดลอกรหัสมีดังนี้:
เป็นโมฆะ uv_update_time (uv_loop_t* loop) {
/* รับเวลาระบบปัจจุบัน*/
dword ticks = getTickCount ();
/ * สมมติฐานถูกสร้างขึ้นที่ large_integer.quadpart มีประเภทเดียวกัน */
/* loop-> เวลาซึ่งเกิดขึ้นเป็น มีวิธีใดที่จะยืนยันสิ่งนี้ได้หรือไม่? -
large_integer* time = (large_integer*) & loop-> time;
/* หากตัวจับเวลาห่อให้เพิ่ม 1 ลงใน DWORD ลำดับสูง -
/ * uv_poll ต้องตรวจสอบให้แน่ใจว่าตัวจับเวลาไม่สามารถล้นเกิน *//
/* หนึ่งครั้งระหว่างการโทร UV_UPDATE_TIME ที่ตามมาสองครั้ง -
if (ticks <time-> lowpart) {
เวลา-> highpart += 1;
-
เวลา-> lowpart = เห็บ;
-
การใช้งานภายในของฟังก์ชั่นนี้ใช้ฟังก์ชัน getTickCount () ของ Windows เพื่อตั้งค่าเวลาปัจจุบัน พูดง่ายๆหลังจากเรียกใช้ฟังก์ชัน settimeout หลังจากการต่อสู้หลายชุดตัวจับเวลาภายใน-> ครบกำหนดจะถูกตั้งค่าเป็นเวลาวนรอบปัจจุบัน + หมดเวลา ในเหตุการณ์ลูปอัปเดตครั้งแรกเวลาของลูปปัจจุบันผ่าน UV_UPDATE_TIME จากนั้นตรวจสอบว่าตัวจับเวลาจะหมดอายุใน UV_PROCESS_TIMES หรือไม่ ถ้าเป็นเช่นนั้นเข้าสู่โลกแห่งจาวาสคริปต์ หลังจากอ่านบทความทั้งหมดลูปเหตุการณ์จะเหมือนกับกระบวนการนี้:
อัปเดตเวลาทั่วโลก
ตรวจสอบตัวจับเวลา หากตัวจับเวลาหมดอายุให้เรียกใช้การโทรกลับ
ตรวจสอบคิว REQS และดำเนินการตามคำขอรอคอย
ป้อนฟังก์ชั่นการสำรวจความคิดเห็นเพื่อรวบรวมเหตุการณ์ IO หากเหตุการณ์ IO มาถึงให้เพิ่มฟังก์ชั่นการประมวลผลที่สอดคล้องกันในคิว REQS สำหรับการดำเนินการในลูปเหตุการณ์ถัดไป ภายในฟังก์ชั่นการสำรวจความคิดเห็นจะมีการเรียกใช้วิธีการของระบบเพื่อรวบรวมเหตุการณ์ IO วิธีนี้จะทำให้กระบวนการบล็อกจนกว่าเหตุการณ์ IO จะมาถึงหรือถึงเวลาหมดเวลาที่กำหนด เมื่อวิธีการนี้ถูกเรียกใช้เวลาหมดเวลาจะถูกตั้งค่าเป็นเวลาที่ตัวจับเวลาล่าสุดหมดอายุ หมายความว่าเหตุการณ์ IO จะถูกรวบรวมโดยการปิดกั้นและเวลาปิดกั้นสูงสุดเป็นครั้งสุดท้ายของการจับเวลาครั้งต่อไป
ซอร์สโค้ดของหนึ่งในฟังก์ชั่นการสำรวจภายใต้ Windows:
การคัดลอกรหัสมีดังนี้:
โมฆะคงที่ UV_POLL (UV_LOOP_T* ลูป, บล็อก int) {
DWORD BYTES, หมดเวลา;
คีย์ ulong_ptr;
ซ้อนทับ* ซ้อนทับ;
uv_req_t* req;
ถ้า (บล็อก) {
/* ใช้เวลาหมดอายุของตัวจับเวลาล่าสุด*/
หมดเวลา = UV_GET_POLL_TIMEOUT (ลูป);
} อื่น {
หมดเวลา = 0;
-
getqueuedcompletionstatus (loop-> iocp,
& ไบต์
&สำคัญ,
& ทับซ้อนกัน
/* ที่บล็อกมากที่สุดจนกว่าตัวจับเวลาถัดไปจะหมดอายุ*/
หมดเวลา);
ถ้า (ซ้อนทับ) {
/ * แพ็คเกจถูก dequeued */
REQ = UV_OVERLAPPED_TO_REQ (ซ้อนทับ);
/* แทรกเหตุการณ์ io ลงในคิว*/
uv_insert_pending_req (ลูป, req);
} อื่นถ้า (getLasterRor ()! = wait_timeout) {
/ * ข้อผิดพลาดร้ายแรง */
uv_fatal_error (getLasterror (), "getqueuedcompletionstatus");
-
-
ตามขั้นตอนข้างต้นสมมติว่าเราตั้งค่าตัวจับเวลา timeout = 1ms ฟังก์ชั่นการสำรวจจะบล็อกที่ 1ms ส่วนใหญ่แล้วกู้คืนหลังจากการกู้คืน (หากไม่มีเหตุการณ์ IO ในช่วงเวลา) เมื่อดำเนินการต่อเพื่อป้อนลูปลูปเหตุการณ์ UV_UPDATE_TIME จะอัปเดตเวลาจากนั้น UV_PROCESS_TIMERS จะพบว่าตัวจับเวลาของเราหมดอายุและเรียกใช้การโทรกลับ ดังนั้นการวิเคราะห์เบื้องต้นคือ UV_UPDATE_TIME มีปัญหา (เวลาปัจจุบันไม่ได้รับการปรับปรุงอย่างถูกต้อง) หรือฟังก์ชั่นการสำรวจความคิดเห็นรอ 1ms แล้วกู้คืน มีปัญหากับการรอ 1 ม. นี้
เมื่อดูที่ MSDN เราค้นพบคำอธิบายของฟังก์ชั่น getTickCount:
ความละเอียดของฟังก์ชัน getTickCount นั้น จำกัด อยู่ที่ความละเอียดของตัวจับเวลาระบบซึ่งโดยทั่วไปจะอยู่ในช่วง 10 ล้านวินาทีถึง 16 ล้านวินาที
ความแม่นยำของ GetTickCount นั้นหยาบมาก! สมมติว่าฟังก์ชั่นการสำรวจความคิดเห็นบล็อกเวลา 1ms อย่างถูกต้อง แต่ในครั้งต่อไป UV_UPDATE_TIME จะถูกดำเนินการเวลาวนรอบปัจจุบันไม่ได้รับการอัปเดตอย่างถูกต้อง! ดังนั้นตัวจับเวลาของเราจึงไม่ได้รับการตัดสินว่าหมดอายุดังนั้นแบบสำรวจจึงรออีก 1ms และป้อนลูปเหตุการณ์ต่อไป จนกว่าจะได้รับการอัปเดตอย่างถูกต้องในที่สุด (ที่เรียกว่า 15.625ms ได้รับการอัปเดตหนึ่งครั้ง) เวลาปัจจุบันของลูปได้รับการอัปเดตและตัวจับเวลาของเราจะถูกตัดสินว่าหมดอายุใน UV_Process_Timers
ขอความช่วยเหลือจาก WebKit
ซอร์สโค้ดนี้ของ node.js นี้ทำอะไรไม่ถูกมาก: เขาใช้ฟังก์ชั่นเวลาที่มีความแม่นยำต่ำและไม่ได้ทำอะไรเลย แต่เราคิดทันทีว่าเนื่องจากเราใช้ Node-Webkit นอกเหนือจากการตั้งถิ่นฐานของ Node.js เรายังมีการตั้งถิ่นฐานของโครเมียมด้วย เขียนรหัสทดสอบและใช้เบราว์เซอร์หรือ Node-webkit เพื่อเรียกใช้: http://marks.lrednight.com/test.html#1 (# ตามด้วยหมายเลขระบุช่วงเวลาที่จะวัด) ผลลัพธ์มีดังนี้:
ตามข้อกำหนดของ HTML5 ผลลัพธ์ทางทฤษฎีควรเป็นผลลัพธ์ 5 รายการแรกคือ 1ms และผลลัพธ์ถัดไปคือ 4ms ผลลัพธ์ที่แสดงในกรณีทดสอบเริ่มต้นจากครั้งที่สามซึ่งหมายความว่าข้อมูลในตารางควรเป็น 1ms ในทางทฤษฎีในสามครั้งแรกและผลลัพธ์หลังจากนั้นเป็น 4ms ผลที่ได้มีข้อผิดพลาดบางอย่างและตามกฎระเบียบผลลัพธ์ทางทฤษฎีที่เล็กที่สุดที่เราสามารถได้รับคือ 4ms แม้ว่าเราจะไม่พอใจ แต่ก็เห็นได้ชัดว่าน่าพึงพอใจมากกว่าผลของ Node.js แนวโน้มความอยากรู้อยากเห็นที่แข็งแกร่งลองมาดูซอร์สโค้ดของโครเมียมเพื่อดูว่ามันถูกนำไปใช้อย่างไร (https://chromium.googleource.com/chromium/src.git/+/38.0.2125.101/base/time/time_win.cc)
ขั้นแรกโครเมียมใช้ฟังก์ชั่น TimeGetTime () ในการกำหนดเวลาปัจจุบันของลูป เมื่อดูที่ MSDN คุณจะพบว่าความแม่นยำของฟังก์ชั่นนี้ได้รับผลกระทบจากช่วงเวลาการจับเวลาปัจจุบันของระบบ ในเครื่องทดสอบของเรามันเป็นทางทฤษฎี 1.001ms ที่กล่าวถึงข้างต้น อย่างไรก็ตามโดยค่าเริ่มต้นช่วงเวลาตัวจับเวลาคือค่าสูงสุด (15.625ms บนเครื่องทดสอบ) เว้นแต่แอปพลิเคชันจะปรับเปลี่ยนช่วงเวลาของตัวจับเวลาทั่วโลก
หากคุณติดตามข่าวในอุตสาหกรรมไอทีคุณต้องเห็นข่าวดังกล่าว ดูเหมือนว่าโครเมียมของเราได้กำหนดช่วงเวลาตัวจับเวลาเล็กมาก! ดูเหมือนว่าเราไม่ต้องกังวลเกี่ยวกับช่วงเวลาตัวจับเวลาระบบใช่ไหม อย่ามีความสุขเร็วเกินไปการซ่อมแซมเช่นนี้ทำให้เราระเบิด ในความเป็นจริงปัญหานี้ได้รับการแก้ไขใน Chrome 38 เราควรใช้การแก้ไขโหนด Webkit ก่อนหน้านี้หรือไม่? เห็นได้ชัดว่านี่ไม่หรูหราพอและป้องกันไม่ให้เราใช้โครเมียมรุ่นที่มีประสิทธิภาพมากขึ้น
เมื่อมองเพิ่มเติมที่ซอร์สโค้ดโครเมียมเราสามารถพบว่าเมื่อมีตัวจับเวลาและหมดเวลาของการหมดเวลา <32ms โครเมียมจะเปลี่ยนช่วงเวลาตัวจับเวลาทั่วโลกของระบบเพื่อให้ได้ตัวจับเวลาด้วยความแม่นยำน้อยกว่า 15.625ms (ดูซอร์สโค้ด) เมื่อเริ่มจับเวลาสิ่งที่เรียกว่า HighResolutionTimerManager จะถูกเปิดใช้งาน คลาสนี้จะเรียกฟังก์ชัน EnableHighResolutionTimer ตามประเภทพลังงานของอุปกรณ์ปัจจุบัน โดยเฉพาะอย่างยิ่งเมื่ออุปกรณ์ปัจจุบันใช้แบตเตอรี่มันจะเรียกว่า enablehighresolutiontimer (เท็จ) และจริงจะถูกส่งผ่านเมื่อใช้พลังงาน การใช้งานฟังก์ชั่น enablehighresolutiontimer มีดังนี้:
การคัดลอกรหัสมีดังนี้:
เป็นโมฆะเวลา :: enableHighResolutionTimer (เปิดใช้งานบูล) {
ฐาน :: ล็อคอัตโนมัติ (g_high_res_lock.get ());
if (g_high_res_timer_enabled == เปิดใช้งาน)
กลับ;
g_high_res_timer_enabled = enable;
if (! g_high_res_timer_count)
กลับ;
// ตั้งแต่ g_high_res_timer_count! = 0, activateHighResolutionTimer (จริง)
// ถูกเรียกว่าเรียกว่า timebeginperiod ด้วย g_high_res_timer_enabled
// ด้วยค่าซึ่งตรงกันข้ามกับ | เปิดใช้งาน | ด้วยข้อมูลนั้นเรา
// โทร TimeendPeriod ด้วยค่าเดียวกับที่ใช้ใน TimebeGinPeriod และ
// ดังนั้นจึงยกเลิกผลระยะเวลา
ถ้า (เปิดใช้งาน) {
TimeendPeriod (KmintimerIntervallowResms);
TimebeginPeriod (KmintimerIntervalhighresms);
} อื่น {
TimeendPeriod (KmintimerIntervalhighresms);
TimebeginPeriod (KmintimerIntervallowResms);
-
-
โดยที่ kmintimerIntervallowResms = 4 และ kmintimerIntervalHighResms = 1. timebeginperiod และ TimeNendPeriod เป็นฟังก์ชั่นที่ Windows จัดทำขึ้นเพื่อแก้ไขช่วงเวลาการจับเวลาระบบ กล่าวคือเมื่อเชื่อมต่อกับแหล่งจ่ายไฟช่วงเวลาตัวจับเวลาที่เล็กที่สุดที่เราจะได้รับคือ 1ms ในขณะที่เมื่อใช้แบตเตอรี่จะเป็น 4ms เนื่องจากการวนซ้ำของเราเรียกอย่างต่อเนื่องว่า Settimeout ตามข้อกำหนดของ W3C ช่วงเวลาขั้นต่ำจึงเป็น 4ms ดังนั้นฉันจึงรู้สึกโล่งใจสิ่งนี้มีผลกระทบเล็กน้อยต่อเรา
ปัญหาที่แม่นยำอีกประการหนึ่ง
กลับไปที่จุดเริ่มต้นเราพบว่าผลการทดสอบแสดงให้เห็นว่าช่วงเวลาของการตั้งถิ่นฐานไม่มั่นคงที่ 4ms แต่มีความผันผวนอย่างต่อเนื่อง http://marks.lrednight.com/test.html#48 ผลการทดสอบยังแสดงให้เห็นว่าช่วงเวลากำลังเต้นระหว่าง 48ms และ 49ms เหตุผลก็คือในเหตุการณ์วนรอบโครเมียมและ node.js ความแม่นยำของการเรียกใช้ฟังก์ชัน Windows ที่รอเหตุการณ์ IO ได้รับผลกระทบจากตัวจับเวลาของระบบปัจจุบัน การใช้งานของลอจิกเกมต้องใช้ฟังก์ชันการร้องขอ Frame (อัปเดตผืนผ้าใบอย่างต่อเนื่อง) ซึ่งสามารถช่วยเราตั้งค่าช่วงเวลาตัวจับเวลาเป็นอย่างน้อย KmintimerIntervallowResms (เนื่องจากต้องใช้ตัวจับเวลา 16MS ซึ่งกระตุ้นความต้องการของตัวจับเวลาที่มีความแม่นยำสูง) เมื่อเครื่องทดสอบใช้พลังงานช่วงเวลาตัวจับเวลาระบบคือ 1ms ดังนั้นผลการทดสอบจึงมีข้อผิดพลาด± 1ms หากคอมพิวเตอร์ของคุณไม่ได้เปลี่ยนช่วงเวลาตัวจับเวลาระบบและเรียกใช้การทดสอบ #48 ด้านบนสูงสุดอาจถึง 48+16 = 64ms
การใช้การใช้งาน Settimeout ของ Chromium เราสามารถควบคุมข้อผิดพลาดของ SettimeOut (FN, 1) ถึงประมาณ 4ms ในขณะที่ข้อผิดพลาดของ Settimeout (FN, 48) สามารถควบคุมข้อผิดพลาดของ Settimeout (FN, 48) ถึงประมาณ 1ms ดังนั้นเรามีพิมพ์เขียวใหม่ในใจของเราซึ่งทำให้รหัสของเราเป็นแบบนี้:
การคัดลอกรหัสมีดังนี้:
/ * รับการตกแต่งช่วงเวลาสูงสุด */
การตกแต่ง var = getMaxIntervaldeviation (bucketsize); // bucketsSize = 48, ส่วนเบี่ยงเบน = 2;
ฟังก์ชั่น gameloop () {
var now = date.now ();
ถ้า (ก่อนหน้า bucket + bucketsize <= ตอนนี้) {
PreviousBucket = ตอนนี้;
Dologic ();
-
if (ก่อนหน้า bucket + bucketsize - date.now ()> การตกแต่ง) {
// รอ 46ms ความล่าช้าจริงน้อยกว่า 48ms
settimeout (gameloop, bucketsize - การออกแบบ);
} อื่น {
// ยุ่งอยู่กับการรอ ใช้ setimmediate แทน process.nexttick เพราะอดีตไม่ได้บล็อกเหตุการณ์ IO
setimmediate (gameloop);
-
-
รหัสข้างต้นช่วยให้เรารอเวลาที่มีข้อผิดพลาดน้อยกว่า bucket_size (นิยาม bucket_size) แทนการเท่ากับ bucket_size โดยตรง แม้ว่าข้อผิดพลาดสูงสุดจะเกิดขึ้นในความล่าช้า 46ms ตามทฤษฎีข้างต้นช่วงเวลาที่เกิดขึ้นจริงน้อยกว่า 48ms ส่วนที่เหลือของเวลาที่เราใช้วิธีการรอคอยที่วุ่นวายเพื่อให้แน่ใจว่า gameloop ของเราดำเนินการภายใต้ช่วงเวลาที่มีความแม่นยำเพียงพอ
ในขณะที่เราแก้ไขปัญหาในระดับหนึ่งด้วยโครเมียม แต่เห็นได้ชัดว่าไม่สง่างามพอ
จำคำขอเริ่มต้นของเราได้หรือไม่? รหัสฝั่งเซิร์ฟเวอร์ของเราควรทำงานโดยตรงบนคอมพิวเตอร์ด้วยสภาพแวดล้อม node.js โดยไม่ต้องใช้ไคลเอนต์ Node-Webkit หากคุณเรียกใช้รหัสด้านบนโดยตรงค่าของคำจำกัดความคืออย่างน้อย 16ms ซึ่งหมายความว่าในแต่ละ 48ms แต่ละครั้งเราต้องรอ 16ms อัตราการใช้งาน CPU เพิ่มขึ้น
ความประหลาดใจที่ไม่คาดคิด
มันน่ารำคาญมาก ไม่มีใครสังเกตเห็นข้อผิดพลาดขนาดใหญ่ใน node.js? คำตอบทำให้เรามีความสุขจริงๆ ข้อผิดพลาดนี้ได้รับการแก้ไขในเวอร์ชัน V.0.11.3 นอกจากนี้คุณยังสามารถดูผลลัพธ์ที่แก้ไขได้โดยการดูสาขาหลักของรหัส LIBUV โดยตรง วิธีการเฉพาะคือการเพิ่มการหมดเวลาในเวลาปัจจุบันของลูปหลังจากฟังก์ชั่นการสำรวจความคิดเห็นกำลังรอเสร็จสิ้น ด้วยวิธีนี้แม้ว่า GetTickCount จะไม่ตอบสนองเรายังคงเพิ่มเวลารอคอยนี้หลังจากการสำรวจความคิดเห็นกำลังรออยู่ ดังนั้นตัวจับเวลาสามารถหมดอายุได้อย่างราบรื่น
กล่าวอีกนัยหนึ่งปัญหาที่ทำงานอย่างหนักเป็นเวลานานได้รับการแก้ไขใน v.0.11.3 อย่างไรก็ตามความพยายามของเราไม่ได้ไร้ประโยชน์ เพราะแม้ว่าฟังก์ชั่น getTickCount จะถูกกำจัดฟังก์ชันการสำรวจความคิดเห็นนั้นได้รับผลกระทบจากตัวจับเวลาระบบ ทางออกหนึ่งคือการเขียนปลั๊กอิน node.js เพื่อเปลี่ยนช่วงเวลาของตัวจับเวลาระบบ
อย่างไรก็ตามการตั้งค่าเริ่มต้นสำหรับเกมของเราในครั้งนี้ไม่ได้ปราศจากเซิร์ฟเวอร์ หลังจากไคลเอนต์สร้างห้องมันจะกลายเป็นเซิร์ฟเวอร์ รหัสเซิร์ฟเวอร์สามารถทำงานในสภาพแวดล้อม Node-Webkit ดังนั้นลำดับความสำคัญของปัญหาการจับเวลาบนระบบ Windows ไม่ได้สูงสุด หลังจากการแก้ปัญหาที่เราให้ไว้ข้างต้นผลลัพธ์ก็เพียงพอที่จะทำให้เราพึงพอใจ
ตอนจบ
หลังจากแก้ปัญหาการจับเวลาการใช้งานกรอบของเราจะไม่ถูกขัดขวางอีกต่อไป เราให้การสนับสนุน WebSocket (ในสภาพแวดล้อม HTML5 บริสุทธิ์) และปรับแต่งโปรโตคอลการสื่อสารเพื่อให้ได้การสนับสนุนซ็อกเก็ตประสิทธิภาพที่สูงขึ้น (ในสภาพแวดล้อม Node-Webkit) แน่นอนว่าฟังก์ชั่นของ Spaceroom ค่อนข้างง่ายในตอนแรก แต่เมื่อความต้องการถูกเสนอและเวลาที่เพิ่มขึ้นเราค่อยๆปรับปรุงกรอบนี้
ตัวอย่างเช่นเมื่อเราพบว่าเมื่อเราจำเป็นต้องสร้างตัวเลขสุ่มที่สอดคล้องกันในเกมของเราเราจะเพิ่มฟังก์ชั่นนี้ใน spaceroom ในตอนต้นของเกม Spaceroom จะแจกจ่ายเมล็ดจำนวนสุ่ม ห้องพักของลูกค้ามีวิธีการใช้การสุ่มของ MD5 เพื่อสร้างตัวเลขสุ่มด้วยความช่วยเหลือของเมล็ดจำนวนสุ่ม
ดูเหมือนว่าจะโล่งใจมาก ฉันยังได้เรียนรู้มากมายในกระบวนการเขียนกรอบดังกล่าว หากคุณมีความสนใจใน spaceroom คุณสามารถเข้าร่วมได้ ฉันเชื่อว่า Spaceroom จะใช้หมัดและเท้าในสถานที่อื่น ๆ