Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

Internet telah lama berubah. Salah satu protokol utama Internet - UDP digunakan oleh aplikasi bukan sahaja untuk menyampaikan datagram dan siaran, tetapi juga untuk menyediakan sambungan "peer-to-peer" antara nod rangkaian. Oleh kerana reka bentuknya yang ringkas, protokol ini mempunyai banyak kegunaan yang tidak dirancang sebelum ini, namun kekurangan protokol, seperti kekurangan penghantaran yang terjamin, tidak hilang di mana-mana. Artikel ini menerangkan pelaksanaan protokol penghantaran terjamin ke atas UDP.
Kandungan:Entry
Keperluan Protokol
Pengepala UDP yang boleh dipercayai
Prinsip umum protokol
Masa tamat dan pemasa protokol
Rajah keadaan penghantaran UDP yang boleh dipercayai
Lebih mendalam ke dalam kod. unit kawalan penghantaran
Lebih mendalam ke dalam kod. negeri

Lebih mendalam ke dalam kod. Mencipta dan Mewujudkan Sambungan
Lebih mendalam ke dalam kod. Menutup sambungan apabila tamat masa
Lebih mendalam ke dalam kod. Memulihkan pemindahan data
API UDP yang boleh dipercayai
Kesimpulan
Pautan dan artikel yang berguna

Entry

Seni bina asal Internet menganggap ruang alamat homogen di mana setiap nod mempunyai alamat IP global dan unik dan boleh berkomunikasi secara langsung dengan nod lain. Kini Internet, sebenarnya, mempunyai seni bina yang berbeza - satu kawasan alamat IP global dan banyak kawasan dengan alamat peribadi tersembunyi di sebalik peranti NAT.Dalam seni bina ini, hanya peranti dalam ruang alamat global boleh berkomunikasi dengan mudah dengan sesiapa sahaja di rangkaian kerana mereka mempunyai alamat IP yang unik dan boleh dialihkan secara global. Nod pada rangkaian persendirian boleh menyambung ke nod lain pada rangkaian yang sama, dan juga boleh menyambung ke nod lain yang terkenal dalam ruang alamat global. Interaksi ini dicapai sebahagian besarnya disebabkan oleh mekanisme terjemahan alamat rangkaian. Peranti NAT, seperti penghala Wi-Fi, mencipta entri jadual terjemahan khas untuk sambungan keluar dan mengubah suai alamat IP dan nombor port dalam paket. Ini membolehkan sambungan keluar dari rangkaian peribadi ke hos dalam ruang alamat global. Tetapi pada masa yang sama, peranti NAT biasanya menyekat semua trafik masuk melainkan peraturan berasingan untuk sambungan masuk ditetapkan.

Seni bina Internet ini cukup betul untuk komunikasi pelanggan-pelayan, di mana pelanggan boleh berada dalam rangkaian peribadi, dan pelayan mempunyai alamat global. Tetapi ia mewujudkan kesukaran untuk sambungan langsung dua nod antara pelbagai rangkaian peribadi. Sambungan terus antara dua nod adalah penting untuk aplikasi peer-to-peer seperti penghantaran suara (Skype), mendapatkan akses jauh ke komputer (TeamViewer), atau permainan dalam talian.

Salah satu kaedah paling berkesan untuk mewujudkan sambungan peer-to-peer antara peranti pada rangkaian peribadi yang berbeza dipanggil penebuk lubang. Teknik ini paling biasa digunakan dengan aplikasi berdasarkan protokol UDP.

Tetapi jika aplikasi anda memerlukan penghantaran data yang terjamin, contohnya, anda memindahkan fail antara komputer, maka menggunakan UDP akan menghadapi banyak kesukaran kerana fakta bahawa UDP bukanlah protokol penghantaran yang terjamin dan tidak menyediakan penghantaran paket secara teratur, tidak seperti TCP protokol.

Dalam kes ini, untuk memastikan penghantaran paket terjamin, ia diperlukan untuk melaksanakan protokol lapisan aplikasi yang menyediakan fungsi yang diperlukan dan berfungsi melalui UDP.

