Υλοποίηση του πρωτοκόλλου Reliable Udp για .Net

Το Διαδίκτυο έχει αλλάξει εδώ και πολύ καιρό. Ένα από τα κύρια πρωτόκολλα του Διαδικτύου - το UDP χρησιμοποιείται από εφαρμογές όχι μόνο για την παράδοση δεδομένων και εκπομπών, αλλά και για την παροχή "peer-to-peer" συνδέσεων μεταξύ κόμβων δικτύου. Λόγω του απλού σχεδιασμού του, αυτό το πρωτόκολλο έχει πολλές μη προγραμματισμένες προηγουμένως χρήσεις, ωστόσο, οι ελλείψεις του πρωτοκόλλου, όπως η έλλειψη εγγυημένης παράδοσης, δεν έχουν εξαφανιστεί πουθενά. Αυτό το άρθρο περιγράφει την υλοποίηση του πρωτοκόλλου εγγυημένης παράδοσης μέσω UDP.
Περιεχόμενα:Είσοδος
Απαιτήσεις Πρωτοκόλλου
Αξιόπιστη κεφαλίδα UDP
Γενικές αρχές του πρωτοκόλλου
Χρονικά όρια και χρονόμετρα πρωτοκόλλου
Αξιόπιστο διάγραμμα κατάστασης μετάδοσης UDP
Πιο βαθιά μέσα στον κώδικα. μονάδα ελέγχου μετάδοσης
Πιο βαθιά μέσα στον κώδικα. πολιτείες

Πιο βαθιά μέσα στον κώδικα. Δημιουργία και δημιουργία συνδέσεων
Πιο βαθιά μέσα στον κώδικα. Κλείσιμο της σύνδεσης στο χρονικό όριο
Πιο βαθιά μέσα στον κώδικα. Επαναφορά μεταφοράς δεδομένων
Αξιόπιστο UDP API
Συμπέρασμα
Χρήσιμοι σύνδεσμοι και άρθρα

Είσοδος

Η αρχική αρχιτεκτονική του Διαδικτύου υιοθέτησε έναν ομοιογενή χώρο διευθύνσεων στον οποίο κάθε κόμβος είχε μια παγκόσμια και μοναδική διεύθυνση IP και μπορούσε να επικοινωνήσει απευθείας με άλλους κόμβους. Τώρα το Διαδίκτυο, στην πραγματικότητα, έχει μια διαφορετική αρχιτεκτονική - μια περιοχή παγκόσμιων διευθύνσεων IP και πολλές περιοχές με ιδιωτικές διευθύνσεις κρυμμένες πίσω από συσκευές NAT.Σε αυτήν την αρχιτεκτονική, μόνο οι συσκευές στον παγκόσμιο χώρο διευθύνσεων μπορούν να επικοινωνούν εύκολα με οποιονδήποτε στο δίκτυο, επειδή έχουν μια μοναδική, καθολικά δρομολογήσιμη διεύθυνση IP. Ένας κόμβος σε ένα ιδιωτικό δίκτυο μπορεί να συνδεθεί με άλλους κόμβους στο ίδιο δίκτυο και μπορεί επίσης να συνδεθεί με άλλους γνωστούς κόμβους στον παγκόσμιο χώρο διευθύνσεων. Αυτή η αλληλεπίδραση επιτυγχάνεται σε μεγάλο βαθμό λόγω του μηχανισμού μετάφρασης διευθύνσεων δικτύου. Οι συσκευές NAT, όπως οι δρομολογητές Wi-Fi, δημιουργούν ειδικές καταχωρήσεις πίνακα μετάφρασης για εξερχόμενες συνδέσεις και τροποποιούν τις διευθύνσεις IP και τους αριθμούς θυρών σε πακέτα. Αυτό επιτρέπει εξερχόμενες συνδέσεις από το ιδιωτικό δίκτυο σε κεντρικούς υπολογιστές στον παγκόσμιο χώρο διευθύνσεων. Ταυτόχρονα όμως, οι συσκευές NAT συνήθως μπλοκάρουν όλη την εισερχόμενη κίνηση εκτός και αν οριστούν ξεχωριστοί κανόνες για τις εισερχόμενες συνδέσεις.

