การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

อินเทอร์เน็ตมีการเปลี่ยนแปลงไปนานแล้ว หนึ่งในโปรโตคอลหลักของอินเทอร์เน็ต UDP ถูกใช้โดยแอปพลิเคชัน ไม่เพียงแต่เพื่อส่งดาตาแกรมและการออกอากาศเท่านั้น แต่ยังให้การเชื่อมต่อแบบ "เพียร์ทูเพียร์" ระหว่างโหนดเครือข่ายอีกด้วย เนื่องจากการออกแบบที่เรียบง่าย โปรโตคอลนี้จึงมีการใช้งานที่ไม่ได้วางแผนไว้ก่อนหน้านี้หลายครั้ง อย่างไรก็ตาม ข้อบกพร่องของโปรโตคอล เช่น การขาดการรับประกันการส่งมอบ ไม่ได้หายไปไหน บทความนี้จะอธิบายถึงการนำโปรโตคอลการจัดส่งที่รับประกันผ่าน UDP ไปใช้
สารบัญ:การเข้า
ข้อกำหนดโปรโตคอล
ส่วนหัว UDP ที่เชื่อถือได้
หลักการทั่วไปของโปรโตคอล
หมดเวลาและตัวจับเวลาโปรโตคอล
ไดอะแกรมสถานะการส่ง UDP ที่เชื่อถือได้
ลึกลงไปในรหัส ชุดควบคุมการส่งกำลัง
ลึกลงไปในรหัส รัฐ

ลึกลงไปในรหัส การสร้างและการสร้างการเชื่อมต่อ
ลึกลงไปในรหัส ปิดการเชื่อมต่อเมื่อหมดเวลา
ลึกลงไปในรหัส การกู้คืนการถ่ายโอนข้อมูล
UDP API ที่เชื่อถือได้
ข้อสรุป
ลิงค์และบทความที่เป็นประโยชน์

การเข้า

สถาปัตยกรรมดั้งเดิมของอินเทอร์เน็ตใช้พื้นที่แอดเดรสที่เป็นเนื้อเดียวกัน ซึ่งแต่ละโหนดมีที่อยู่ IP สากลและไม่ซ้ำกัน และสามารถสื่อสารโดยตรงกับโหนดอื่นๆ อันที่จริงแล้วอินเทอร์เน็ตมีสถาปัตยกรรมที่แตกต่างกัน - พื้นที่หนึ่งของที่อยู่ IP ทั่วโลกและหลาย ๆ พื้นที่ที่มีที่อยู่ส่วนตัวซ่อนอยู่หลังอุปกรณ์ NATในสถาปัตยกรรมนี้ เฉพาะอุปกรณ์ในพื้นที่ที่อยู่ส่วนกลางเท่านั้นที่สามารถสื่อสารกับทุกคนในเครือข่ายได้อย่างง่ายดาย เนื่องจากมีที่อยู่ IP ที่กำหนดเส้นทางได้ทั่วโลกไม่ซ้ำกัน โหนดบนเครือข่ายส่วนตัวสามารถเชื่อมต่อกับโหนดอื่นๆ บนเครือข่ายเดียวกัน และยังสามารถเชื่อมต่อกับโหนดอื่นๆ ที่รู้จักกันดีในพื้นที่ที่อยู่ส่วนกลาง การโต้ตอบนี้เกิดขึ้นได้อย่างมากเนื่องจากกลไกการแปลที่อยู่เครือข่าย อุปกรณ์ NAT เช่น เราเตอร์ Wi-Fi สร้างรายการตารางการแปลพิเศษสำหรับการเชื่อมต่อขาออก และแก้ไขที่อยู่ IP และหมายเลขพอร์ตในแพ็คเก็ต สิ่งนี้ทำให้การเชื่อมต่อขาออกจากเครือข่ายส่วนตัวไปยังโฮสต์ในพื้นที่ที่อยู่ส่วนกลาง แต่ในขณะเดียวกัน อุปกรณ์ NAT มักจะบล็อกทราฟฟิกขาเข้าทั้งหมด เว้นแต่จะมีการตั้งค่ากฎแยกต่างหากสำหรับการเชื่อมต่อขาเข้า

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

วิธีที่มีประสิทธิภาพที่สุดวิธีหนึ่งในการสร้างการเชื่อมต่อแบบเพียร์ทูเพียร์ระหว่างอุปกรณ์บนเครือข่ายส่วนตัวที่แตกต่างกันเรียกว่าการเจาะรู เทคนิคนี้มักใช้กับแอปพลิเคชันที่ใช้โปรโตคอล UDP

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

ในกรณีนี้ เพื่อให้แน่ใจว่ามีการส่งมอบแพ็กเก็ตที่รับประกัน จำเป็นต้องใช้โปรโตคอลชั้นแอปพลิเคชันที่มีฟังก์ชันการทำงานที่จำเป็นและทำงานผ่าน UDP

