.Net үчүн Reliable Udp протоколун ишке ашыруу

Интернет көп убакыт мурун өзгөргөн. Интернеттин негизги протоколдорунун бири - UDP тиркемелер тарабынан датаграммаларды жана берүүлөрдү жеткирүү үчүн гана эмес, ошондой эле тармак түйүндөрүнүн ортосундагы "тең-теңге" байланыштарды камсыз кылуу үчүн колдонулат. Жөнөкөй дизайндан улам бул протоколдун мурда пландаштырылбаган көптөгөн колдонулушу бар, бирок протоколдун кемчиликтери, кепилденген жеткирүүнүн жоктугу эч жерде жок болгон эмес. Бул макалада UDP аркылуу кепилденген жеткирүү протоколун ишке ашыруу сүрөттөлөт.
Мазмуну:кирүү
Протоколдун талаптары
Ишенимдүү UDP аталышы
Протоколдун жалпы принциптери
Тайм-ауттар жана протокол таймерлери
Ишенимдүү UDP берүү абалынын диаграммасы
Кодго тереңирээк. берүү башкаруу бирдиги
Кодго тереңирээк. мамлекеттер

Кодго тереңирээк. Байланыштарды түзүү жана орнотуу
Кодго тереңирээк. Байланыш күтүү учурунда жабылууда
Кодго тереңирээк. Берилиштерди өткөрүү калыбына келтирилүүдө
Ишенимдүү UDP API
жыйынтыктоо
Пайдалуу шилтемелер жана макалалар

кирүү

Интернеттин баштапкы архитектурасы бир тектүү дарек мейкиндигин болжолдогон, анда ар бир түйүн глобалдуу жана уникалдуу IP дарекке ээ жана башка түйүндөр менен түздөн-түз байланыша алат. Азыр Интернет, чындыгында, башка архитектурага ээ - глобалдык IP даректеринин бир аймагы жана NAT түзмөктөрүнүн артында жашыруун даректери бар көптөгөн аймактар.Бул архитектурада глобалдык дарек мейкиндигиндеги түзмөктөр гана тармактагы ар бир адам менен оңой байланыша алат, анткени алар уникалдуу, глобалдык багыттала турган IP дарекке ээ. Жеке тармактагы түйүн ошол эле тармактагы башка түйүндөр менен туташа алат, ошондой эле глобалдык дарек мейкиндигиндеги башка белгилүү түйүндөр менен да туташа алат. Бул өз ара аракеттенүү негизинен тармактык даректи которуу механизминин эсебинен ишке ашат. Wi-Fi роутерлери сыяктуу NAT түзмөктөрү чыгуучу байланыштар үчүн атайын котормо таблицасы жазууларын түзүшөт жана пакеттердеги IP даректерин жана порт номерлерин өзгөртүшөт. Бул жеке тармактан глобалдык дарек мейкиндигиндеги хостторго чыгуучу туташууларга мүмкүндүк берет. Бирок, ошол эле учурда, NAT түзмөктөрү, адатта, кирүүчү байланыштар үчүн өзүнчө эрежелер коюлмайынча, бардык келген трафикти блоктошот.

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

Ар кандай жеке тармактардагы түзмөктөрдүн ортосунда теңдүү байланышты түзүүнүн эң эффективдүү ыкмаларынын бири тешик тешүү деп аталат. Бул ыкма көбүнчө UDP протоколуна негизделген тиркемелерде колдонулат.

Бирок, эгерде сиздин тиркемеңиз маалыматтардын кепилденген жеткирилишин талап кылса, мисалы, сиз компьютерлер арасында файлдарды өткөрүп берсеңиз, анда UDPди колдонууда UDP кепилденген жеткирүү протоколу болбогондуктан жана TCPден айырмаланып пакетти иретинде жеткирүүнү камсыз кылбагандыктан көптөгөн кыйынчылыктарга дуушар болот. протокол.

Бул учурда пакеттин кепилденген жеткирилишин камсыз кылуу үчүн зарыл болгон функцияларды камсыз кылган жана UDP аркылуу иштеген тиркеме катмарынын протоколун ишке ашыруу талап кылынат.

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

Бул макаланын калган бөлүгүндө мен кепилденген жеткирүү протоколун ишке ашырууга гана токтолом. UDP тешик техникасын ишке ашыруу кийинки макалаларда баяндалат.