Αυτή η αρχιτεκτονική του Διαδικτύου είναι αρκετά σωστή για την επικοινωνία πελάτη-διακομιστή, όπου οι πελάτες μπορούν να βρίσκονται σε ιδιωτικά δίκτυα και οι διακομιστές έχουν μια καθολική διεύθυνση. Δημιουργεί όμως δυσκολίες για την άμεση σύνδεση δύο κόμβων μεταξύ τους διάφορος ιδιωτικών δικτύων. Η άμεση σύνδεση μεταξύ δύο κόμβων είναι σημαντική για εφαρμογές peer-to-peer όπως η μετάδοση φωνής (Skype), η απόκτηση απομακρυσμένης πρόσβασης σε έναν υπολογιστή (TeamViewer) ή τα διαδικτυακά παιχνίδια.

Μία από τις πιο αποτελεσματικές μεθόδους για τη δημιουργία μιας peer-to-peer σύνδεσης μεταξύ συσκευών σε διαφορετικά ιδιωτικά δίκτυα ονομάζεται διάτρηση οπών. Αυτή η τεχνική χρησιμοποιείται πιο συχνά με εφαρμογές που βασίζονται στο πρωτόκολλο UDP.

Αλλά εάν η εφαρμογή σας χρειάζεται εγγυημένη παράδοση δεδομένων, για παράδειγμα, μεταφέρετε αρχεία μεταξύ υπολογιστών, τότε η χρήση του UDP θα έχει πολλές δυσκολίες λόγω του γεγονότος ότι το UDP δεν είναι εγγυημένο πρωτόκολλο παράδοσης και δεν παρέχει παράδοση πακέτων με τη σειρά, σε αντίθεση με το TCP πρωτόκολλο.

Σε αυτήν την περίπτωση, για να διασφαλιστεί η εγγυημένη παράδοση πακέτων, απαιτείται η εφαρμογή ενός πρωτοκόλλου επιπέδου εφαρμογής που παρέχει την απαραίτητη λειτουργικότητα και λειτουργεί μέσω UDP.

Θέλω να σημειώσω αμέσως ότι υπάρχει μια τεχνική διάτρησης οπών TCP για τη δημιουργία συνδέσεων TCP μεταξύ κόμβων σε διαφορετικά ιδιωτικά δίκτυα, αλλά λόγω της έλλειψης υποστήριξης για αυτήν από πολλές συσκευές NAT, συνήθως δεν θεωρείται ως ο κύριος τρόπος σύνδεσης τέτοιους κόμβους.

Για το υπόλοιπο αυτού του άρθρου, θα επικεντρωθώ μόνο στην εφαρμογή του πρωτοκόλλου εγγυημένης παράδοσης. Η εφαρμογή της τεχνικής διάτρησης οπών UDP θα περιγραφεί στα ακόλουθα άρθρα.

Απαιτήσεις Πρωτοκόλλου

  1. Αξιόπιστη παράδοση πακέτων που υλοποιείται μέσω ενός μηχανισμού θετικής ανάδρασης (η λεγόμενη θετική επιβεβαίωση)
  2. Η ανάγκη για αποτελεσματική μεταφορά μεγάλων δεδομένων, π.χ. το πρωτόκολλο πρέπει να αποφεύγει την περιττή αναμετάδοση πακέτων
  3. Θα πρέπει να είναι δυνατή η ακύρωση του μηχανισμού επιβεβαίωσης παράδοσης (η ικανότητα να λειτουργεί ως "καθαρό" πρωτόκολλο UDP)
  4. Δυνατότητα υλοποίησης λειτουργίας εντολής, με επιβεβαίωση κάθε μηνύματος
  5. Η βασική μονάδα μεταφοράς δεδομένων μέσω του πρωτοκόλλου πρέπει να είναι ένα μήνυμα