Saya ingin ambil perhatian segera bahawa terdapat teknik penebuk lubang TCP untuk mewujudkan sambungan TCP antara nod dalam rangkaian peribadi yang berbeza, tetapi disebabkan kekurangan sokongan untuknya oleh banyak peranti NAT, ia biasanya tidak dianggap sebagai cara utama untuk menyambung nod sedemikian.

Untuk baki artikel ini, saya hanya akan menumpukan pada pelaksanaan protokol penghantaran terjamin. Pelaksanaan teknik tebuk lubang UDP akan diterangkan dalam artikel berikut.

Keperluan Protokol

  1. Penghantaran paket yang boleh dipercayai dilaksanakan melalui mekanisme maklum balas positif (yang dipanggil pengakuan positif )
  2. Keperluan untuk pemindahan data besar yang cekap, i.e. protokol mesti mengelakkan penyampaian paket yang tidak perlu
  3. Mekanisme pengesahan penghantaran mungkin boleh dibatalkan (keupayaan untuk berfungsi sebagai protokol UDP "tulen")
  4. Keupayaan untuk melaksanakan mod arahan, dengan pengesahan setiap mesej
  5. Unit asas pemindahan data melalui protokol mestilah mesej

Keperluan ini sebahagian besarnya bertepatan dengan keperluan Protokol Data Boleh Dipercayai yang diterangkan dalam rfc908 и rfc1151, dan saya bergantung pada piawaian tersebut semasa membangunkan protokol ini.

Untuk memahami keperluan ini, mari kita lihat masa pemindahan data antara dua nod rangkaian menggunakan protokol TCP dan UDP. Biarkan dalam kedua-dua kes kita akan kehilangan satu paket.
Pemindahan data bukan interaktif melalui TCP:Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

Seperti yang anda lihat dari rajah, sekiranya paket kehilangan, TCP akan mengesan paket yang hilang dan melaporkannya kepada penghantar dengan meminta nombor segmen yang hilang.
Pemindahan data melalui protokol UDP:Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

UDP tidak mengambil sebarang langkah pengesanan kerugian. Kawalan ralat penghantaran dalam protokol UDP adalah tanggungjawab aplikasi sepenuhnya.

Pengesanan ralat dalam protokol TCP dicapai dengan mewujudkan sambungan dengan nod akhir, menyimpan keadaan sambungan itu, menunjukkan bilangan bait yang dihantar dalam setiap pengepala paket, dan memberitahu resit menggunakan nombor pengakuan.

Selain itu, untuk meningkatkan prestasi (iaitu menghantar lebih daripada satu segmen tanpa menerima pengakuan), protokol TCP menggunakan apa yang dipanggil tetingkap penghantaran - bilangan bait data yang dijangka diterima oleh pengirim segmen.

Untuk maklumat lanjut tentang protokol TCP, lihat rfc793, daripada UDP kepada rfc768di mana, sebenarnya, mereka ditakrifkan.

Daripada perkara di atas, adalah jelas bahawa untuk mencipta protokol penghantaran mesej yang boleh dipercayai melalui UDP (selepas ini dirujuk sebagai UDP yang boleh dipercayai), ia diperlukan untuk melaksanakan mekanisme pemindahan data yang serupa dengan TCP. Iaitu:

  • simpan keadaan sambungan
  • gunakan penomboran segmen
  • gunakan pakej pengesahan khas
  • gunakan mekanisme tetingkap yang dipermudahkan untuk meningkatkan pemprosesan protokol

Selain itu, anda memerlukan:

  • menandakan permulaan mesej, untuk memperuntukkan sumber untuk sambungan
  • menandakan penghujung mesej, untuk menghantar mesej yang diterima kepada aplikasi huluan dan mengeluarkan sumber protokol
  • benarkan protokol khusus sambungan untuk melumpuhkan mekanisme pengesahan penghantaran untuk berfungsi sebagai UDP "tulen".

Pengepala UDP yang boleh dipercayai

Ingat bahawa datagram UDP dikapsulkan dalam datagram IP. Paket UDP yang Boleh Dipercayai sesuai "dibalut" ke dalam datagram UDP.
Enkapsulasi pengepala UDP yang boleh dipercayai:Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