ฉันต้องการทราบทันทีว่ามีเทคนิคการเจาะรู TCP สำหรับสร้างการเชื่อมต่อ TCP ระหว่างโหนดในเครือข่ายส่วนตัวที่แตกต่างกัน แต่เนื่องจากอุปกรณ์ NAT จำนวนมากขาดการสนับสนุน จึงมักไม่ถือว่าเป็นวิธีหลักในการเชื่อมต่อ โหนดดังกล่าว

สำหรับส่วนที่เหลือของบทความนี้ เราจะมุ่งเน้นที่การดำเนินการตามโปรโตคอลการจัดส่งที่รับประกันเท่านั้น การใช้เทคนิคการเจาะรู UDP จะอธิบายในบทความต่อไปนี้

ข้อกำหนดโปรโตคอล

  1. การส่งแพ็กเก็ตที่เชื่อถือได้ดำเนินการผ่านกลไกการตอบกลับเชิงบวก (สิ่งที่เรียกว่าการรับทราบเชิงบวก)
  2. ความจำเป็นในการถ่ายโอนข้อมูลขนาดใหญ่อย่างมีประสิทธิภาพ เช่น โปรโตคอลต้องหลีกเลี่ยงการส่งต่อแพ็กเก็ตโดยไม่จำเป็น
  3. ควรเป็นไปได้ที่จะยกเลิกกลไกการยืนยันการส่งมอบ (ความสามารถในการทำงานเป็นโปรโตคอล UDP ที่ "บริสุทธิ์")
  4. ความสามารถในการใช้โหมดคำสั่งพร้อมการยืนยันแต่ละข้อความ
  5. หน่วยพื้นฐานของการถ่ายโอนข้อมูลผ่านโปรโตคอลต้องเป็นข้อความ

ข้อกำหนดเหล่านี้ส่วนใหญ่สอดคล้องกับข้อกำหนดโปรโตคอลข้อมูลที่เชื่อถือได้ซึ่งอธิบายไว้ใน rfc908 и rfc1151และฉันใช้มาตรฐานเหล่านั้นเมื่อพัฒนาโปรโตคอลนี้

เพื่อให้เข้าใจข้อกำหนดเหล่านี้ มาดูที่ระยะเวลาของการถ่ายโอนข้อมูลระหว่างโหนดเครือข่ายสองโหนดโดยใช้โปรโตคอล TCP และ UDP ในทั้งสองกรณีเราจะสูญเสียหนึ่งแพ็คเก็ต
การถ่ายโอนข้อมูลที่ไม่โต้ตอบผ่าน TCP:การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

ดังที่คุณเห็นจากแผนภาพ ในกรณีที่แพ็กเก็ตสูญหาย TCP จะตรวจจับแพ็กเก็ตที่สูญหายและรายงานไปยังผู้ส่งโดยขอหมายเลขของเซกเมนต์ที่สูญหาย
การถ่ายโอนข้อมูลผ่านโปรโตคอล UDP:การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

UDP ไม่มีขั้นตอนการตรวจจับการสูญเสียใดๆ การควบคุมข้อผิดพลาดในการส่งในโปรโตคอล UDP เป็นความรับผิดชอบของแอปพลิเคชันทั้งหมด

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

นอกจากนี้ เพื่อปรับปรุงประสิทธิภาพ (เช่น ส่งมากกว่าหนึ่งเซ็กเมนต์โดยไม่ได้รับการตอบรับ) โปรโตคอล TCP ใช้หน้าต่างการส่งข้อมูลที่เรียกว่า - จำนวนไบต์ของข้อมูลที่ผู้ส่งเซ็กเมนต์คาดว่าจะได้รับ

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับโปรโตคอล TCP โปรดดูที่ rfc793, จาก UDP ถึง rfc768ในความเป็นจริงพวกเขากำหนดไว้ที่ไหน

จากข้างต้น เป็นที่ชัดเจนว่าเพื่อสร้างโปรโตคอลการส่งข้อความที่เชื่อถือได้ผ่าน UDP (ต่อไปนี้จะเรียกว่า UDP ที่เชื่อถือได้) จำเป็นต้องใช้กลไกการถ่ายโอนข้อมูลที่คล้ายกับ TCP คือ:

  • บันทึกสถานะการเชื่อมต่อ
  • ใช้การกำหนดหมายเลขส่วน
  • ใช้แพ็คเกจการยืนยันพิเศษ
  • ใช้กลไกหน้าต่างที่เรียบง่ายเพื่อเพิ่มปริมาณงานของโปรโตคอล

นอกจากนี้ คุณต้อง:

  • ส่งสัญญาณการเริ่มต้นของข้อความ เพื่อจัดสรรทรัพยากรสำหรับการเชื่อมต่อ
  • ส่งสัญญาณการสิ้นสุดของข้อความ เพื่อส่งข้อความที่ได้รับไปยังแอ็พพลิเคชันอัปสตรีมและรีลีสโปรโตคอลทรัพยากร
  • อนุญาตให้โปรโตคอลเฉพาะการเชื่อมต่อปิดใช้งานกลไกการยืนยันการส่งเพื่อทำหน้าที่เป็น UDP "บริสุทธิ์"