Αυτές οι απαιτήσεις συμπίπτουν σε μεγάλο βαθμό με τις απαιτήσεις του πρωτοκόλλου αξιόπιστων δεδομένων που περιγράφονται στο rfc908 и rfc1151, και βασίστηκα σε αυτά τα πρότυπα κατά την ανάπτυξη αυτού του πρωτοκόλλου.

Για να κατανοήσουμε αυτές τις απαιτήσεις, ας δούμε το χρονοδιάγραμμα μεταφοράς δεδομένων μεταξύ δύο κόμβων δικτύου χρησιμοποιώντας τα πρωτόκολλα TCP και UDP. Αφήστε και στις δύο περιπτώσεις να έχουμε χαμένο ένα πακέτο.
Μεταφορά μη διαδραστικών δεδομένων μέσω TCP:Υλοποίηση του πρωτοκόλλου Reliable Udp για .Net

Όπως μπορείτε να δείτε από το διάγραμμα, σε περίπτωση απώλειας πακέτου, το TCP θα εντοπίσει το χαμένο πακέτο και θα το αναφέρει στον αποστολέα ζητώντας τον αριθμό του χαμένου τμήματος.
Μεταφορά δεδομένων μέσω πρωτοκόλλου UDP:Υλοποίηση του πρωτοκόλλου Reliable Udp για .Net

Το UDP δεν πραγματοποιεί βήματα ανίχνευσης απώλειας. Ο έλεγχος των σφαλμάτων μετάδοσης στο πρωτόκολλο UDP είναι αποκλειστικά ευθύνη της εφαρμογής.

Η ανίχνευση σφαλμάτων στο πρωτόκολλο TCP επιτυγχάνεται με τη δημιουργία μιας σύνδεσης με έναν τερματικό κόμβο, την αποθήκευση της κατάστασης αυτής της σύνδεσης, την ένδειξη του αριθμού των byte που αποστέλλονται σε κάθε κεφαλίδα πακέτου και την κοινοποίηση αποδείξεων χρησιμοποιώντας έναν αριθμό επιβεβαίωσης.

Επιπλέον, για να βελτιώσει την απόδοση (δηλαδή να στείλει περισσότερα από ένα τμήματα χωρίς να λάβει επιβεβαίωση), το πρωτόκολλο TCP χρησιμοποιεί το λεγόμενο παράθυρο μετάδοσης - τον αριθμό των byte δεδομένων που αναμένει να λάβει ο αποστολέας του τμήματος.

Για περισσότερες πληροφορίες σχετικά με το πρωτόκολλο TCP, βλ rfc793, από UDP έως rfc768όπου μάλιστα ορίζονται.

Από τα παραπάνω, είναι σαφές ότι προκειμένου να δημιουργηθεί ένα αξιόπιστο πρωτόκολλο παράδοσης μηνυμάτων μέσω UDP (εφεξής Αξιόπιστο UDP), απαιτείται η εφαρμογή μηχανισμών μεταφοράς δεδομένων παρόμοιων με το TCP. Και συγκεκριμένα:

  • αποθήκευση κατάστασης σύνδεσης
  • χρησιμοποιήστε αρίθμηση τμημάτων
  • χρησιμοποιήστε ειδικά πακέτα επιβεβαίωσης
  • χρησιμοποιήστε έναν απλοποιημένο μηχανισμό παραθύρου για να αυξήσετε την απόδοση του πρωτοκόλλου