Struktur pengepala UDP yang Boleh Dipercayai agak mudah:

Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

  • Bendera - bendera kawalan pakej
  • MessageType - jenis mesej yang digunakan oleh aplikasi huluan untuk melanggan mesej tertentu
  • TransmissionId - nombor penghantaran, bersama-sama dengan alamat dan port penerima, mengenal pasti sambungan secara unik
  • PacketNumber - nombor paket
  • Pilihan - pilihan protokol tambahan. Dalam kes paket pertama, ia digunakan untuk menunjukkan saiz mesej

Bendera adalah seperti berikut:

  • FirstPacket - paket pertama mesej
  • NoAsk - mesej tidak memerlukan mekanisme pengakuan untuk didayakan
  • LastPacket - paket terakhir mesej
  • RequestForPacket - paket pengesahan atau permintaan untuk paket yang hilang

Prinsip umum protokol

Memandangkan UDP yang Boleh Dipercayai tertumpu pada penghantaran mesej yang dijamin antara dua nod, ia mesti dapat mewujudkan sambungan dengan pihak lain. Untuk mewujudkan sambungan, pengirim menghantar paket dengan bendera FirstPacket, tindak balas yang bermaksud sambungan telah diwujudkan. Semua paket respons, atau, dengan kata lain, paket pengakuan, sentiasa menetapkan nilai medan PacketNumber kepada satu lebih daripada nilai PacketNumber terbesar bagi paket yang berjaya diterima. Medan Pilihan untuk paket pertama yang dihantar ialah saiz mesej.

Mekanisme serupa digunakan untuk menamatkan sambungan. Bendera LastPacket ditetapkan pada paket terakhir mesej. Dalam paket tindak balas, nombor paket terakhir + 1 ditunjukkan, yang bagi bahagian penerima bermakna penghantaran mesej yang berjaya.
Gambarajah penubuhan sambungan dan penamatan:Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

Apabila sambungan diwujudkan, pemindahan data bermula. Data dihantar dalam blok paket. Setiap blok, kecuali yang terakhir, mengandungi bilangan paket yang tetap. Ia sama dengan saiz tetingkap terima/hantar. Blok terakhir data mungkin mempunyai lebih sedikit paket. Selepas menghantar setiap blok, pihak penghantar menunggu pengesahan penghantaran atau permintaan untuk menghantar semula paket yang hilang, meninggalkan tetingkap terima/hantar terbuka untuk menerima respons. Selepas menerima pengesahan penghantaran blok, tetingkap terima/hantar beralih dan blok data seterusnya dihantar.

Bahagian penerima menerima paket. Setiap paket disemak untuk melihat sama ada ia berada dalam tetingkap penghantaran. Paket dan pendua yang tidak jatuh ke dalam tetingkap ditapis keluar. Kerana Jika saiz tetingkap ditetapkan dan sama untuk penerima dan pengirim, maka dalam kes blok paket dihantar tanpa kehilangan, tetingkap dialihkan untuk menerima paket blok data seterusnya dan pengesahan penghantaran adalah dihantar. Jika tetingkap tidak terisi dalam tempoh yang ditetapkan oleh pemasa kerja, maka semakan akan dimulakan pada paket yang belum dihantar dan permintaan untuk penghantaran semula akan dihantar.
Rajah penghantaran semula:Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

Masa tamat dan pemasa protokol

Terdapat beberapa sebab mengapa sambungan tidak dapat diwujudkan. Contohnya, jika pihak yang menerima berada di luar talian. Dalam kes ini, apabila cuba mewujudkan sambungan, sambungan akan ditutup dengan tamat masa. Pelaksanaan UDP yang Boleh Dipercayai menggunakan dua pemasa untuk menetapkan tamat masa. Yang pertama, pemasa yang berfungsi, digunakan untuk menunggu respons daripada hos jauh. Jika ia menyala di sebelah penghantar, maka paket terakhir dihantar dihantar semula. Jika pemasa tamat tempoh pada penerima, maka semakan untuk paket yang hilang dilakukan dan permintaan untuk penghantaran semula dihantar.

