.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

Интернет нэлээд эрт өөрчлөгдсөн. Интернэтийн үндсэн протоколуудын нэг - UDP программууд нь зөвхөн датаграмм болон өргөн нэвтрүүлгийг дамжуулахаас гадна сүлжээний зангилааны хооронд "peer-to-peer" холболтыг хангахад ашиглагддаг. Энэхүү протокол нь энгийн хийцтэй учир урьд өмнө төлөвлөөгүй олон хэрэглээтэй боловч баталгаатай хүргэлт байхгүй зэрэг протоколын дутагдал хаана ч арилаагүй байна. Энэ нийтлэлд UDP-ээр баталгаатай хүргэх протоколын хэрэгжилтийг тайлбарласан болно.
Агуулга:нэвтрэх
Протоколын шаардлага
Найдвартай UDP толгой
Протоколын ерөнхий зарчим
Хугацаа болон протоколын таймер
Найдвартай UDP дамжуулалтын төлөвийн диаграм
Кодын талаар илүү гүнзгий. дамжуулах хяналтын нэгж
Кодын талаар илүү гүнзгий. мужууд

Кодын талаар илүү гүнзгий. Харилцаа холбоог бий болгох, бий болгох
Кодын талаар илүү гүнзгий. Хугацаа дуусах үед холболтыг хааж байна
Кодын талаар илүү гүнзгий. Өгөгдлийн дамжуулалтыг сэргээж байна
Найдвартай UDP API
дүгнэлт
Хэрэгтэй холбоосууд болон нийтлэлүүд

нэвтрэх

Интернэтийн анхны архитектур нь зангилаа бүр нь глобал, өвөрмөц IP хаягтай бөгөөд бусад зангилаатай шууд харилцаж чаддаг нэгэн төрлийн хаягийн орон зайг авч үзсэн. Одоо Интернет нь үнэндээ өөр бүтэцтэй - дэлхийн IP хаягуудын нэг хэсэг, NAT төхөөрөмжүүдийн ард нууцлагдсан олон тооны хувийн хаягууд байдаг.Энэхүү архитектурт зөвхөн дэлхийн хаягийн орон зайд байгаа төхөөрөмжүүд нь дэлхий даяар чиглүүлэх өвөрмөц IP хаягтай тул сүлжээн дэх хэнтэй ч хялбар харилцаж чаддаг. Хувийн сүлжээн дэх зангилаа нь нэг сүлжээний бусад зангилаатай холбогдож болохоос гадна дэлхийн хаягийн орон зайн бусад алдартай зангилаатай холбогдож болно. Энэхүү харилцан үйлчлэл нь сүлжээний хаягийн орчуулгын механизмын ачаар голчлон хийгддэг. Wi-Fi чиглүүлэгч гэх мэт NAT төхөөрөмжүүд нь гарч буй холболтод зориулж орчуулгын хүснэгтийн тусгай оруулгуудыг үүсгэж, IP хаяг, портын дугаарыг багц хэлбэрээр өөрчилдөг. Энэ нь хувийн сүлжээнээс дэлхийн хаягийн орон зай дахь хостууд руу гарах холболтыг зөвшөөрдөг. Гэхдээ үүнтэй зэрэгцэн NAT төхөөрөмжүүд нь ирж буй холболтын хувьд тусдаа дүрмийг тогтоогоогүй бол бүх ирж буй траффикийг хаадаг.

Интернэтийн энэхүү архитектур нь үйлчлүүлэгч-серверийн харилцааны хувьд хангалттай зөв бөгөөд үйлчлүүлэгчид нь хувийн сүлжээнд байж болох бөгөөд серверүүд нь глобал хаягтай байдаг. Гэхдээ энэ нь хоёр зангилааг хооронд нь шууд холбоход хүндрэл учруулдаг төрөл бүрийн хувийн сүлжээнүүд. Хоёр зангилаа хоорондын шууд холболт нь дуу дамжуулах (Skype), компьютерт алсаас хандах (TeamViewer) эсвэл онлайн тоглоом тоглох зэрэг үе тэнгийн програмуудад чухал ач холбогдолтой.