Επιπλέον, χρειάζεστε:

  • σηματοδοτεί την έναρξη ενός μηνύματος, για την κατανομή πόρων για τη σύνδεση
  • σηματοδοτεί το τέλος ενός μηνύματος, για να περάσει το ληφθέν μήνυμα στην ανοδική εφαρμογή και να αποδεσμεύσει πόρους πρωτοκόλλου
  • επιτρέψτε στο πρωτόκολλο της συγκεκριμένης σύνδεσης να απενεργοποιήσει τον μηχανισμό επιβεβαίωσης παράδοσης να λειτουργεί ως "καθαρό" UDP

Αξιόπιστη κεφαλίδα UDP

Θυμηθείτε ότι ένα datagram UDP είναι ενθυλακωμένο σε ένα datagram IP. Το Reliable UDP πακέτο είναι κατάλληλα "τυλιγμένο" σε ένα datagram UDP.
Αξιόπιστη ενθυλάκωση κεφαλίδας UDP:Υλοποίηση του πρωτοκόλλου Reliable Udp για .Net

Η δομή της κεφαλίδας Reliable UDP είναι αρκετά απλή:

Υλοποίηση του πρωτοκόλλου Reliable Udp για .Net

  • Σημαίες - σημαίες ελέγχου πακέτων
  • MessageType - τύπος μηνύματος που χρησιμοποιείται από upstream εφαρμογές για εγγραφή σε συγκεκριμένα μηνύματα
  • TransmissionId - ο αριθμός της μετάδοσης, μαζί με τη διεύθυνση και τη θύρα του παραλήπτη, προσδιορίζει μοναδικά τη σύνδεση
  • PacketNumber - αριθμός πακέτου
  • Επιλογές - πρόσθετες επιλογές πρωτοκόλλου. Στην περίπτωση του πρώτου πακέτου, χρησιμοποιείται για να υποδείξει το μέγεθος του μηνύματος

Οι σημαίες είναι οι εξής:

  • FirstPacket - το πρώτο πακέτο του μηνύματος
  • NoAsk - το μήνυμα δεν απαιτεί την ενεργοποίηση ενός μηχανισμού επιβεβαίωσης
  • LastPacket - το τελευταίο πακέτο του μηνύματος
  • RequestForPacket - πακέτο επιβεβαίωσης ή αίτημα για χαμένο πακέτο

Γενικές αρχές του πρωτοκόλλου

Εφόσον το αξιόπιστο UDP εστιάζει στην εγγυημένη μετάδοση μηνυμάτων μεταξύ δύο κόμβων, πρέπει να μπορεί να δημιουργήσει μια σύνδεση με την άλλη πλευρά. Για να δημιουργήσετε μια σύνδεση, ο αποστολέας στέλνει ένα πακέτο με τη σημαία FirstPacket, η απάντηση στην οποία θα σημαίνει ότι η σύνδεση έχει δημιουργηθεί. Όλα τα πακέτα απόκρισης, ή, με άλλα λόγια, τα πακέτα επιβεβαίωσης, ορίζουν πάντα την τιμή του πεδίου PacketNumber σε μία μεγαλύτερη από τη μεγαλύτερη τιμή PacketNumber των πακέτων που ελήφθησαν με επιτυχία. Το πεδίο Επιλογές για το πρώτο πακέτο που αποστέλλεται έχει το μέγεθος του μηνύματος.

Ένας παρόμοιος μηχανισμός χρησιμοποιείται για τον τερματισμό μιας σύνδεσης. Η σημαία LastPacket έχει οριστεί στο τελευταίο πακέτο του μηνύματος. Στο πακέτο απόκρισης, υποδεικνύεται ο αριθμός του τελευταίου πακέτου + 1, που για την πλευρά λήψης σημαίνει επιτυχή παράδοση του μηνύματος.
Διάγραμμα εγκατάστασης και τερματισμού σύνδεσης:Υλοποίηση του πρωτοκόλλου Reliable Udp για .Net