Pemasa kedua diperlukan untuk menutup sambungan sekiranya berlaku kekurangan komunikasi antara nod. Untuk bahagian penghantar, ia bermula serta-merta selepas pemasa yang berfungsi tamat tempoh, dan menunggu respons daripada nod jauh. Jika tiada tindak balas dalam tempoh yang ditetapkan, sambungan ditamatkan dan sumber dikeluarkan. Untuk bahagian penerima, pemasa tutup sambungan dimulakan selepas pemasa kerja tamat tempoh dua kali. Ini adalah perlu untuk menginsuranskan terhadap kehilangan paket pengesahan. Apabila pemasa tamat tempoh, sambungan juga ditamatkan dan sumber dikeluarkan.

Rajah keadaan penghantaran UDP yang boleh dipercayai

Prinsip protokol dilaksanakan dalam mesin keadaan terhingga, setiap keadaan bertanggungjawab untuk logik tertentu pemprosesan paket.
Rajah Keadaan UDP yang Boleh Dipercayai:

Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

Tutup - sebenarnya bukan keadaan, ia adalah titik permulaan dan penamat untuk automaton. Untuk negeri Tutup blok kawalan penghantaran diterima, yang, melaksanakan pelayan UDP tak segerak, memajukan paket ke sambungan yang sesuai dan memulakan pemprosesan keadaan.

FirstPacketSending – keadaan awal di mana sambungan keluar adalah apabila mesej dihantar.

Dalam keadaan ini, paket pertama untuk mesej biasa dihantar. Untuk mesej tanpa pengesahan penghantaran, ini adalah satu-satunya negeri di mana keseluruhan mesej dihantar.

SendingCycle – keadaan asas untuk penghantaran paket mesej.

Peralihan kepadanya dari negeri FirstPacketSending dijalankan selepas paket pertama mesej dihantar. Dalam keadaan inilah semua pengakuan dan permintaan untuk penghantaran semula datang. Keluar daripadanya mungkin dalam dua kes - sekiranya mesej berjaya dihantar atau tamat masa.

FirstPacketReceived – keadaan awal untuk penerima mesej.

Ia menyemak ketepatan permulaan penghantaran, mencipta struktur yang diperlukan, dan menghantar pengakuan penerimaan paket pertama.

Untuk mesej yang terdiri daripada satu paket dan dihantar tanpa menggunakan bukti penghantaran, ini adalah satu-satunya keadaan. Selepas memproses mesej sedemikian, sambungan ditutup.

Memasang – keadaan asas untuk menerima paket mesej.

Ia menulis paket ke storan sementara, menyemak kehilangan paket, menghantar pengakuan untuk penghantaran blok paket dan keseluruhan mesej, dan menghantar permintaan untuk penghantaran semula paket yang hilang. Sekiranya berjaya menerima keseluruhan mesej, sambungan masuk ke negeri Siap, jika tidak, tamat masa akan tamat.

Siap – menutup sambungan sekiranya keseluruhan mesej berjaya diterima.

Keadaan ini diperlukan untuk pemasangan mesej dan untuk kes apabila pengesahan penghantaran mesej hilang dalam perjalanan ke pengirim. Keadaan ini keluar dengan tamat masa, tetapi sambungan dianggap berjaya ditutup.

Lebih mendalam ke dalam kod. unit kawalan penghantaran

Salah satu elemen utama UDP Boleh Dipercayai ialah blok kawalan penghantaran. Tugas blok ini adalah untuk menyimpan sambungan semasa dan elemen tambahan, mengedarkan paket masuk ke sambungan yang sepadan, menyediakan antara muka untuk menghantar paket ke sambungan, dan melaksanakan API protokol. Blok kawalan penghantaran menerima paket dari lapisan UDP dan memajukannya ke mesin keadaan untuk diproses. Untuk menerima paket, ia melaksanakan pelayan UDP tak segerak.
Beberapa ahli kelas 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;    	
  //...
}

Pelaksanaan pelayan UDP tak segerak:

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