Төрөл бүрийн хувийн сүлжээн дэх төхөөрөмжүүдийн хооронд үе тэнгийн холболтыг бий болгох хамгийн үр дүнтэй аргуудын нэг бол нүх цоолох гэж нэрлэгддэг. Энэ техникийг UDP протокол дээр суурилсан програмуудад ихэвчлэн ашигладаг.

Гэхдээ хэрэв таны програм өгөгдөл дамжуулах баталгаатай байхыг шаарддаг бол, жишээлбэл, та компьютер хооронд файл дамжуулдаг бол UDP нь TCP-ээс ялгаатай нь UDP нь баталгаат хүргэх протокол биш бөгөөд багцын хүргэлтийг дарааллаар нь хангадаггүй тул UDP-г ашиглах нь олон бэрхшээлтэй тулгарах болно. протокол.

Энэ тохиолдолд багцын баталгаат хүргэлтийг хангахын тулд шаардлагатай функцийг хангадаг, UDP дээр ажилладаг хэрэглээний түвшний протоколыг хэрэгжүүлэх шаардлагатай.

Янз бүрийн хувийн сүлжээн дэх зангилааны хооронд TCP холболт үүсгэх TCP цооногийн техник байдгийг би нэн даруй тэмдэглэхийг хүсч байна, гэхдээ олон NAT төхөөрөмж үүнийг дэмждэггүй тул үүнийг ихэвчлэн холбох гол арга гэж үздэггүй. ийм зангилаа.

Энэ нийтлэлийн үлдсэн хугацаанд би зөвхөн баталгаат хүргэх протоколын хэрэгжилтэд анхаарлаа хандуулах болно. UDP цоолтуурын техникийн хэрэгжилтийг дараах нийтлэлүүдэд тайлбарлах болно.

Протоколын шаардлага

  1. Найдвартай багц хүргэлт нь эерэг санал хүсэлтийн механизмаар хэрэгждэг (эерэг хүлээн зөвшөөрөлт гэж нэрлэгддэг)
  2. Том өгөгдлийг үр дүнтэй дамжуулах хэрэгцээ, i.e. Протокол нь шаардлагагүй пакет дамжуулахаас зайлсхийх ёстой
  3. Хүргэлтийг баталгаажуулах механизмыг цуцлах боломжтой байх ёстой ("цэвэр" UDP протоколоор ажиллах чадвар)
  4. Мессеж бүрийг баталгаажуулсан тушаалын горимыг хэрэгжүүлэх чадвар
  5. Протоколоор өгөгдөл дамжуулах үндсэн нэгж нь мессеж байх ёстой

Эдгээр шаардлагууд нь-д дурдсан Найдвартай өгөгдлийн протоколын шаардлагуудтай үндсэндээ давхцдаг rfc908 и rfc1151, мөн би энэ протоколыг боловсруулахдаа тэдгээр стандартад тулгуурласан.

Эдгээр шаардлагыг ойлгохын тулд TCP болон UDP протоколуудыг ашиглан сүлжээний хоёр зангилааны хооронд өгөгдөл дамжуулах хугацааг харцгаая. Аль ч тохиолдолд бид нэг пакет алдагдах болно.
Интерактив бус өгөгдлийг TCP-ээр дамжуулах:.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

Диаграмаас харахад пакет алдагдсан тохиолдолд TCP алдагдсан пакетийг илрүүлж, алдагдсан сегментийн дугаарыг асууж илгээгчид мэдээлэх болно.
UDP протоколоор өгөгдөл дамжуулах:.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

UDP нь алдагдлыг илрүүлэх ямар ч алхам хийдэггүй. UDP протокол дахь дамжуулалтын алдааг хянах нь бүхэлдээ програмын үүрэг юм.

TCP протокол дахь алдааг илрүүлэх нь төгсгөлийн зангилаатай холболт үүсгэж, уг холболтын төлөвийг хадгалах, пакетийн толгой хэсэгт илгээсэн байтуудын тоог зааж, хүлээн авсан баримтыг хүлээн зөвшөөрсөн дугаар ашиглан мэдэгдэнэ.