Όταν δημιουργηθεί η σύνδεση, ξεκινά η μεταφορά δεδομένων. Τα δεδομένα μεταδίδονται σε μπλοκ πακέτων. Κάθε μπλοκ, εκτός από το τελευταίο, περιέχει έναν σταθερό αριθμό πακέτων. Είναι ίσο με το μέγεθος του παραθύρου λήψης/μετάδοσης. Το τελευταίο μπλοκ δεδομένων μπορεί να έχει λιγότερα πακέτα. Μετά την αποστολή κάθε μπλοκ, η πλευρά αποστολής περιμένει μια επιβεβαίωση παράδοσης ή ένα αίτημα για την εκ νέου παράδοση των χαμένων πακέτων, αφήνοντας ανοιχτό το παράθυρο λήψης/μετάδοσης για να λάβει απαντήσεις. Μετά τη λήψη της επιβεβαίωσης της παράδοσης μπλοκ, το παράθυρο λήψης/μετάδοσης αλλάζει και αποστέλλεται το επόμενο μπλοκ δεδομένων.

Η πλευρά λήψης λαμβάνει τα πακέτα. Κάθε πακέτο ελέγχεται για να διαπιστωθεί εάν εμπίπτει στο παράθυρο μετάδοσης. Τα πακέτα και τα διπλότυπα που δεν εμπίπτουν στο παράθυρο φιλτράρονται. Επειδή Εάν το μέγεθος του παραθύρου είναι σταθερό και το ίδιο για τον παραλήπτη και τον αποστολέα, τότε στην περίπτωση που ένα μπλοκ πακέτων παραδίδεται χωρίς απώλεια, το παράθυρο μετατοπίζεται στη λήψη πακέτων του επόμενου μπλοκ δεδομένων και επιβεβαιώνεται η παράδοση. Απεσταλμένα. Εάν το παράθυρο δεν γεμίσει εντός της προθεσμίας που έχει ορίσει ο χρονοδιακόπτης εργασίας, τότε θα ξεκινήσει ένας έλεγχος στα οποία δεν έχουν παραδοθεί πακέτα και θα αποσταλούν αιτήματα για εκ νέου παράδοση.
Διάγραμμα αναμετάδοσης:Υλοποίηση του πρωτοκόλλου Reliable Udp για .Net

Χρονικά όρια και χρονόμετρα πρωτοκόλλου

Υπάρχουν διάφοροι λόγοι για τους οποίους δεν μπορεί να πραγματοποιηθεί μια σύνδεση. Για παράδειγμα, εάν ο παραλήπτης είναι εκτός σύνδεσης. Σε αυτήν την περίπτωση, όταν προσπαθείτε να δημιουργήσετε μια σύνδεση, η σύνδεση θα κλείσει λόγω χρονικού ορίου. Η υλοποίηση του αξιόπιστου UDP χρησιμοποιεί δύο χρονόμετρα για να ορίσει χρονικά όρια. Το πρώτο, ο χρονοδιακόπτης λειτουργίας, χρησιμοποιείται για να περιμένει μια απάντηση από τον απομακρυσμένο κεντρικό υπολογιστή. Εάν πυροδοτηθεί από την πλευρά του αποστολέα, τότε το τελευταίο σταλμένο πακέτο αποστέλλεται ξανά. Εάν το χρονόμετρο λήξει στον παραλήπτη, τότε γίνεται έλεγχος για χαμένα πακέτα και αποστέλλονται αιτήματα για εκ νέου παράδοση.

Το δεύτερο χρονόμετρο χρειάζεται για να κλείσει η σύνδεση σε περίπτωση έλλειψης επικοινωνίας μεταξύ των κόμβων. Για την πλευρά του αποστολέα, ξεκινά αμέσως μετά τη λήξη του χρονοδιακόπτη εργασίας και περιμένει απάντηση από τον απομακρυσμένο κόμβο. Εάν δεν υπάρξει απάντηση εντός της καθορισμένης περιόδου, η σύνδεση τερματίζεται και οι πόροι αποδεσμεύονται. Για την πλευρά λήψης, ο χρονοδιακόπτης κλεισίματος σύνδεσης ξεκινά μετά τη λήξη του χρονοδιακόπτη εργασίας δύο φορές. Αυτό είναι απαραίτητο για να διασφαλιστεί η απώλεια του πακέτου επιβεβαίωσης. Όταν λήξει ο χρονοδιακόπτης, η σύνδεση τερματίζεται επίσης και οι πόροι απελευθερώνονται.