ส่วนหัว UDP ที่เชื่อถือได้

จำได้ว่าดาตาแกรม UDP ถูกห่อหุ้มในดาตาแกรม IP แพ็กเก็ต UDP ที่เชื่อถือได้ถูก "ห่อ" ลงในดาตาแกรม UDP อย่างเหมาะสม
การห่อหุ้มส่วนหัว UDP ที่เชื่อถือได้:การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

โครงสร้างของส่วนหัว UDP ที่เชื่อถือได้นั้นค่อนข้างง่าย:

การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

  • แฟล็ก - แฟล็กควบคุมแพ็กเกจ
  • MessageType - ประเภทข้อความที่ใช้โดยแอปพลิเคชันอัปสตรีมเพื่อสมัครรับข้อความเฉพาะ
  • TransmissionId - หมายเลขของการส่งพร้อมกับที่อยู่และพอร์ตของผู้รับ ระบุการเชื่อมต่อโดยไม่ซ้ำกัน
  • PacketNumber - หมายเลขแพ็กเก็ต
  • ตัวเลือก - ตัวเลือกโปรโตคอลเพิ่มเติม ในกรณีของแพ็คเก็ตแรกใช้เพื่อระบุขนาดของข้อความ

แฟล็กมีดังนี้:

  • FirstPacket - แพ็กเก็ตแรกของข้อความ
  • NoAsk - ข้อความไม่จำเป็นต้องเปิดใช้งานกลไกตอบรับ
  • LastPacket - แพ็กเก็ตสุดท้ายของข้อความ
  • RequestForPacket - แพ็กเก็ตยืนยันหรือร้องขอแพ็กเก็ตที่สูญหาย

หลักการทั่วไปของโปรโตคอล

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

กลไกที่คล้ายกันนี้ใช้เพื่อยุติการเชื่อมต่อ ค่าสถานะ LastPacket ถูกตั้งค่าบนแพ็กเก็ตสุดท้ายของข้อความ ในแพ็กเก็ตตอบกลับ มีการระบุหมายเลขของแพ็กเก็ตสุดท้าย + 1 ซึ่งสำหรับฝั่งรับหมายถึงการส่งข้อความสำเร็จ
แผนภาพการสร้างและสิ้นสุดการเชื่อมต่อ:การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

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

ด้านรับจะรับแพ็กเก็ต แต่ละแพ็กเก็ตจะถูกตรวจสอบเพื่อดูว่าอยู่ในหน้าต่างการส่งข้อมูลหรือไม่ แพ็คเก็ตและสำเนาที่ไม่อยู่ในหน้าต่างจะถูกกรองออก เพราะ หากขนาดของหน้าต่างคงที่และเท่ากันสำหรับผู้รับและผู้ส่ง ในกรณีของบล็อกของแพ็กเก็ตที่ถูกจัดส่งโดยไม่สูญเสีย หน้าต่างจะถูกเลื่อนเพื่อรับแพ็กเก็ตของบล็อกถัดไปของข้อมูล และการยืนยันการส่งคือ ส่งแล้ว. หากหน้าต่างไม่เต็มภายในระยะเวลาที่กำหนดโดยตัวจับเวลาการทำงาน การตรวจสอบจะเริ่มขึ้นว่าแพ็กเก็ตใดที่ยังไม่ได้จัดส่งและคำขอจัดส่งใหม่จะถูกส่งไป
แผนภาพการส่งสัญญาณซ้ำ:การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

หมดเวลาและตัวจับเวลาโปรโตคอล

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

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

ไดอะแกรมสถานะการส่ง UDP ที่เชื่อถือได้

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

การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

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

การส่งแพ็กเก็ตแรก – สถานะเริ่มต้นที่การเชื่อมต่อขาออกคือเมื่อข้อความถูกส่ง

ในสถานะนี้ แพ็คเก็ตแรกสำหรับข้อความปกติจะถูกส่ง สำหรับข้อความที่ไม่มีการยืนยันการส่ง นี่เป็นสถานะเดียวที่มีการส่งข้อความทั้งหมด

รอบการส่ง – สถานะพื้นสำหรับการส่งแพ็กเก็ตข้อความ

การเปลี่ยนผ่านจากสถานะ การส่งแพ็กเก็ตแรก ดำเนินการหลังจากแพ็กเก็ตแรกของข้อความถูกส่งไปแล้ว อยู่ในสถานะนี้ที่รับทราบและร้องขอการส่งสัญญาณซ้ำทั้งหมด ทางออกเป็นไปได้ในสองกรณี - ในกรณีที่ส่งข้อความสำเร็จหรือหมดเวลา

FirstPacketReceeded – สถานะเริ่มต้นสำหรับผู้รับข้อความ

ตรวจสอบความถูกต้องของการเริ่มต้นของการส่งข้อมูล สร้างโครงสร้างที่จำเป็น และส่งการรับทราบการรับแพ็กเก็ตแรก