Протоколдун талаптары

  1. Позитивдүү жооп кайтаруу механизми аркылуу ишке ашырылган пакетти ишенимдүү жеткирүү (позитивдүү ырастоо деп аталган)
  2. Чоң маалыматтарды эффективдүү өткөрүү зарылдыгы, б.а. протокол керексиз пакет реледен качышы керек
  3. Жеткирүүнү ырастоо механизмин жокко чыгаруу мүмкүн болушу керек ("таза" UDP протоколу катары иштөө мүмкүнчүлүгү)
  4. Ар бир билдирүүнү ырастоо менен буйрук режимин ишке ашыруу мүмкүнчүлүгү
  5. Протокол боюнча маалыматтарды берүүнүн негизги бирдиги билдирүү болушу керек

Бул талаптар негизинен Ишенимдүү маалыматтар протоколунун талаптарына дал келет rfc908 и rfc1151, жана мен бул протоколду иштеп чыгууда ошол стандарттарга таяндым.

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

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

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

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

Кошумча, аткарууну жакшыртуу үчүн (б.а. бирден ашык сегментти тастыктоосуз жөнөтүү) TCP протоколу өткөрүү терезеси деп аталат - сегменттин жөнөтүүчүсү алууну күткөн маалыматтардын байттарынын санын колдонот.

TCP протоколу жөнүндө көбүрөөк маалымат алуу үчүн, караңыз rfc793, UDPден rfc768кайда, чындыгында, алар аныкталат.

Жогоруда айтылгандардан көрүнүп тургандай, UDP (мындан ары - бул) аркылуу ишенимдүү билдирүү жеткирүү протоколун түзүү үчүн. Ишенимдүү UDP), ал TCP окшош маалыматтарды берүү механизмдерин ишке ашыруу үчүн талап кылынат. Тактап айтканда:

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

Мындан тышкары, сизге керек:

  • Байланыш үчүн ресурстарды бөлүштүрүү үчүн билдирүүнүн башталышын белгилөө
  • Кабыл алынган билдирүүнү агымдагы тиркемеге өткөрүп берүү жана протоколдун ресурстарын чыгаруу үчүн билдирүүнүн аякташын белгилөө
  • "таза" UDP катары иштеши үчүн жеткирүү тастыктоо механизмин өчүрүү үчүн байланышка тиешелүү протоколго уруксат берүү

Ишенимдүү UDP аталышы

Эске салсак, UDP датаграммасы IP датаграммасында капсулдалат. Ишенимдүү UDP пакети UDP датаграммасына ылайыктуу түрдө "оролгон".
Ишенимдүү UDP баш инкапсуляциясы:.Net үчүн Reliable Udp протоколун ишке ашыруу

Reliable UDP аталышынын түзүлүшү абдан жөнөкөй:

.Net үчүн Reliable Udp протоколун ишке ашыруу

  • Flags - пакетти башкаруу желектери
  • MessageType - белгилүү бир билдирүүлөргө жазылуу үчүн жогорудагы колдонмолор тарабынан колдонулган билдирүү түрү
  • TransmissionId - алуучунун дареги жана порту менен бирге жөнөтүүнүн номери байланышты уникалдуу түрдө аныктайт
  • PacketNumber - пакет номери
  • Параметрлер - протоколдун кошумча параметрлери. Биринчи пакетте ал билдирүүнүн көлөмүн көрсөтүү үчүн колдонулат

Желектер төмөнкүдөй:

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

Протоколдун жалпы принциптери

Ишенимдүү UDP эки түйүн ортосунда кепилденген билдирүүлөрдү берүүгө багытталгандыктан, ал башка тарап менен байланыш түзө алышы керек. Байланышты орнотуу үчүн жөнөтүүчү FirstPacket желеги бар пакетти жөнөтөт, ага жооп байланыш түзүлдү дегенди билдирет. Бардык жооп пакеттери, же башкача айтканда, тастыктоо пакеттери, ар дайым PacketNumber талаасынын маанисин ийгиликтүү кабыл алынган пакеттердин эң чоң PacketNumber маанисинен бир чоңураак кылып коюшат. Биринчи жөнөтүлгөн пакеттин Жолдор талаасы кабардын өлчөмү.

Ушундай эле механизм байланышты токтотуу үчүн колдонулат. LastPacket желеги кабардын акыркы пакетинде орнотулган. Жооп пакетинде акыркы пакеттин номери + 1 көрсөтүлөт, бул кабыл алуучу тарап үчүн билдирүүнүн ийгиликтүү жеткирилишин билдирет.
Байланышты орнотуу жана токтотуу схемасы:.Net үчүн Reliable Udp протоколун ишке ашыруу

Байланыш орнотулгандан кийин, маалыматтарды өткөрүү башталат. Маалыматтар пакеттердин блокторунда берилет. Ар бир блок, акыркысынан башка, пакеттердин белгиленген санын камтыйт. Бул алуу/берүү терезесинин өлчөмүнө барабар. Маалыматтын акыркы блогунда пакеттер азыраак болушу мүмкүн. Ар бир блокту жөнөткөндөн кийин жөнөтүүчү тарап жеткирүү ырастоосун же жоголгон пакеттерди кайра жеткирүү өтүнүчүн күтөт, жооп алуу үчүн кабыл алуу/өткөрүү терезесин ачык калтырат. Блоктун жеткирилишинин тастыктоосун алгандан кийин, кабыл алуу/берүү терезеси жылып, кийинки маалымат блогу жөнөтүлөт.

Кабыл алуучу тарап пакеттерди алат. Ар бир пакет анын берүү терезесине кирээр-келбеси текшерилет. Терезеге түшпөгөн пакеттер жана дубликаттар чыпкадан чыгарылат. Анткени Эгерде терезенин өлчөмү белгиленген болсо жана алуучу менен жөнөтүүчү үчүн бирдей болсо, анда пакеттердин блогу жоготуусуз жеткирилген учурда, терезе маалыматтын кийинки блогунун пакеттерин кабыл алууга жылдырылат жана жеткирүүнү ырастоо. жиберилди. Эгерде терезе жумушчу таймер тарабынан белгиленген мөөнөттө толбосо, анда кайсы пакеттер жеткирилбегендигин текшерүү башталат жана кайра жеткирүү өтүнүчтөрү жөнөтүлөт.
Ретрансляция диаграммасы:.Net үчүн Reliable Udp протоколун ишке ашыруу

Тайм-ауттар жана протокол таймерлери

Байланыш түзүү мүмкүн эмес, бир нече себептер бар. Мисалы, кабыл алуучу тарап оффлайнда болсо. Бул учурда, байланыш түзүүгө аракет кылып жатканда, байланыш күтүү убакыты менен жабылат. Ишенимдүү UDP ишке ашыруу тайм-ауттарды коюу үчүн эки таймерди колдонот. Биринчи, жумушчу таймер, алыскы хосттон жооп күтүү үчүн колдонулат. Эгер ал жөнөтүүчү тарапта күйсө, анда акыркы жөнөтүлгөн пакет кайра жөнөтүлөт. Эгерде таймер алуучуда бүтсө, анда жоголгон пакеттерди текшерүү жүргүзүлөт жана кайра жеткирүү өтүнүчү жөнөтүлөт.

Экинчи таймер түйүндөр ортосунда байланыш жок болгон учурда байланышты жабуу үчүн керек. Жөнөтүүчү тарап үчүн ал жумушчу таймердин мөөнөтү аяктагандан кийин дароо башталат жана алыскы түйүндөн жооп күтөт. Көрсөтүлгөн мөөнөттө жооп болбосо, байланыш токтотулуп, ресурстар бошотулат. Кабыл алуучу тарап үчүн байланышты жабуу таймери жумуш таймеринин мөөнөтү эки жолу бүткөндөн кийин башталат. Бул ырастоо пакетин жоготуудан камсыздандыруу үчүн зарыл. Таймердин мөөнөтү аяктаганда, байланыш да токтотулат жана ресурстар бошотулат.

Ишенимдүү UDP берүү абалынын диаграммасы

Протоколдун принциптери чектүү мамлекеттик машинада ишке ашырылат, анын ар бир абалы пакеттерди иштетүүнүн белгилүү бир логикасына жооп берет.
Ишенимдүү UDP мамлекеттик диаграммасы:

.Net үчүн Reliable Udp протоколун ишке ашыруу

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

FirstPacketSending – билдирүү жөнөтүлгөндө чыгуучу байланыш болгон баштапкы абал.

Бул абалда кадимки билдирүүлөр үчүн биринчи пакет жөнөтүлөт. Жөнөтүү ырастоосу жок билдирүүлөр үчүн, бул билдирүү толугу менен жөнөтүлгөн жалгыз мамлекет.

SendingCycle – билдирүү пакеттерин берүү үчүн негизги абал.

Ага мамлекеттен өтүү FirstPacketSending билдирүүнүн биринчи пакети жөнөтүлгөндөн кийин ишке ашырылат. Дал ушул абалда бардык ырастоолор жана кайра өткөрүп берүү өтүнүчү келет. Андан чыгуу эки учурда мүмкүн - билдирүү ийгиликтүү жеткирилгенде же күтүлбөгөн убакта.

FirstPacketReceived – билдирүүнү алуучу үчүн баштапкы абал.

Ал берүүнүн башталышынын тууралыгын текшерет, керектүү структураларды түзөт жана биринчи пакетти алганы тууралуу тастыктоочу кат жөнөтөт.

Бир пакеттен турган жана жеткирүү далилин колдонбостон жөнөтүлгөн билдирүү үчүн бул жалгыз абал. Мындай билдирүүнү иштеп чыккандан кийин байланыш жабылат.

монтаждоо – билдирүү пакеттерин кабыл алуу үчүн негизги абал.

Ал убактылуу сактоочу жайга пакеттерди жазат, пакеттердин жоголушун текшерет, пакеттердин блогун жана бүтүндөй кабарды жеткирүү үчүн тастыктоолорду жөнөтөт жана жоголгон пакеттерди кайра жеткирүү өтүнүчтөрүн жөнөтөт. Бардык билдирүү ийгиликтүү алынган учурда, байланыш абалга өтөт Өтүлгөн, антпесе, күтүү убакыты чыгат.

Өтүлгөн – бардык билдирүү ийгиликтүү алынган учурда байланышты жабуу.

Бул абал билдирүүнү чогултуу үчүн жана жөнөтүүчүгө жолдон кабардын жеткирилгендигин ырастоо жоголгон учурда зарыл. Бул абалдан күтүү аяктады, бирок байланыш ийгиликтүү жабылды деп эсептелет.

Кодго тереңирээк. берүү башкаруу бирдиги

Reliable 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 үчүн Reliable 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();
}

Бул штатта гана жокко эсе Өтүлгөн.
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 бул ыкма таймерде гана чакырылат жана акыркы билдирүүнү кайра жөнөтүү үчүн, ошондой эле байланышты жабуу таймерин иштетүү үчүн жооптуу.
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 бул ыкма жеткирүүнү ырастоо жана кайра жөнөтүү өтүнүчтөрүн кабыл алуу үчүн жокко чыгарылган.
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 бул ыкмада пакеттердин блогу жөнөтүлөт.
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 үчүн Reliable 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 ыкмасына өткөрөт 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дин маанилүү бөлүгү. Ортодогу түйүн иштебей калган жана эки багытта тең маалыматтарды жеткирүү мүмкүн болбой калган мисалды карап көрөлү.
Байланышты күтүү убакыты боюнча жабуу диаграммасы:.Net үчүн Reliable 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 үчүн Reliable 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 );

Post:

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 аныктоо механизмин ишке ашыруу пайдалуу болот (бирок чоң билдирүүлөр жөнөтүлгөндө гана) .

Көңүл бурганыңыз үчүн рахмат, комментарийлериңизди жана комментарийлериңизди күтөм.

PS Деталдарга кызыккандар же жөн гана протоколду сынап көрүүнү каалагандар үчүн 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. Асинхрондук программалоо моделин тапшырмага негизделген асинхрондук үлгүгө өткөрүү (TAPдеги APM):
    TPL жана салттуу .NET асинхрондуу программалоо
    Башка асинхрондук үлгүлөр жана типтер менен иштешүү

Жаңыртуу: Рахмат майоровп и sidristij интерфейске тапшырма кошуу идеясы үчүн. Китепкананын эски операциялык системалар менен шайкештиги бузулган эмес, анткени 4-алкак XP жана 2003 серверин колдойт.

Source: www.habr.com

Комментарий кошуу