Αξιόπιστο διάγραμμα κατάστασης μετάδοσης UDP

Οι αρχές του πρωτοκόλλου υλοποιούνται σε μια μηχανή πεπερασμένης κατάστασης, κάθε κατάσταση της οποίας είναι υπεύθυνη για μια συγκεκριμένη λογική επεξεργασίας πακέτων.
Αξιόπιστο διάγραμμα κατάστασης UDP:

Υλοποίηση του πρωτοκόλλου Reliable Udp για .Net

Κλειστό - δεν είναι πραγματικά μια κατάσταση, είναι ένα σημείο έναρξης και λήξης για το αυτόματο. Για το κράτος Κλειστό Λαμβάνεται ένα μπλοκ ελέγχου μετάδοσης, το οποίο, υλοποιώντας έναν ασύγχρονο διακομιστή UDP, προωθεί τα πακέτα στις κατάλληλες συνδέσεις και ξεκινά την επεξεργασία κατάστασης.

FirstPacketSending – την αρχική κατάσταση στην οποία βρίσκεται η εξερχόμενη σύνδεση κατά την αποστολή του μηνύματος.

Σε αυτήν την κατάσταση, αποστέλλεται το πρώτο πακέτο για κανονικά μηνύματα. Για μηνύματα χωρίς επιβεβαίωση αποστολής, αυτή είναι η μόνη κατάσταση όπου αποστέλλεται ολόκληρο το μήνυμα.

Κύκλος αποστολής – βασική κατάσταση για τη μετάδοση πακέτων μηνυμάτων.

Μετάβαση σε αυτό από το κράτος FirstPacketSending πραγματοποιείται μετά την αποστολή του πρώτου πακέτου του μηνύματος. Σε αυτήν την κατάσταση έρχονται όλες οι αναγνωρίσεις και τα αιτήματα για αναμετάδοση. Η έξοδος από αυτό είναι δυνατή σε δύο περιπτώσεις - σε περίπτωση επιτυχούς παράδοσης του μηνύματος ή με χρονικό όριο.

FirstPacketReceived – την αρχική κατάσταση για τον παραλήπτη του μηνύματος.

Ελέγχει την ορθότητα της αρχής της μετάδοσης, δημιουργεί τις απαραίτητες δομές και στέλνει μια επιβεβαίωση παραλαβής του πρώτου πακέτου.

Για ένα μήνυμα που αποτελείται από ένα μόνο πακέτο και στάλθηκε χωρίς να χρησιμοποιηθεί απόδειξη παράδοσης, αυτή είναι η μόνη κατάσταση. Μετά την επεξεργασία ενός τέτοιου μηνύματος, η σύνδεση κλείνει.

Συναρμολόγηση – βασική κατάσταση λήψης πακέτων μηνυμάτων.

Γράφει πακέτα σε προσωρινή αποθήκευση, ελέγχει για απώλεια πακέτων, στέλνει επιβεβαιώσεις για την παράδοση ενός μπλοκ πακέτων και ολόκληρου του μηνύματος και στέλνει αιτήματα για εκ νέου παράδοση χαμένων πακέτων. Σε περίπτωση επιτυχούς λήψης ολόκληρου του μηνύματος, η σύνδεση μεταβαίνει στην κατάσταση Ολοκληρώθηκε το, διαφορετικά, λήγει ένα χρονικό όριο.

Ολοκληρώθηκε το – κλείσιμο της σύνδεσης σε περίπτωση επιτυχούς λήψης ολόκληρου του μηνύματος.