สำหรับข้อความที่ประกอบด้วยแพ็กเก็ตเดียวและถูกส่งโดยไม่ใช้หลักฐานการส่ง นี่เป็นเพียงสถานะเดียวเท่านั้น หลังจากประมวลผลข้อความดังกล่าว การเชื่อมต่อจะปิดลง

การรวบรวม – สถานะพื้นฐานสำหรับการรับแพ็กเก็ตข้อความ

มันเขียนแพ็กเก็ตไปยังที่เก็บข้อมูลชั่วคราว ตรวจสอบการสูญหายของแพ็กเก็ต ส่งการรับทราบสำหรับการจัดส่งบล็อกของแพ็กเก็ตและข้อความทั้งหมด และส่งคำร้องขอให้จัดส่งแพ็กเก็ตที่สูญหายอีกครั้ง ในกรณีที่ได้รับข้อความทั้งหมดสำเร็จ การเชื่อมต่อจะเข้าสู่สถานะ เสร็จมิฉะนั้น การหมดเวลาจะสิ้นสุดลง

เสร็จ – ปิดการเชื่อมต่อในกรณีที่ได้รับข้อความทั้งหมดสำเร็จ

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

ลึกลงไปในรหัส ชุดควบคุมการส่งกำลัง

องค์ประกอบสำคัญประการหนึ่งของ UDP ที่เชื่อถือได้คือบล็อกควบคุมการส่ง หน้าที่ของบล็อกนี้คือจัดเก็บการเชื่อมต่อปัจจุบันและองค์ประกอบเสริม แจกจ่ายแพ็กเก็ตขาเข้าไปยังการเชื่อมต่อที่เกี่ยวข้อง จัดหาอินเทอร์เฟซสำหรับการส่งแพ็กเก็ตไปยังการเชื่อมต่อ และนำโปรโตคอล API ไปใช้ บล็อกควบคุมการส่งรับแพ็กเก็ตจากเลเยอร์ UDP และส่งต่อไปยังเครื่องสถานะเพื่อประมวลผล ในการรับแพ็กเก็ต จะใช้เซิร์ฟเวอร์ UDP แบบอะซิงโครนัส
สมาชิกบางคนของคลาส ReliableUdpConnectionControlBlock:

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // массив байт для указанного ключа. Используется для сборки входящих сообщений    
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
  // массив байт для указанного ключа. Используется для отправки исходящих сообщений.
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
  // connection record для указанного ключа.
  private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
  // список подписчиков на сообщения.
  private readonly List<ReliableUdpSubscribeObject> m_subscribers;    
  // локальный сокет    
  private Socket m_socketIn;
  // порт для входящих сообщений
  private int m_port;
  // локальный IP адрес
  private IPAddress m_ipAddress;    
  // локальная конечная точка    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // коллекция предварительно инициализированных
  // состояний конечного автомата
  public StatesCollection States { get; private set; }
  // генератор случайных чисел. Используется для создания TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

การใช้งานเซิร์ฟเวอร์ UDP แบบอะซิงโครนัส:

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // создаем новый буфер, для каждого socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // передаем буфер в качестве параметра для асинхронного метода
  this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}   

private void EndReceive(IAsyncResult ar)
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
  //пакет получен, готовы принимать следующий        
  Receive();
  // т.к. простейший способ решить вопрос с буфером - получить ссылку на него 
  // из IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // получаем заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

สำหรับการถ่ายโอนข้อความแต่ละครั้ง จะมีการสร้างโครงสร้างที่มีข้อมูลเกี่ยวกับการเชื่อมต่อ โครงสร้างดังกล่าวเรียกว่า บันทึกการเชื่อมต่อ.
สมาชิกบางคนของคลาส ReliableUdpConnectionRecord:

internal class ReliableUdpConnectionRecord : IDisposable
{    
  // массив байт с сообщением    
  public byte[] IncomingStream { get; set; }
  // ссылка на состояние конечного автомата    
  public ReliableUdpState State { get; set; }    
  // пара, однозначно определяющая connection record
  // в блоке управления передачей     
  public Tuple<EndPoint, Int32> Key { get; private set;}
  // нижняя граница приемного окна    
  public int WindowLowerBound;
  // размер окна передачи
  public readonly int WindowSize;     
  // номер пакета для отправки
  public int SndNext;
  // количество пакетов для отправки
  public int NumberOfPackets;
  // номер передачи (именно он и есть вторая часть Tuple)
  // для каждого сообщения свой	
  public readonly Int32 TransmissionId;
  // удаленный IP endpoint – собственно получатель сообщения
  public readonly IPEndPoint RemoteClient;
  // размер пакета, во избежание фрагментации на IP уровне
  // не должен превышать MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // блок управления передачей
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // инкапсулирует результаты асинхронной операции для BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // не отправлять пакеты подтверждения
  public bool IsNoAnswerNeeded;
  // последний корректно полученный пакет (всегда устанавливается в наибольший номер)
  public int RcvCurrent;
  // массив с номерами потерянных пакетов
  public int[] LostPackets { get; private set; }
  // пришел ли последний пакет. Используется как bool.
  public int IsLastPacketReceived = 0;
  //...
}

ลึกลงไปในรหัส รัฐ