Нэмж дурдахад, гүйцэтгэлийг сайжруулахын тулд (жишээ нь, нэгээс олон сегментийг хүлээн зөвшөөрөхгүйгээр илгээх) TCP протокол нь сегментийн илгээгчийн хүлээн авахаар хүлээгдэж буй өгөгдлийн байтуудын тоог дамжуулах цонхыг ашигладаг.

TCP протоколын талаар дэлгэрэнгүй мэдээллийг үзнэ үү rfc793, UDP-ээс rfc768үнэндээ тэд хаана тодорхойлогддог.

Дээрхээс харахад UDP (цаашид гэх)-ээр найдвартай мессеж дамжуулах протоколыг бий болгох нь тодорхой байна. Найдвартай UDP), TCP-тэй төстэй өгөгдөл дамжуулах механизмыг хэрэгжүүлэх шаардлагатай. Тухайлбал:

  • холболтын төлөвийг хадгалах
  • сегментийн дугаарлалт ашиглах
  • тусгай баталгаажуулах багцуудыг ашиглах
  • протоколын нэвтрүүлэх чадварыг нэмэгдүүлэхийн тулд хялбаршуулсан цонхны механизмыг ашиглах

Үүнээс гадна танд хэрэгтэй:

  • холболтын нөөцийг хуваарилахын тулд мессежийн эхлэлийг дохио
  • зурвасын төгсгөлийг дохио өгөх, хүлээн авсан мессежийг дээд талын програм руу дамжуулах, протоколын нөөцийг гаргах
  • "цэвэр" UDP байдлаар ажиллахын тулд хүргэлтийн баталгаажуулалтын механизмыг идэвхгүй болгохын тулд холболтын тусгай протоколыг зөвшөөрөх

Найдвартай UDP толгой

UDP датаграммыг IP датаграммд багтаасан гэдгийг санаарай. Найдвартай UDP пакетийг UDP датаграммд зохих ёсоор "боосон".
Найдвартай UDP толгойн капсулжуулалт:.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

Найдвартай UDP толгойн бүтэц нь маш энгийн:

.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

  • Тугнууд - багц хяналтын тугнууд
  • MessageType - тодорхой мессежийг захиалахын тулд дээд талын програмуудад ашигладаг мессежийн төрөл
  • TransmissionId - дамжуулалтын дугаар, хүлээн авагчийн хаяг, портын хамт холболтыг өвөрмөц байдлаар тодорхойлдог.
  • Пакетийн дугаар - багцын дугаар
  • Сонголтууд - нэмэлт протоколын сонголтууд. Эхний багцын хувьд энэ нь мессежийн хэмжээг зааж өгөхөд хэрэглэгддэг

Тугнууд дараах байдалтай байна.

  • FirstPacket - мессежийн эхний пакет
  • NoAsk - мессеж нь хүлээн зөвшөөрөх механизмыг идэвхжүүлэх шаардлагагүй
  • LastPacket - мессежийн сүүлчийн багц
  • RequestForPacket - баталгаажуулах багц эсвэл алдагдсан пакетийн хүсэлт

Протоколын ерөнхий зарчим

Найдвартай UDP нь хоёр зангилааны хооронд баталгаатай мессеж дамжуулахад чиглэгддэг тул нөгөө талтай холбоо тогтоох чадвартай байх ёстой. Холболтыг бий болгохын тулд илгээгч нь FirstPacket туг бүхий пакет илгээдэг бөгөөд хариу нь холболт үүссэн гэсэн үг юм. Бүх хариултын пакетууд эсвэл өөрөөр хэлбэл хүлээн зөвшөөрөгдсөн пакетууд нь амжилттай хүлээн авсан пакетуудын хамгийн том PacketNumber утгаас нэгээс илүү PacketNumber талбарын утгыг үргэлж тохируулдаг. Эхний илгээсэн пакетийн сонголтын талбар нь мессежийн хэмжээ юм.

Үүнтэй төстэй механизмыг холболтыг зогсооход ашигладаг. LastPacket-ийн тугийг мессежийн сүүлчийн пакет дээр суулгасан. Хариултын багцад сүүлийн багцын дугаар + 1-ийг зааж өгсөн бөгөөд энэ нь хүлээн авагч талын хувьд мессежийг амжилттай хүргэсэн гэсэн үг юм.
Холболт ба холболтын схем:.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