Untuk setiap pemindahan mesej, struktur dicipta yang mengandungi maklumat tentang sambungan. Struktur sedemikian dipanggil rekod sambungan.
Beberapa ahli kelas 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;
  //...
}

Lebih mendalam ke dalam kod. negeri

Negara melaksanakan mesin keadaan protokol UDP Dipercayai, di mana pemprosesan utama paket berlaku. Kelas abstrak ReliableUdpState menyediakan antara muka untuk keadaan:

Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

Keseluruhan logik protokol dilaksanakan oleh kelas yang dibentangkan di atas, bersama-sama dengan kelas tambahan yang menyediakan kaedah statik, seperti, sebagai contoh, membina pengepala ReliableUdp daripada rekod sambungan.

Seterusnya, kami akan mempertimbangkan secara terperinci pelaksanaan kaedah antara muka yang menentukan algoritma asas protokol.

Kaedah DisposeByTimeout

Kaedah DisposeByTimeout bertanggungjawab untuk melepaskan sumber sambungan selepas tamat masa dan untuk menandakan penghantaran mesej berjaya/tidak berjaya.
ReliableUdpState.DisposeByTimeout:

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

Ia hanya ditindih di negeri ini Siap.
Selesai.BuangByTimeout:

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

Kaedah ProcessPackets

Kaedah ProcessPackets bertanggungjawab untuk pemprosesan tambahan bagi pakej atau pakej. Dipanggil terus atau melalui pemasa tunggu paket.

Dalam keadaan Memasang kaedah itu ditindih dan bertanggungjawab untuk menyemak paket yang hilang dan beralih ke keadaan Siap, sekiranya menerima paket terakhir dan lulus semakan yang berjaya
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);
  }
}

Dalam keadaan SendingCycle kaedah ini dipanggil hanya pada pemasa, dan bertanggungjawab untuk menghantar semula mesej terakhir, serta mendayakan pemasa tutup sambungan.
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);
}

Dalam keadaan Siap kaedah menghentikan pemasa berjalan dan menghantar mesej kepada pelanggan.
Selesai.ProsesPaket:

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

Kaedah ReceivePacket

Dalam keadaan FirstPacketReceived tugas utama kaedah ini adalah untuk menentukan sama ada paket mesej pertama benar-benar tiba di antara muka, dan juga untuk mengumpul mesej yang terdiri daripada satu paket.
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);
  }
}

Dalam keadaan SendingCycle kaedah ini ditindih untuk menerima pengakuan penghantaran dan permintaan penghantaran semula.
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));
}

Dalam keadaan Memasang dalam kaedah ReceivePacket, kerja utama untuk memasang mesej daripada paket masuk berlaku.
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);
  }
}

Dalam keadaan Siap satu-satunya tugas kaedah ini adalah untuk menghantar pengakuan semula tentang kejayaan penghantaran mesej.
Selesai.ReceivePacket:

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

Kaedah Hantar Paket

Dalam keadaan FirstPacketSending kaedah ini menghantar paket pertama data, atau, jika mesej tidak memerlukan pengesahan penghantaran, keseluruhan mesej.
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);
}

Dalam keadaan SendingCycle dalam kaedah ini, satu blok paket dihantar.
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 );
  }
}

Lebih mendalam ke dalam kod. Mencipta dan Mewujudkan Sambungan

Memandangkan kita telah melihat keadaan asas dan kaedah yang digunakan untuk mengendalikan keadaan, mari kita pecahkan beberapa contoh cara protokol berfungsi dengan lebih terperinci.
Gambar rajah penghantaran data dalam keadaan biasa:Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

Pertimbangkan secara terperinci penciptaan rekod sambungan untuk menyambung dan menghantar paket pertama. Pemindahan sentiasa dimulakan oleh aplikasi yang memanggil API hantar mesej. Seterusnya, kaedah StartTransmission bagi blok kawalan penghantaran digunakan, yang memulakan penghantaran data untuk mesej baharu.
Mencipta sambungan keluar:

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

Menghantar paket pertama (keadaan 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);
}

Selepas menghantar paket pertama, penghantar memasuki negeri SendingCycle – tunggu pengesahan penghantaran bungkusan.
Bahagian penerima, menggunakan kaedah EndReceive, menerima paket yang dihantar, mencipta yang baharu rekod sambungan dan luluskan paket ini, dengan pengepala yang telah dihuraikan, kepada kaedah ReceivePacket bagi keadaan untuk diproses FirstPacketReceived
Mencipta sambungan pada bahagian penerima:

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

Menerima paket pertama dan menghantar pengakuan (keadaan 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);
  }
}

Lebih mendalam ke dalam kod. Menutup sambungan apabila tamat masa

Pengendalian tamat masa ialah bahagian penting UDP Boleh Dipercayai. Pertimbangkan contoh di mana nod perantaraan gagal dan penghantaran data dalam kedua-dua arah menjadi mustahil.
Gambar rajah untuk menutup sambungan dengan tamat masa:Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

Seperti yang dapat dilihat daripada rajah, pemasa kerja penghantar bermula serta-merta selepas menghantar satu blok paket. Ini berlaku dalam kaedah SendPacket negeri SendingCycle.
Mendayakan pemasa kerja (keadaan SendingCycle):

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

Tempoh pemasa ditetapkan apabila sambungan dibuat. ShortTimerPeriod lalai ialah 5 saat. Dalam contoh, ia ditetapkan kepada 1,5 saat.

Untuk sambungan masuk, pemasa bermula selepas menerima paket data masuk terakhir, ini berlaku dalam kaedah ReceivePacket keadaan Memasang
Mendayakan pemasa kerja (Keadaan pemasangan):

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

Tiada lagi paket tiba pada sambungan masuk sementara menunggu pemasa berfungsi. Pemasa dimatikan dan memanggil kaedah ProcessPackets, di mana paket yang hilang ditemui dan permintaan penghantaran semula dihantar buat kali pertama.
Menghantar permintaan penghantaran semula (Keadaan pemasangan):

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

Pembolehubah TimerSecondTry ditetapkan kepada benar. Pembolehubah ini bertanggungjawab untuk memulakan semula pemasa yang berfungsi.

Di pihak penghantar, pemasa yang berfungsi juga dicetuskan dan paket terakhir dihantar dihantar semula.
Mendayakan pemasa tutup sambungan (keadaan SendingCycle):

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

Selepas itu, pemasa tutup sambungan bermula dalam sambungan keluar.
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);
}

Tempoh tamat pemasa tutup sambungan ialah 30 saat secara lalai.

Selepas masa yang singkat, pemasa yang berfungsi di sisi penerima menyala semula, permintaan dihantar semula, selepas itu pemasa tutup sambungan bermula untuk sambungan masuk

Apabila pemasa tutup menyala, semua sumber kedua-dua rekod sambungan dikeluarkan. Pengirim melaporkan kegagalan penghantaran kepada aplikasi huluan (lihat API UDP yang Boleh Dipercayai).
Mengeluarkan sumber rekod sambungan:

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

Lebih mendalam ke dalam kod. Memulihkan pemindahan data

Gambar rajah pemulihan penghantaran data sekiranya paket hilang:Pelaksanaan protokol Udp Boleh Dipercayai untuk .Net

Seperti yang telah dibincangkan dalam menutup sambungan pada tamat masa, apabila pemasa yang berfungsi tamat tempoh, penerima akan menyemak paket yang hilang. Dalam kes kehilangan paket, senarai bilangan paket yang tidak sampai kepada penerima akan disusun. Nombor-nombor ini dimasukkan ke dalam tatasusunan LostPackets bagi sambungan tertentu, dan permintaan untuk penghantaran semula dihantar.
Menghantar permintaan untuk menghantar semula pakej (Keadaan pemasangan):

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

Pengirim akan menerima permintaan penghantaran semula dan menghantar paket yang hilang. Perlu diingat bahawa pada masa ini pengirim telah pun memulakan pemasa tutup sambungan dan, apabila permintaan diterima, ia ditetapkan semula.
Menghantar semula paket yang hilang (keadaan 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));
}

Paket yang dihantar semula (paket #3 dalam rajah) diterima oleh sambungan masuk. Semakan dibuat untuk melihat sama ada tetingkap terima penuh dan penghantaran data normal dipulihkan.
Menyemak hits dalam tetingkap terima (Keadaan pemasangan):

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

API UDP yang boleh dipercayai

Untuk berinteraksi dengan protokol pemindahan data, terdapat kelas Udp Boleh Dipercayai terbuka, yang merupakan pembalut di atas blok kawalan pemindahan. Berikut ialah ahli kelas yang paling penting:

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

Mesej diterima melalui langganan. Wakilkan tandatangan untuk kaedah panggil balik:

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

Mesej:

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

Untuk melanggan jenis mesej tertentu dan/atau penghantar tertentu, dua parameter pilihan digunakan: ReliableUdpMessageTypes messageType dan IPEndPoint ipEndPoint.

Jenis mesej:

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

Mesej dihantar secara tak segerak; untuk ini, protokol melaksanakan model pengaturcaraan tak segerak:

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

Hasil penghantaran mesej adalah benar - jika mesej berjaya sampai ke penerima dan palsu - jika sambungan ditutup oleh tamat masa:

public bool EndSendMessage(IAsyncResult asyncResult)

Kesimpulan

Banyak yang belum diterangkan dalam artikel ini. Mekanisme pemadanan benang, pengecualian dan pengendalian ralat, pelaksanaan kaedah penghantaran mesej tak segerak. Tetapi teras protokol, perihalan logik untuk memproses paket, mewujudkan sambungan, dan mengendalikan tamat masa, harus jelas kepada anda.

Versi yang ditunjukkan bagi protokol penghantaran yang boleh dipercayai adalah teguh dan cukup fleksibel untuk memenuhi keperluan yang ditetapkan sebelum ini. Tetapi saya ingin menambah bahawa pelaksanaan yang diterangkan boleh diperbaiki. Contohnya, untuk meningkatkan daya pemprosesan dan menukar tempoh pemasa secara dinamik, mekanisme seperti tetingkap gelongsor dan RTT boleh ditambah pada protokol, ia juga berguna untuk melaksanakan mekanisme untuk menentukan MTU antara nod sambungan (tetapi hanya jika mesej besar dihantar) .

Terima kasih atas perhatian anda, saya mengharapkan komen dan komen anda.

PS Bagi mereka yang berminat dengan butiran atau hanya ingin menguji protokol, pautan ke projek di GitHube:
Projek UDP yang boleh dipercayai

Pautan dan artikel yang berguna

  1. Spesifikasi protokol TCP: dalam bahasa inggeris и dalam bahasa Rusia
  2. Spesifikasi protokol UDP: dalam bahasa inggeris и dalam bahasa Rusia
  3. Perbincangan protokol RUDP: draf-ietf-sigtran-dipercayai-udp-00
  4. Protokol Data Boleh Dipercayai: rfc908 и rfc1151
  5. Pelaksanaan mudah pengesahan penghantaran melalui UDP: Ambil Kawalan Penuh Rangkaian Anda Dengan .NET Dan UDP
  6. Artikel yang menerangkan mekanisme lintasan NAT: Komunikasi Peer-to-Peer Merentas Penterjemah Alamat Rangkaian
  7. Pelaksanaan model pengaturcaraan tak segerak: Melaksanakan Model Pengaturcaraan Asynchronous CLR и Bagaimana untuk melaksanakan corak reka bentuk IAsyncResult
  8. Memindahkan model pengaturcaraan tak segerak ke corak tak segerak berasaskan tugas (APM dalam TAP):
    TPL dan Pengaturcaraan Asynchronous .NET Tradisional
    Interop dengan Corak dan Jenis Asynchronous Lain

Kemas kini: Terima kasih mayorovp и sidristij untuk idea menambah tugasan pada antara muka. Keserasian perpustakaan dengan sistem pengendalian lama tidak dilanggar, kerana Rangka kerja ke-4 menyokong pelayan XP dan 2003.

Sumber: www.habr.com

Tambah komen