รัฐใช้เครื่องสถานะของโปรโตคอล UDP ที่เชื่อถือได้ซึ่งการประมวลผลหลักของแพ็คเก็ตเกิดขึ้น คลาสนามธรรม ReliableUdpState จัดเตรียมอินเทอร์เฟซสำหรับสถานะ:

การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

ตรรกะทั้งหมดของโปรโตคอลถูกนำไปใช้โดยคลาสที่แสดงด้านบน ร่วมกับคลาสเสริมที่ให้เมธอดแบบสแตติก เช่น การสร้างส่วนหัว ReliableUdp จากเรกคอร์ดการเชื่อมต่อ

ต่อไปเราจะพิจารณารายละเอียดเกี่ยวกับการใช้วิธีอินเทอร์เฟซที่กำหนดอัลกอริทึมพื้นฐานของโปรโตคอล

วิธี DisposeByTimeout

เมธอด DisposeByTimeout รับผิดชอบการปล่อยทรัพยากรการเชื่อมต่อหลังจากหมดเวลาและส่งสัญญาณการส่งข้อความที่สำเร็จ/ไม่สำเร็จ
TrustedUdpState.DisposeByTimeout:

protected virtual void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;      
  if (record.AsyncResult != null)
  {
    connectionRecord.AsyncResult.SetAsCompleted(false);
  }
  connectionRecord.Dispose();
}

มันถูกแทนที่ในรัฐเท่านั้น เสร็จ.
เสร็จสิ้น DisposeByTimeout:

protected override void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  // сообщаем об успешном получении сообщения
  SetAsCompleted(connectionRecord);        
}

วิธี ProcessPackets

เมธอด ProcessPackets มีหน้าที่รับผิดชอบในการประมวลผลเพิ่มเติมของแพ็คเกจหรือแพ็คเกจ โทรโดยตรงหรือผ่านตัวตั้งเวลารอแพ็กเก็ต

สามารถ การรวบรวม เมธอดถูกแทนที่และมีหน้าที่ตรวจสอบแพ็กเก็ตที่สูญหายและเปลี่ยนสถานะ เสร็จในกรณีได้รับแพ็คเก็ตสุดท้ายและผ่านการตรวจสอบเรียบร้อยแล้ว
การประกอบ ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // если после двух попыток срабатываний WaitForPacketTimer 
    // не удалось получить пакеты - запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // успешная проверка 
  {
    // высылаем подтверждение о получении блока данных
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // вместо моментальной реализации ресурсов
    // запускаем таймер, на случай, если
    // если последний ack не дойдет до отправителя и он запросит его снова.
    // по срабатыванию таймера - реализуем ресурсы
    // в состоянии Completed метод таймера переопределен
    StartCloseWaitTimer(connectionRecord);
  }
  // это случай, когда ack на блок пакетов был потерян
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

สามารถ รอบการส่ง วิธีนี้เรียกว่าตัวจับเวลาเท่านั้น และมีหน้าที่ส่งข้อความสุดท้ายอีกครั้ง รวมทั้งเปิดใช้ตัวจับเวลาปิดการเชื่อมต่อ
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // отправляем повторно последний пакет 
  // ( в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

สามารถ เสร็จ วิธีการหยุดตัวจับเวลาการทำงานและส่งข้อความไปยังสมาชิก
เสร็จสิ้นกระบวนการแพ็คเก็ต:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // собираем сообщение и передаем его подписчикам
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

วิธีการรับแพ็คเก็ต

สามารถ FirstPacketReceeded งานหลักของเมธอดคือตรวจสอบว่าแพ็กเก็ตข้อความแรกมาถึงอินเทอร์เฟซจริงหรือไม่ และรวบรวมข้อความที่ประกอบด้วยแพ็กเก็ตเดียว
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
      header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
    if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
    {
      // отправляем пакет подтверждение          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // если не требуется механизм подтверждение
  // запускаем таймер который высвободит все структуры         
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
  else
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

สามารถ รอบการส่ง วิธีการนี้ถูกแทนที่เพื่อยอมรับการรับทราบการส่งมอบและคำขอการส่งข้อมูลซ้ำ
SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

สามารถ การรวบรวม ในวิธีการรับแพ็กเก็ต งานหลักของการรวบรวมข้อความจากแพ็กเก็ตขาเข้าจะเกิดขึ้น
การประกอบ รับแพ็คเก็ต:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // обработка пакетов с отключенным механизмом подтверждения доставки
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // сбрасываем таймер
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // записываем данные
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // если получили пакет с последним флагом - делаем завершаем          
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // расчет конечной границы окна
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // отбрасываем не попадающие в окно пакеты
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound))
    return;
  // отбрасываем дубликаты
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // записываем данные 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // если пришел последний пакет
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // если последний пакет уже имеется        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // проверяем пакеты          
    ProcessPackets(connectionRecord);
  }
}

สามารถ เสร็จ งานเดียวของวิธีนี้คือการส่งการตอบรับอีกครั้งของการส่งข้อความที่สำเร็จ
เสร็จเรียบร้อย รับแพ็คเก็ต:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // повторная отправка последнего пакета в связи с тем,
  // что последний ack не дошел до отправителя
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

วิธีส่งแพ็คเก็ต

สามารถ การส่งแพ็กเก็ตแรก วิธีนี้จะส่งข้อมูลชุดแรก หรือหากข้อความไม่ต้องการการยืนยันการส่ง ก็จะส่งทั้งข้อความ
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // если подтверждения не требуется - отправляем все пакеты
  // и высвобождаем ресурсы
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Здесь происходит отправка As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

สามารถ รอบการส่ง ในวิธีนี้ บล็อกของแพ็กเก็ตจะถูกส่ง
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов      
  for (connectionRecord.PacketCounter = 0;
        connectionRecord.PacketCounter < connectionRecord.WindowSize &&
        connectionRecord.SndNext < connectionRecord.NumberOfPackets;
        connectionRecord.PacketCounter++)
  {
    ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
    connectionRecord.SndNext++;
  }
  // на случай большого окна передачи, перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

ลึกลงไปในรหัส การสร้างและการสร้างการเชื่อมต่อ

ตอนนี้เราได้เห็นสถานะพื้นฐานและเมธอดที่ใช้ในการจัดการกับสถานะต่างๆ แล้ว เรามาแจกแจงตัวอย่างเล็กๆ น้อยๆ ของวิธีการทำงานของโปรโตคอลในรายละเอียดเพิ่มเติมอีกเล็กน้อย
แผนภาพการรับส่งข้อมูลภายใต้สภาวะปกติ:การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

พิจารณารายละเอียดการสร้าง บันทึกการเชื่อมต่อ เพื่อเชื่อมต่อและส่งแพ็คเก็ตแรก การถ่ายโอนจะเริ่มต้นโดยแอปพลิเคชันที่เรียก API ส่งข้อความเสมอ ถัดไป เรียกใช้เมธอด StartTransmission ของบล็อกควบคุมการส่งข้อมูล ซึ่งจะเริ่มการส่งข้อมูลสำหรับข้อความใหม่
การสร้างการเชื่อมต่อขาออก:

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException( "", "You must use constructor with parameters or start listener before sending message" );
    }
    // запускаем обработку входящих пакетов
    StartListener(LocalEndpoint);
  }
  // создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // создаем случайный номер transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // создаем новую запись для соединения и проверяем, 
  // существует ли уже такой номер в наших словарях
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // если существует – то повторно генерируем случайный номер 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // если снова не удалось – генерируем исключение
      throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
  }
  // запустили состояние в обработку         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

การส่งแพ็กเก็ตแรก (สถานะ FirstPacketSending):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  // переходим в состояние SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

หลังจากส่งแพ็กเก็ตแรก ผู้ส่งจะเข้าสู่สถานะ รอบการส่ง – รอการยืนยันการส่งพัสดุ
ด้านรับโดยใช้เมธอด EndReceive รับแพ็กเก็ตที่ส่ง สร้างใหม่ บันทึกการเชื่อมต่อ และส่งแพ็กเก็ตนี้พร้อมส่วนหัวที่แยกวิเคราะห์ล่วงหน้าไปยังวิธีการรับแพ็คเก็ตของสถานะสำหรับการประมวลผล FirstPacketReceeded
สร้างการเชื่อมต่อด้านรับ:

private void EndReceive(IAsyncResult ar)
{
  // ...
  // пакет получен
  // парсим заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

รับแพ็คเก็ตแรกและส่งการตอบรับ (สถานะ FirstPacketReceived):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // ...
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  // инициализируем массив для хранения частей сообщения
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // записываем данные пакет в массив
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*если не требуется механизм подтверждение*/)
  // ...
  else
  {
    // отправляем подтверждение
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

ลึกลงไปในรหัส ปิดการเชื่อมต่อเมื่อหมดเวลา

การจัดการการหมดเวลาเป็นส่วนสำคัญของ UDP ที่เชื่อถือได้ พิจารณาตัวอย่างที่โหนดกลางล้มเหลวและการส่งข้อมูลในทั้งสองทิศทางกลายเป็นไปไม่ได้
แผนภาพสำหรับการปิดการเชื่อมต่อโดยหมดเวลา:การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

ดังที่เห็นได้จากแผนภาพ ตัวจับเวลาการทำงานของผู้ส่งจะเริ่มทำงานทันทีหลังจากส่งกลุ่มของแพ็กเก็ต สิ่งนี้เกิดขึ้นในเมธอด SendPacket ของรัฐ รอบการส่ง.
การเปิดใช้งานตัวจับเวลาการทำงาน (สถานะ SendingCycle):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов   
  // ...   
  // перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
}

ช่วงเวลาถูกตั้งค่าเมื่อสร้างการเชื่อมต่อ ShortTimerPeriod เริ่มต้นคือ 5 วินาที ในตัวอย่าง ตั้งค่าไว้ที่ 1,5 วินาที

สำหรับการเชื่อมต่อขาเข้า ตัวจับเวลาเริ่มต้นหลังจากได้รับแพ็กเก็ตข้อมูลขาเข้าล่าสุด ซึ่งจะเกิดขึ้นในเมธอด การรวบรวม
การเปิดใช้งานตัวจับเวลาการทำงาน (สถานะการประกอบ):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // перезапускаем таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

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

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*проверка на потерянные пакеты */)
  {
    // отправляем запросы на повторную доставку
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
    connectionRecord.TimerSecondTry = true;
    return;
    }
  // если после двух попыток срабатываний WaitForPacketTimer 
  // не удалось получить пакеты - запускаем таймер завершения соединения
  StartCloseWaitTimer(connectionRecord);
  }
  else if (/*пришел последний пакет и успешная проверка */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // если ack на блок пакетов был потерян
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // повторно отсылаем ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

ตัวแปร TimerSecondTry ถูกตั้งค่าเป็น จริง. ตัวแปรนี้มีหน้าที่ในการรีสตาร์ทตัวจับเวลาการทำงาน

ในด้านของผู้ส่ง ตัวจับเวลาการทำงานจะถูกกระตุ้นเช่นกัน และส่งแพ็กเก็ตที่ส่งล่าสุดอีกครั้ง
การเปิดใช้งานตัวจับเวลาปิดการเชื่อมต่อ (สถานะ SendingCycle):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // отправляем повторно последний пакет 
  // ...        
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

หลังจากนั้น ตัวตั้งเวลาปิดการเชื่อมต่อจะเริ่มขึ้นในการเชื่อมต่อขาออก
TrustedUdpState.StartCloseWaitTimer:

protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
  else
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}

ระยะหมดเวลาของตัวจับเวลาปิดการเชื่อมต่อคือ 30 วินาทีตามค่าเริ่มต้น

หลังจากนั้นไม่นาน ตัวจับเวลาการทำงานที่ฝั่งผู้รับจะเริ่มทำงานอีกครั้ง คำขอจะถูกส่งอีกครั้ง หลังจากนั้นตัวจับเวลาการปิดการเชื่อมต่อจะเริ่มทำงานสำหรับการเชื่อมต่อขาเข้า

เมื่อตัวจับเวลาปิดทำงาน ทรัพยากรทั้งหมดของบันทึกการเชื่อมต่อทั้งสองจะถูกปล่อย ผู้ส่งรายงานความล้มเหลวในการส่งไปยังแอ็พพลิเคชันอัปสตรีม (ดู UDP API ที่เชื่อถือได้).
รีลีสทรัพยากรบันทึกการเชื่อมต่อ:

public void Dispose()
{
  try
  {
    System.Threading.Monitor.Enter(this.LockerReceive);
  }
  finally
  {
    Interlocked.Increment(ref this.IsDone);
    if (WaitForPacketsTimer != null)
    {
      WaitForPacketsTimer.Dispose();
    }
    if (CloseWaitTimer != null)
    {
      CloseWaitTimer.Dispose();
    }
    byte[] stream;
    Tcb.IncomingStreams.TryRemove(Key, out stream);
    stream = null;
    Tcb.OutcomingStreams.TryRemove(Key, out stream);
    stream = null;
    System.Threading.Monitor.Exit(this.LockerReceive);
  }
}

ลึกลงไปในรหัส การกู้คืนการถ่ายโอนข้อมูล

แผนภาพการกู้คืนการส่งข้อมูลในกรณีที่แพ็กเก็ตสูญหาย:การใช้งานโปรโตคอล Udp ที่เชื่อถือได้สำหรับ .Net

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

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

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

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // сброс таймера закрытия соединения 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

แพ็กเก็ตที่ส่งใหม่ (แพ็กเก็ต #3 ในไดอะแกรม) ได้รับจากการเชื่อมต่อขาเข้า มีการตรวจสอบเพื่อดูว่าหน้าต่างรับข้อมูลเต็มและมีการคืนค่าการรับส่งข้อมูลตามปกติหรือไม่
ตรวจสอบการเข้าชมในหน้าต่างรับ (สถานะการประกอบ):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

UDP API ที่เชื่อถือได้

ในการโต้ตอบกับโปรโตคอลการถ่ายโอนข้อมูล มีคลาส Udp ที่เชื่อถือได้แบบเปิด ซึ่งเป็นตัวห่อหุ้มเหนือบล็อกควบคุมการถ่ายโอน นี่คือสมาชิกที่สำคัญที่สุดของชั้นเรียน:

public sealed class ReliableUdp : IDisposable
{
  // получает локальную конечную точку
  public IPEndPoint LocalEndpoint    
  // создает экземпляр ReliableUdp и запускает
  // прослушивание входящих пакетов на указанном IP адресе
  // и порту. Значение 0 для порта означает использование
  // динамически выделенного порта
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // подписка на получение входящих сообщений
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // отписка от получения сообщений
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // асинхронно отправить сообщение 
  // Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
  public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // начать асинхронную отправку сообщения
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // получить результат асинхронной отправки
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // очистить ресурсы
  public void Dispose()    
}

ข้อความจะได้รับจากการสมัครรับข้อมูล ลายเซ็นผู้รับมอบสิทธิ์สำหรับวิธีการโทรกลับ:

public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );

ข้อความ:

public class ReliableUdpMessage
{
  // тип сообщения, простое перечисление
  public ReliableUdpMessageTypes Type { get; private set; }
  // данные сообщения
  public byte[] Body { get; private set; }
  // если установлено в true – механизм подтверждения доставки будет отключен
  // для передачи конкретного сообщения
  public bool NoAsk { get; private set; }
}

หากต้องการสมัครรับข้อความประเภทใดประเภทหนึ่งและ/หรือผู้ส่งเฉพาะ จะใช้พารามิเตอร์ทางเลือกสองตัว: ReliableUdpMessageTypes messageType และ IPEndPoint ipEndPoint

ประเภทข้อความ:

public enum ReliableUdpMessageTypes : short
{ 
  // Любое
  Any = 0,
  // Запрос к STUN server 
  StunRequest = 1,
  // Ответ от STUN server
  StunResponse = 2,
  // Передача файла
  FileTransfer =3,
  // ...
}

ข้อความถูกส่งแบบอะซิงโครนัส สำหรับสิ่งนี้ โปรโตคอลใช้รูปแบบการเขียนโปรแกรมแบบอะซิงโครนัส:

public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)

ผลลัพธ์ของการส่งข้อความจะเป็นจริง - หากข้อความส่งถึงผู้รับสำเร็จและเท็จ - หากการเชื่อมต่อถูกปิดโดยหมดเวลา:

public bool EndSendMessage(IAsyncResult asyncResult)

ข้อสรุป

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

เวอร์ชันสาธิตของโปรโตคอลการจัดส่งที่เชื่อถือได้นั้นแข็งแกร่งและยืดหยุ่นเพียงพอที่จะตอบสนองความต้องการที่กำหนดไว้ก่อนหน้านี้ แต่ฉันต้องการเพิ่มว่าสามารถปรับปรุงการใช้งานที่อธิบายไว้ได้ ตัวอย่างเช่น หากต้องการเพิ่มปริมาณงานและเปลี่ยนช่วงเวลาของตัวจับเวลาแบบไดนามิก สามารถเพิ่มกลไกต่างๆ เช่น หน้าต่างเลื่อนและ RTT ลงในโปรโตคอลได้ นอกจากนี้ ยังมีประโยชน์ในการใช้กลไกสำหรับกำหนด MTU ระหว่างโหนดการเชื่อมต่อ (แต่เฉพาะเมื่อส่งข้อความขนาดใหญ่เท่านั้น) .

ขอบคุณสำหรับความสนใจของคุณ ฉันหวังว่าจะได้แสดงความคิดเห็นและความคิดเห็นของคุณ

ป.ล. สำหรับผู้ที่สนใจรายละเอียดหรือเพียงแค่ต้องการทดสอบโปรโตคอล ลิงก์ไปยังโครงการบน GitHube:
โครงการ UDP ที่เชื่อถือได้

ลิงค์และบทความที่เป็นประโยชน์

  1. ข้อกำหนดโปรโตคอล TCP: ในภาษาอังกฤษ и нарусском
  2. ข้อกำหนดโปรโตคอล UDP: ในภาษาอังกฤษ и нарусском
  3. การอภิปรายเกี่ยวกับโปรโตคอล RUDP: Draft-ietf-sigtran-reliable-udp-00
  4. โปรโตคอลข้อมูลที่เชื่อถือได้: rfc908 и rfc1151
  5. การดำเนินการยืนยันการส่งมอบผ่าน UDP อย่างง่าย: ควบคุมเครือข่ายทั้งหมดของคุณด้วย .NET และ UDP
  6. บทความที่อธิบายถึงกลไกการผ่านผ่านของ NAT: การสื่อสารแบบ Peer-to-Peer ข้ามตัวแปลที่อยู่เครือข่าย
  7. การนำโมเดลการเขียนโปรแกรมแบบอะซิงโครนัสไปใช้: การใช้โมเดลการเขียนโปรแกรมแบบอะซิงโครนัส CLR и วิธีใช้รูปแบบการออกแบบ IAsyncResult
  8. พอร์ตโมเดลโปรแกรมมิงแบบอะซิงโครนัสไปยังรูปแบบอะซิงโครนัสตามงาน (APM ใน TAP):
    การเขียนโปรแกรมอะซิงโครนัส TPL และ .NET แบบดั้งเดิม
    การทำงานร่วมกันกับรูปแบบและประเภทอะซิงโครนัสอื่นๆ

ปรับปรุง: ขอบคุณ มายอรอฟ и ซิดริสทิจ สำหรับแนวคิดในการเพิ่มงานในอินเทอร์เฟซ ไม่มีการละเมิดความเข้ากันได้ของไลบรารีกับระบบปฏิบัติการเก่าเพราะ เฟรมเวิร์กที่ 4 รองรับทั้งเซิร์ฟเวอร์ XP และ 2003

ที่มา: will.com

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