Холболт хийгдсэний дараа өгөгдөл дамжуулж эхэлнэ. Өгөгдлийг багц блокоор дамжуулдаг. Сүүлчийн блокоос бусад блок бүр тодорхой тооны пакетуудыг агуулна. Энэ нь хүлээн авах/дамжуулах цонхны хэмжээтэй тэнцүү байна. Сүүлийн блок өгөгдлийн багц цөөн байж болно. Блок бүрийг илгээсний дараа илгээгч тал хүргэлтийн баталгаажуулалт эсвэл алдагдсан пакетуудыг дахин хүргэх хүсэлтийг хүлээж хариу хүлээн авах/дамжуулах цонхыг нээлттэй үлдээдэг. Блок хүргэлтийн баталгааг хүлээн авсны дараа хүлээн авах/дамжуулах цонх шилжиж, дараагийн блок өгөгдөл илгээгдэнэ.

Хүлээн авагч тал пакетуудыг хүлээн авдаг. Пакет бүр нь дамжуулах цонхонд багтаж байгаа эсэхийг шалгадаг. Цонхонд унахгүй байгаа пакетууд болон хуулбаруудыг шүүдэг. Учир нь Хэрэв цонхны хэмжээ нь тогтмол бөгөөд хүлээн авагч болон илгээгчийн хувьд ижил байвал багцын багцыг алдагдуулахгүйгээр хүргэх тохиолдолд цонхыг дараагийн блокийн өгөгдлийн багц хүлээн авахаар шилжүүлж, хүргэлтийг баталгаажуулна. илгээсэн. Хэрэв ажлын цаг хэмжигчээс тогтоосон хугацаанд цонх дүүрэхгүй бол аль багцыг ирүүлээгүй эсэхийг шалгаж, дахин хүргэх хүсэлтийг илгээнэ.
Дахин дамжуулах диаграм:.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

Хугацаа болон протоколын таймер

Холболт үүсгэж чадахгүй байгаа хэд хэдэн шалтгаан бий. Жишээлбэл, хэрэв хүлээн авагч тал офлайн бол. Энэ тохиолдолд холболт үүсгэхийг оролдох үед холболт нь завсарлагаанаар хаагдах болно. Найдвартай UDP хэрэгжүүлэлт нь хугацаа хэтрүүлэхийн тулд хоёр таймер ашигладаг. Эхнийх нь ажлын таймер нь алсын хостоос хариу хүлээхэд ашиглагддаг. Хэрэв энэ нь илгээгчийн тал дээр асвал хамгийн сүүлд илгээсэн пакет дахин илгээгдэнэ. Хэрэв таймер хүлээн авагч дээр дуусвал алга болсон пакетуудыг шалгаж, дахин хүргэх хүсэлтийг илгээнэ.

Хоёр дахь таймер нь зангилааны хоорондох холбоо байхгүй тохиолдолд холболтыг хаахад шаардлагатай. Илгээгчийн хувьд энэ нь ажлын цаг дууссаны дараа шууд эхэлж, алсын зангилаанаас хариу хүлээж байна. Хэрэв заасан хугацаанд хариу өгөхгүй бол холболтыг зогсоож, нөөцийг чөлөөлнө. Хүлээн авагч талын хувьд ажлын таймерын хугацаа хоёр удаа дууссаны дараа холболтыг хаах таймерыг эхлүүлнэ. Энэ нь баталгаажуулалтын багцыг алдахаас хамгаалахад зайлшгүй шаардлагатай. Цаг тоологчийн хугацаа дуусахад холболтыг зогсоож, нөөцийг чөлөөлнө.

Найдвартай UDP дамжуулалтын төлөвийн диаграм

Протоколын зарчмууд нь хязгаарлагдмал төлөвт машинд хэрэгждэг бөгөөд төлөв бүр нь пакет боловсруулах тодорхой логикийг хариуцдаг.
Найдвартай UDP төлөвийн диаграм:

.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

Хаалттай - энэ нь үнэндээ төлөв биш, энэ нь автомат машины эхлэл ба төгсгөлийн цэг юм. Төрийн төлөө Хаалттай дамжуулалтын хяналтын блок хүлээн авсан бөгөөд энэ нь асинхрон UDP серверийг хэрэгжүүлж, пакетуудыг зохих холболтууд руу дамжуулж, төлөвийн боловсруулалтыг эхлүүлдэг.

FirstPacketSending – зурвас илгээх үед гарах холболтын анхны төлөв.

Энэ төлөвт ердийн мессежийн эхний пакет илгээгдэнэ. Илгээх баталгаагүй мессежийн хувьд энэ нь мессежийг бүхэлд нь илгээсэн цорын ганц муж юм.

SendingCycle – зурвасын багцыг дамжуулах үндсэн төлөв.

Төрөөс түүнд шилжих FirstPacketSending мессежийн эхний багцыг илгээсний дараа гүйцэтгэнэ. Энэ төлөвт бүх хүлээн зөвшөөрөлт, дахин дамжуулах хүсэлт ирдэг. Үүнээс гарах нь хоёр тохиолдолд боломжтой - мессежийг амжилттай хүргэсэн эсвэл хугацаа хэтэрсэн тохиолдолд.

Эхний багц хүлээн авсан – мессеж хүлээн авагчийн анхны төлөв.

Энэ нь дамжуулалтын эхлэлийн зөв эсэхийг шалгаж, шаардлагатай бүтцийг бий болгож, эхний багцыг хүлээн авсан тухай мэдэгдлийг илгээдэг.

Нэг пакетаас бүрдэх, хүргэлтийн баталгааг ашиглахгүйгээр илгээсэн мессежийн хувьд энэ нь цорын ганц төлөв юм. Ийм мессежийг боловсруулсны дараа холболт хаагдана.

Угсралт – зурвасын багц хүлээн авах үндсэн төлөв.

Энэ нь пакетуудыг түр хадгалах сан руу бичиж, пакет алдагдсан эсэхийг шалгаж, багцын блок болон нийт мессежийг хүргэсэн тухай мэдэгдлийг илгээж, алдагдсан пакетуудыг дахин хүргэх хүсэлтийг илгээдэг. Мессежийг бүхэлд нь амжилттай хүлээн авсан тохиолдолд холболт нь төлөвт шилждэг дууссан, эс бөгөөс завсарлага гарна.

дууссан – мессежийг бүхэлд нь амжилттай хүлээн авсан тохиолдолд холболтыг хаах.

Энэ төлөв нь мессежийг цуглуулах, илгээгч рүү очих замд мессежийн хүргэлтийн баталгаа алдагдсан тохиолдолд зайлшгүй шаардлагатай. Энэ төлөвөөс завсарлагаанаар гарсан боловч холболтыг амжилттай хаасан гэж үзнэ.

Кодын талаар илүү гүнзгий. дамжуулах хяналтын нэгж

Найдвартай 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 хийсвэр анги нь төлөвийн интерфейсийг өгдөг:

.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

Протоколын бүх логикийг дээр дурдсан ангиуд, жишээлбэл холболтын бичлэгээс 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();
}

Энэ нь зөвхөн мужид хүчингүй болсон дууссан.
Дууссан. DisposeByTimeout:

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

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 Энэ аргыг зөвхөн таймер дээр дууддаг бөгөөд сүүлийн мессежийг дахин илгээх, мөн холболтыг хаах таймерыг идэвхжүүлэх үүрэгтэй.
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);
}

Пакет хүлээн авах арга

Боломжтой Эхний багц хүлээн авсан Аргын гол ажил бол интерфэйс дээр анхны мессежийн пакет ирсэн эсэхийг тодорхойлох, мөн нэг пакетаас бүрдэх мессежийг цуглуулах явдал юм.
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 Энэ аргыг хүргэлтийн хүлээн зөвшөөрөлт болон дахин дамжуулах хүсэлтийг хүлээн авахын тулд хүчингүй болгосон.
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 аргад ирж буй пакетуудаас мессеж цуглуулах үндсэн ажил явагдана.
Пакетийг угсарч байна.Хүлээн авах:

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 Энэ арга нь өгөгдлийн эхний багцыг илгээдэг, хэрэв мессеж нь хүргэлтийн баталгаажуулалтыг шаарддаггүй бол бүхэл бүтэн мессежийг илгээдэг.
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 Энэ аргын хувьд багцын блок илгээгддэг.
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 );
  }
}

Кодын талаар илүү гүнзгий. Харилцаа холбоог бий болгох, бий болгох

Одоо бид үндсэн төлөвүүд болон төлөвүүдийг зохицуулах аргуудыг үзсэнийхээ дараа протокол хэрхэн ажилладаг тухай цөөн хэдэн жишээг арай илүү нарийвчлан авч үзье.
Хэвийн нөхцөлд өгөгдөл дамжуулах диаграм:.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

Бүтээлийг нарийвчлан авч үзье холболтын бичлэг эхний багцыг холбож илгээх. Шилжүүлгийг зурвас илгээх 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);
}

Эхний багцыг илгээсний дараа илгээгч муж руу орно SendingCycle – багц хүргэлтийн баталгааг хүлээнэ үү.
Хүлээн авагч тал нь EndReceive аргыг ашиглан илгээсэн пакетыг хүлээн авч, шинээр үүсгэнэ холболтын бичлэг мөн энэ пакетыг урьдчилан задалсан толгойн тусламжтайгаар төлөвийн ReceivePacket арга руу дамжуулдаг. Эхний багц хүлээн авсан
Хүлээн авагч тал дээр холболт үүсгэх:

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-ийн чухал хэсэг бол хугацаа хэтэрсэн зохицуулалт юм. Завсрын зангилаа бүтэлгүйтэж, хоёр чиглэлд өгөгдөл дамжуулах боломжгүй болсон жишээг авч үзье.
Хугацаа дууссанаар холболтыг хаах диаграм:.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

Диаграммаас харахад илгээгчийн ажлын таймер блок багц илгээсний дараа шууд эхэлдэг. Энэ нь төлөвийн SendPacket аргад тохиолддог SendingCycle.
Ажлын цаг хэмжигчийг идэвхжүүлж байна (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);
  }
}

Кодын талаар илүү гүнзгий. Өгөгдлийн дамжуулалтыг сэргээж байна

Пакет алдагдсан тохиолдолд өгөгдөл дамжуулалтыг сэргээх схем:.Net-д зориулсан найдвартай Udp протоколыг хэрэгжүүлэх

Хугацаа дуусах үед холболтыг хаах талаар аль хэдийн ярилцсанчлан ажлын таймер дуусахад хүлээн авагч алга болсон пакетуудыг шалгах болно. Пакет алдагдсан тохиолдолд хүлээн авагчид хүрээгүй багцын жагсаалтыг гаргана. Эдгээр дугаарыг тодорхой холболтын 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-найдвартай-udp-00
  4. Найдвартай мэдээллийн протокол: rfc908 и rfc1151
  5. UDP дээр хүргэлтийн баталгаажуулалтын энгийн хэрэгжилт: .NET болон UDP ашиглан сүлжээгээ бүрэн хянах
  6. NAT дамжих механизмыг тайлбарласан нийтлэл: Сүлжээний хаягийн орчуулагчид дундах үе тэнгийн харилцаа
  7. Асинхрон програмчлалын загварыг хэрэгжүүлэх: CLR асинхрон програмчлалын загварыг хэрэгжүүлэх и IAsyncResult дизайны загварыг хэрхэн хэрэгжүүлэх вэ
  8. Асинхрон програмчлалын загварыг даалгаварт суурилсан асинхрон загварт шилжүүлэх (TAP дахь APM):
    TPL болон уламжлалт .NET асинхрон програмчлал
    Бусад асинхрон загвар, төрлүүдтэй харилцан ажиллах

Шинэчлэлт: Баярлалаа майоровп и сидристиж интерфэйс дээр даалгавар нэмэх санааны хувьд. Номын сангийн хуучин үйлдлийн системтэй нийцтэй байдал зөрчигдөөгүй, учир нь 4-р хүрээ нь XP болон 2003 серверийг хоёуланг нь дэмждэг.

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх