.Net үшін сенімді 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. Протокол арқылы деректерді берудің негізгі бірлігі хабарлама болуы керек

Бұл талаптар негізінен бөлімде сипатталған Сенімді деректер протоколының талаптарына сәйкес келеді rpc 908 и rpc 1151, және мен осы хаттаманы әзірлеу кезінде сол стандарттарға сүйендім.

Бұл талаптарды түсіну үшін TCP және UDP хаттамалары арқылы екі желі түйіні арасында деректерді беру уақытын қарастырайық. Екі жағдайда да бір пакет жоғалады.
TCP арқылы интерактивті емес деректерді тасымалдау:.Net үшін сенімді Udp протоколын енгізу

Диаграммадан көріп отырғанымыздай, пакет жоғалған жағдайда, TCP жоғалған пакетті анықтайды және жоғалған сегменттің нөмірін сұрау арқылы жіберушіге хабарлайды.
UDP протоколы арқылы деректерді беру:.Net үшін сенімді Udp протоколын енгізу

UDP ешбір шығынды анықтау қадамдарын жасамайды. UDP хаттамасындағы жіберу қателерін басқару толығымен қолданбаның жауапкершілігінде.

TCP хаттамасындағы қатені анықтау соңғы түйінмен байланыс орнату, осы қосылымның күйін сақтау, әрбір пакет тақырыбына жіберілген байттардың санын көрсету және растау нөмірі арқылы түбіртектерді хабарлау арқылы қол жеткізіледі.

Сонымен қатар, өнімділікті жақсарту үшін (яғни, растауды алмастан бірнеше сегментті жіберу) TCP хаттамасы сегмент жіберушісі қабылдайтын деректер байттарының саны деп аталатын жіберу терезесін пайдаланады.

TCP протоколы туралы қосымша ақпаратты қараңыз rpc 793, UDP-ден rpc 768мұнда, шын мәнінде, олар анықталады.

Жоғарыда айтылғандардан, UDP арқылы сенімді хабарды жеткізу хаттамасын жасау үшін (бұдан әрі - бұл) түсінікті. Сенімді UDP), TCP-ге ұқсас деректерді беру механизмдерін енгізу қажет. Атап айтқанда:

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

Сонымен қатар, сізге қажет:

  • байланыс үшін ресурстарды бөлу үшін хабардың басталуын білдіреді
  • хабардың аяқталуы туралы сигнал беру, қабылданған хабарды жоғары ағындық қолданбаға жіберу және протокол ресурстарын шығару
  • қосылымға арналған хаттамаға жеткізуді растау механизмін «таза» UDP ретінде жұмыс істеу үшін өшіруге мүмкіндік беріңіз

Сенімді UDP тақырыбы

Еске салайық, UDP датаграммасы IP датаграммасында инкапсулирленген. Сенімді UDP пакеті UDP датаграммасына сәйкесінше "оралған".
Сенімді UDP тақырыбы инкапсуляциясы:.Net үшін сенімді Udp протоколын енгізу

Reliable UDP тақырыбының құрылымы өте қарапайым:

.Net үшін сенімді Udp протоколын енгізу

  • Flags – буманы басқару жалаушалары
  • 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 хабарламаның бірінші пакеті жіберілгеннен кейін жүзеге асырылады. Дәл осы күйде барлық растаулар мен қайта жіберу туралы сұраулар келеді. Одан шығу екі жағдайда мүмкін болады - хабарлама сәтті жеткізілген жағдайда немесе күту уақытында.

FirstPacketReceived – хабарламаны алушының бастапқы күйі.

Ол жіберудің басталуының дұрыстығын тексереді, қажетті құрылымдарды жасайды және бірінші пакетті алғаны туралы растау жібереді.

Бір пакеттен тұратын және жеткізу дәлелін пайдаланбай жіберілген хабарлама үшін бұл жалғыз күй. Мұндай хабарламаны өңдегеннен кейін байланыс жабылады.

Құрастыру – хабарлама пакеттерін қабылдаудың негізгі күйі.

Ол пакеттерді уақытша сақтауға жазады, пакеттердің жоғалуын тексереді, пакеттер блогын және бүкіл хабарламаны жеткізу үшін растау жібереді және жоғалған пакеттерді қайта жеткізуге сұраныс жібереді. Бүкіл хабарлама сәтті алынған жағдайда байланыс күйге өтеді Аяқталған, әйтпесе күту уақыты шығады.

Аяқталған – бүкіл хабарлама сәтті алынған жағдайда қосылымды жабу.

Бұл күй хабарламаны жинақтау үшін және жіберушіге жолда хабардың жеткізілуін растау жоғалған жағдайда қажет. Бұл күй күту уақытымен шықты, бірақ қосылым сәтті жабылды деп саналады.

Кодқа тереңірек. беруді басқару блогы

Сенімді UDP негізгі элементтерінің бірі трансмиссияны басқару блогы болып табылады. Бұл блоктың міндеті ағымдағы қосылымдар мен көмекші элементтерді сақтау, кіріс пакеттерді сәйкес қосылымдарға тарату, қосылымға пакеттерді жіберу интерфейсін қамтамасыз ету және API протоколын жүзеге асыру болып табылады. Трансмиссияны басқару блогы UDP деңгейінен пакеттерді қабылдайды және оларды өңдеу үшін мемлекеттік машинаға жібереді. Пакеттерді қабылдау үшін ол асинхронды UDP серверін жүзеге асырады.
ReliableUdpConnectionControlBlock класының кейбір мүшелері:

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

Асинхронды UDP серверін енгізу:

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

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

Әрбір хабарламаны тасымалдау үшін қосылым туралы ақпаратты қамтитын құрылым жасалады. Мұндай құрылым деп аталады қосылу жазбасы.
ReliableUdpConnectionRecord класының кейбір мүшелері:

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

Кодқа тереңірек. мемлекеттер

Мемлекеттер Reliable UDP протоколының мемлекеттік машинасын іске асырады, онда пакеттердің негізгі өңдеуі жүреді. ReliableUdpState дерексіз класы күйге арналған интерфейсті қамтамасыз етеді:

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

Бұл тек штатта жойылады Аяқталған.
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 үшін сенімді 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 үшін сенімді 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 );

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. Сенімді деректер протоколы: rpc 908 и rpc 1151
  5. UDP арқылы жеткізуді растаудың қарапайым орындалуы: .NET және UDP көмегімен желіні толық бақылауға алыңыз
  6. NAT өту механизмдерін сипаттайтын мақала: Желілік мекенжай аудармашылары бойынша тең дәрежелі байланыс
  7. Асинхронды бағдарламалау моделін енгізу: CLR асинхронды бағдарламалау үлгісін енгізу и IAsyncResult дизайн үлгісін енгізу жолы
  8. Асинхронды бағдарламалау үлгісін тапсырмаға негізделген асинхронды үлгіге тасымалдау (TAP ішіндегі APM):
    TPL және дәстүрлі .NET асинхронды бағдарламалау
    Басқа асинхронды үлгілермен және түрлермен әрекеттесу

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

Ақпарат көзі: www.habr.com

пікір қалдыру