Αυτή η κατάσταση είναι απαραίτητη για τη συναρμολόγηση του μηνύματος και για την περίπτωση που η επιβεβαίωση παράδοσης του μηνύματος χάθηκε στο δρόμο προς τον αποστολέα. Αυτή η κατάσταση εξέρχεται από ένα χρονικό όριο, αλλά η σύνδεση θεωρείται επιτυχώς κλειστή.

Πιο βαθιά μέσα στον κώδικα. μονάδα ελέγχου μετάδοσης

Ένα από τα βασικά στοιχεία του αξιόπιστου 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;
  //...
}

Πιο βαθιά μέσα στον κώδικα. πολιτείες

Τα κράτη εφαρμόζουν τη μηχανή κατάστασης του πρωτοκόλλου Reliable UDP, όπου λαμβάνει χώρα η κύρια επεξεργασία των πακέτων. Η αφηρημένη κλάση ReliableUdpState παρέχει μια διεπαφή για την κατάσταση:

Υλοποίηση του πρωτοκόλλου Reliable Udp για .Net

Ολόκληρη η λογική του πρωτοκόλλου υλοποιείται από τις κλάσεις που παρουσιάζονται παραπάνω, μαζί με μια βοηθητική κλάση που παρέχει στατικές μεθόδους, όπως, για παράδειγμα, την κατασκευή της κεφαλίδας ReliableUdp από την εγγραφή σύνδεσης.

Στη συνέχεια, θα εξετάσουμε λεπτομερώς την υλοποίηση των μεθόδων διεπαφής που καθορίζουν τους βασικούς αλγόριθμους του πρωτοκόλλου.

Μέθοδος DisposeByTimeout

Η μέθοδος DisposeByTimeout είναι υπεύθυνη για την απελευθέρωση πόρων σύνδεσης μετά από ένα χρονικό όριο λήξης και για τη σηματοδότηση επιτυχούς/μη επιτυχούς παράδοσης μηνυμάτων.
ReliableUdpState.DisposeByTimeout:

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

Παρακάμπτεται μόνο στο κράτος Ολοκληρώθηκε το.
Completed.DisposeByTimeout:

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

Μέθοδος ProcessPackets

Η μέθοδος ProcessPackets είναι υπεύθυνη για την πρόσθετη επεξεργασία ενός πακέτου ή πακέτων. Κλήση απευθείας ή μέσω χρονοδιακόπτη αναμονής πακέτων.

Ικανός να Συναρμολόγηση η μέθοδος παρακάμπτεται και είναι υπεύθυνη για τον έλεγχο για χαμένα πακέτα και τη μετάβαση στην κατάσταση Ολοκληρώθηκε το, σε περίπτωση παραλαβής του τελευταίου πακέτου και επιτυχούς ελέγχου
Assembling.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);
}

Ικανός να Ολοκληρώθηκε το η μέθοδος σταματά το χρονόμετρο που τρέχει και στέλνει το μήνυμα στους συνδρομητές.
Completed.ProcessPackets:

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

Μέθοδος ReceivePacket

Ικανός να FirstPacketReceived Το κύριο καθήκον της μεθόδου είναι να προσδιορίσει εάν το πρώτο πακέτο μηνυμάτων έφτασε πράγματι στη διεπαφή, καθώς και να συλλέξει ένα μήνυμα που αποτελείται από ένα μόνο πακέτο.
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));
}

Ικανός να Συναρμολόγηση στη μέθοδο ReceivePacket, πραγματοποιείται η κύρια εργασία της συναρμολόγησης ενός μηνύματος από τα εισερχόμενα πακέτα.
Assembling.ReceivePacket:

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

Ικανός να Ολοκληρώθηκε το το μόνο καθήκον της μεθόδου είναι να στείλει μια εκ νέου επιβεβαίωση της επιτυχούς παράδοσης του μηνύματος.
Completed.ReceivePacket:

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

Μέθοδος αποστολής πακέτου

Ικανός να FirstPacketSending Αυτή η μέθοδος στέλνει το πρώτο πακέτο δεδομένων ή εάν το μήνυμα δεν απαιτεί επιβεβαίωση παράδοσης, ολόκληρο το μήνυμα.
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 );
  }
}

Πιο βαθιά μέσα στον κώδικα. Δημιουργία και δημιουργία συνδέσεων

Τώρα που είδαμε τις βασικές καταστάσεις και τις μεθόδους που χρησιμοποιούνται για τον χειρισμό καταστάσεων, ας αναλύσουμε μερικά παραδείγματα για το πώς λειτουργεί το πρωτόκολλο με λίγο περισσότερες λεπτομέρειες.
Διάγραμμα μετάδοσης δεδομένων υπό κανονικές συνθήκες:Υλοποίηση του πρωτοκόλλου Reliable 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, λαμβάνει το απεσταλμένο πακέτο, δημιουργεί ένα νέο εγγραφή σύνδεσης και μεταβιβάζει αυτό το πακέτο, με μια προαναλυμένη κεφαλίδα, στη μέθοδο ReceivePacket της κατάστασης για επεξεργασία FirstPacketReceived
Δημιουργία σύνδεσης στην πλευρά λήψης:

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. Εξετάστε ένα παράδειγμα στο οποίο ένας ενδιάμεσος κόμβος απέτυχε και η παράδοση δεδομένων και προς τις δύο κατευθύνσεις έγινε αδύνατη.
Διάγραμμα για το κλείσιμο μιας σύνδεσης κατά το χρονικό όριο:Υλοποίηση του πρωτοκόλλου Reliable 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 δευτερόλεπτα.

Για μια εισερχόμενη σύνδεση, ο χρονοδιακόπτης ξεκινά μετά τη λήψη του τελευταίου εισερχόμενου πακέτου δεδομένων, αυτό συμβαίνει στη μέθοδο ReceivePacket της κατάστασης Συναρμολόγηση
Ενεργοποίηση του χρονοδιακόπτη εργασίας (κατάσταση συναρμολόγησης):

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

Μετά από αυτό, ο χρονοδιακόπτης κλεισίματος σύνδεσης ξεκινά στην εξερχόμενη σύνδεση.
ReliableUdpState.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);
  }
}

Πιο βαθιά μέσα στον κώδικα. Επαναφορά μεταφοράς δεδομένων

Διάγραμμα ανάκτησης μετάδοσης δεδομένων σε περίπτωση απώλειας πακέτων:Υλοποίηση του πρωτοκόλλου Reliable 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

Για να αλληλεπιδράσετε με το πρωτόκολλο μεταφοράς δεδομένων, υπάρχει μια ανοιχτή κλάση Reliable 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: Ομότιμη επικοινωνία μέσω μεταφραστών διευθύνσεων δικτύου
  7. Εφαρμογή του μοντέλου ασύγχρονου προγραμματισμού: Εφαρμογή του Μοντέλου Ασύγχρονου Προγραμματισμού CLR и Πώς να εφαρμόσετε το μοτίβο σχεδίασης IAsyncResult
  8. Μεταφορά του μοντέλου ασύγχρονου προγραμματισμού στο ασύγχρονο μοτίβο που βασίζεται σε εργασίες (APM στο TAP):
    TPL και Παραδοσιακός Ασύγχρονος Προγραμματισμός .NET
    Αλληλεπίδραση με άλλα ασύγχρονα μοτίβα και τύπους

Ενημέρωση: Ευχαριστώ mayorovp и sidristij για την ιδέα της προσθήκης μιας εργασίας στη διεπαφή. Δεν παραβιάζεται η συμβατότητα της βιβλιοθήκης με παλιά λειτουργικά συστήματα, γιατί Το 4ο πλαίσιο υποστηρίζει διακομιστή XP και 2003.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο