Implementasi protokol Udp yang Andal untuk .Net

Internet telah berubah sejak lama. Salah satu protokol utama Internet - UDP digunakan oleh aplikasi tidak hanya untuk mengirimkan datagram dan siaran, tetapi juga untuk menyediakan koneksi "peer-to-peer" antar node jaringan. Karena desainnya yang sederhana, protokol ini memiliki banyak kegunaan yang sebelumnya tidak direncanakan, namun kekurangan protokol, seperti kurangnya jaminan pengiriman, tidak hilang kemana-mana. Artikel ini menjelaskan penerapan protokol pengiriman terjamin melalui UDP.
Isi:Masuk
Persyaratan Protokol
Tajuk UDP yang andal
Prinsip umum protokol
Timeout dan timer protokol
Diagram status transmisi UDP yang andal
Lebih dalam ke kode. unit kontrol transmisi
Lebih dalam ke kode. negara bagian

Lebih dalam ke kode. Membuat dan Membangun Koneksi
Lebih dalam ke kode. Menutup koneksi pada batas waktu
Lebih dalam ke kode. Memulihkan transfer data
API UDP yang andal
Kesimpulan
Tautan dan artikel yang bermanfaat

Masuk

Arsitektur asli Internet mengasumsikan ruang alamat homogen di mana setiap node memiliki alamat IP global dan unik dan dapat berkomunikasi langsung dengan node lain. Sekarang Internet sebenarnya memiliki arsitektur yang berbeda - satu area alamat IP global dan banyak area dengan alamat pribadi yang tersembunyi di balik perangkat NAT.Dalam arsitektur ini, hanya perangkat di ruang alamat global yang dapat dengan mudah berkomunikasi dengan siapa pun di jaringan karena mereka memiliki alamat IP yang unik dan dapat dirutekan secara global. Sebuah node di jaringan pribadi dapat terhubung ke node lain di jaringan yang sama, dan juga dapat terhubung ke node terkenal lainnya di ruang alamat global. Interaksi ini dicapai sebagian besar karena mekanisme terjemahan alamat jaringan. Perangkat NAT, seperti router Wi-Fi, membuat entri tabel terjemahan khusus untuk koneksi keluar dan mengubah alamat IP dan nomor port dalam paket. Ini memungkinkan koneksi keluar dari jaringan pribadi ke host di ruang alamat global. Tetapi pada saat yang sama, perangkat NAT biasanya memblokir semua lalu lintas masuk kecuali aturan terpisah untuk koneksi masuk ditetapkan.

Arsitektur Internet ini cukup tepat untuk komunikasi klien-server, di mana klien dapat berada di jaringan pribadi, dan server memiliki alamat global. Tapi itu menciptakan kesulitan untuk koneksi langsung antara dua node bermacam-macam jaringan pribadi. Koneksi langsung antara dua node penting untuk aplikasi peer-to-peer seperti transmisi suara (Skype), mendapatkan akses jarak jauh ke komputer (TeamViewer), atau game online.

Salah satu metode paling efektif untuk membuat koneksi peer-to-peer antar perangkat di jaringan pribadi yang berbeda disebut pelubangan lubang. Teknik ini paling umum digunakan dengan aplikasi berbasis protokol UDP.

Tetapi jika aplikasi Anda membutuhkan jaminan pengiriman data, misalnya Anda mentransfer file antar komputer, maka menggunakan UDP akan mengalami banyak kesulitan karena UDP bukan protokol pengiriman yang dijamin dan tidak menyediakan pengiriman paket secara berurutan, tidak seperti TCP protokol.

Dalam hal ini, untuk memastikan pengiriman paket yang terjamin, diperlukan penerapan protokol lapisan aplikasi yang menyediakan fungsionalitas yang diperlukan dan bekerja melalui UDP.

Saya ingin segera mencatat bahwa ada teknik meninju lubang TCP untuk membuat koneksi TCP antar node di jaringan pribadi yang berbeda, tetapi karena kurangnya dukungan untuk itu oleh banyak perangkat NAT, biasanya tidak dianggap sebagai cara utama untuk terhubung node seperti itu.

Untuk sisa artikel ini, saya hanya akan fokus pada penerapan protokol pengiriman terjamin. Penerapan teknik hole punching UDP akan dijelaskan pada artikel berikut.

Persyaratan Protokol

  1. Pengiriman paket yang andal diimplementasikan melalui mekanisme umpan balik positif (yang disebut pengakuan positif)
  2. Kebutuhan untuk transfer data besar yang efisien, mis. protokol harus menghindari penyampaian paket yang tidak perlu
  3. Harus dimungkinkan untuk membatalkan mekanisme konfirmasi pengiriman (kemampuan untuk berfungsi sebagai protokol UDP "murni")
  4. Kemampuan untuk mengimplementasikan mode perintah, dengan konfirmasi setiap pesan
  5. Unit dasar transfer data melalui protokol harus berupa pesan

Persyaratan ini sebagian besar sesuai dengan persyaratan Reliable Data Protocol yang dijelaskan di rfc908 и rfc1151, dan saya mengandalkan standar tersebut saat mengembangkan protokol ini.

Untuk memahami persyaratan ini, mari kita lihat waktu transfer data antara dua node jaringan menggunakan protokol TCP dan UDP. Biarkan dalam kedua kasus kita akan kehilangan satu paket.
Transfer data non-interaktif melalui TCP:Implementasi protokol Udp yang Andal untuk .Net

Seperti yang Anda lihat dari diagram, jika terjadi kehilangan paket, TCP akan mendeteksi paket yang hilang dan melaporkannya ke pengirim dengan menanyakan nomor segmen yang hilang.
Transfer data melalui protokol UDP:Implementasi protokol Udp yang Andal untuk .Net

UDP tidak melakukan langkah deteksi kerugian apa pun. Pengendalian kesalahan transmisi dalam protokol UDP sepenuhnya menjadi tanggung jawab aplikasi.

Deteksi kesalahan dalam protokol TCP dicapai dengan membuat koneksi dengan simpul akhir, menyimpan status koneksi itu, menunjukkan jumlah byte yang dikirim di setiap header paket, dan memberi tahu penerimaan menggunakan nomor pengakuan.

Selain itu, untuk meningkatkan kinerja (yaitu mengirim lebih dari satu segmen tanpa menerima pengakuan), protokol TCP menggunakan apa yang disebut jendela transmisi - jumlah byte data yang diharapkan diterima oleh pengirim segmen.

Untuk informasi lebih lanjut tentang protokol TCP, lihat rfc793, dari UDP ke rfc768di mana, pada kenyataannya, mereka didefinisikan.

Dari penjelasan di atas, jelaslah bahwa untuk membuat protokol pengiriman pesan yang andal melalui UDP (selanjutnya disebut sebagai UDP yang andal), diperlukan untuk mengimplementasikan mekanisme transfer data yang mirip dengan TCP. Yaitu:

  • menyimpan status koneksi
  • menggunakan penomoran segmen
  • gunakan paket konfirmasi khusus
  • gunakan mekanisme windowing yang disederhanakan untuk meningkatkan throughput protokol

Selain itu, Anda memerlukan:

  • sinyal dimulainya pesan, untuk mengalokasikan sumber daya untuk koneksi
  • menandakan akhir pesan, untuk meneruskan pesan yang diterima ke aplikasi upstream dan melepaskan sumber daya protokol
  • izinkan protokol khusus koneksi untuk menonaktifkan mekanisme konfirmasi pengiriman agar berfungsi sebagai UDP "murni".

Tajuk UDP yang andal

Ingatlah bahwa datagram UDP dienkapsulasi dalam datagram IP. Paket UDP yang Andal "dibungkus" dengan tepat ke dalam datagram UDP.
Enkapsulasi header UDP yang andal:Implementasi protokol Udp yang Andal untuk .Net

Struktur header Reliable UDP cukup sederhana:

Implementasi protokol Udp yang Andal untuk .Net

  • Bendera - bendera kontrol paket
  • MessageType - tipe pesan yang digunakan oleh aplikasi upstream untuk berlangganan pesan tertentu
  • TransmissionId - nomor transmisi, bersama dengan alamat dan port penerima, mengidentifikasi koneksi secara unik
  • PacketNumber - nomor paket
  • Opsi - opsi protokol tambahan. Dalam kasus paket pertama, ini digunakan untuk menunjukkan ukuran pesan

Bendera adalah sebagai berikut:

  • FirstPacket - paket pertama dari pesan
  • NoAsk - pesan tidak memerlukan mekanisme pengakuan untuk diaktifkan
  • LastPacket - paket terakhir dari pesan
  • RequestForPacket - paket konfirmasi atau permintaan paket yang hilang

Prinsip umum protokol

Karena Reliable UDP difokuskan pada pengiriman pesan yang dijamin antara dua node, ia harus dapat membuat koneksi dengan pihak lain. Untuk membuat koneksi, pengirim mengirim paket dengan bendera FirstPacket, respons yang berarti koneksi dibuat. Semua paket respons, atau, dengan kata lain, paket pengakuan, selalu menyetel nilai bidang PacketNumber menjadi satu lebih dari nilai PacketNumber terbesar dari paket yang berhasil diterima. Bidang Opsi untuk paket pertama yang dikirim adalah ukuran pesan.

Mekanisme serupa digunakan untuk mengakhiri koneksi. Bendera LastPacket diatur pada paket terakhir pesan. Dalam paket respons, nomor paket terakhir + 1 ditunjukkan, yang bagi pihak penerima berarti pengiriman pesan berhasil.
Pembuatan koneksi dan diagram pemutusan:Implementasi protokol Udp yang Andal untuk .Net

Ketika koneksi dibuat, transfer data dimulai. Data ditransmisikan dalam blok paket. Setiap blok, kecuali yang terakhir, berisi sejumlah paket tetap. Ini sama dengan ukuran jendela terima/kirim. Blok data terakhir mungkin memiliki lebih sedikit paket. Setelah mengirim setiap blok, pihak pengirim menunggu konfirmasi pengiriman atau permintaan untuk mengirimkan kembali paket yang hilang, membiarkan jendela terima/kirim terbuka untuk menerima tanggapan. Setelah menerima konfirmasi pengiriman blok, jendela terima/kirim bergeser dan blok data berikutnya dikirim.

Sisi penerima menerima paket. Setiap paket diperiksa untuk melihat apakah itu termasuk dalam jendela transmisi. Paket dan duplikat yang tidak masuk ke jendela akan disaring. Karena Jika ukuran jendela tetap dan sama untuk penerima dan pengirim, maka dalam kasus blok paket yang dikirim tanpa kehilangan, jendela digeser untuk menerima paket dari blok data berikutnya dan konfirmasi pengiriman adalah terkirim. Jika jendela tidak terisi dalam periode yang ditentukan oleh pengatur waktu kerja, maka pemeriksaan akan dimulai pada paket mana yang belum terkirim dan permintaan pengiriman ulang akan dikirim.
Diagram transmisi ulang:Implementasi protokol Udp yang Andal untuk .Net

Timeout dan timer protokol

Ada beberapa alasan mengapa koneksi tidak dapat dibuat. Misalnya, jika pihak penerima sedang offline. Dalam hal ini, saat mencoba membuat koneksi, koneksi akan ditutup oleh batas waktu. Implementasi UDP yang andal menggunakan dua pengatur waktu untuk mengatur waktu tunggu. Yang pertama, pengatur waktu yang berfungsi, digunakan untuk menunggu respons dari host jarak jauh. Jika menyala di sisi pengirim, maka paket yang terakhir dikirim dikirim ulang. Jika timer kedaluwarsa pada penerima, maka pemeriksaan paket yang hilang dilakukan dan permintaan pengiriman ulang dikirim.

Timer kedua diperlukan untuk menutup koneksi jika terjadi kurangnya komunikasi antar node. Untuk sisi pengirim, ini dimulai segera setelah timer kerja berakhir, dan menunggu respons dari node jarak jauh. Jika tidak ada respons dalam jangka waktu yang ditentukan, koneksi diakhiri dan sumber daya dilepaskan. Untuk sisi penerima, timer tutup koneksi dimulai setelah timer kerja habis dua kali. Ini diperlukan untuk memastikan hilangnya paket konfirmasi. Saat penghitung waktu berakhir, koneksi juga diakhiri dan sumber daya dilepaskan.

Diagram status transmisi UDP yang andal

Prinsip-prinsip protokol diimplementasikan dalam mesin keadaan terbatas, yang masing-masing keadaannya bertanggung jawab atas logika pemrosesan paket tertentu.
Diagram Status UDP yang Andal:

Implementasi protokol Udp yang Andal untuk .Net

Tertutup - sebenarnya bukan keadaan, ini adalah titik awal dan akhir untuk otomat. Untuk negara Tertutup blok kontrol transmisi diterima, yang mengimplementasikan server UDP asinkron, meneruskan paket ke koneksi yang sesuai dan memulai pemrosesan status.

Pengiriman Paket Pertama – keadaan awal di mana koneksi keluar adalah saat pesan dikirim.

Dalam keadaan ini, paket pertama untuk pesan normal dikirim. Untuk pesan tanpa konfirmasi pengiriman, ini adalah satu-satunya status tempat seluruh pesan terkirim.

Siklus Pengiriman – keadaan dasar untuk transmisi paket pesan.

Transisi ke sana dari negara bagian Pengiriman Paket Pertama dilakukan setelah paket pertama dari pesan telah dikirim. Dalam keadaan inilah semua ucapan terima kasih dan permintaan untuk pengiriman ulang datang. Keluar darinya dimungkinkan dalam dua kasus - jika pengiriman pesan berhasil atau dengan batas waktu.

Paket Pertama Diterima – keadaan awal untuk penerima pesan.

Ini memeriksa kebenaran awal transmisi, membuat struktur yang diperlukan, dan mengirimkan pemberitahuan penerimaan paket pertama.

Untuk pesan yang terdiri dari satu paket dan dikirim tanpa menggunakan bukti pengiriman, ini adalah satu-satunya status. Setelah memproses pesan seperti itu, koneksi ditutup.

Perakitan – keadaan dasar untuk menerima paket pesan.

Itu menulis paket ke penyimpanan sementara, memeriksa kehilangan paket, mengirimkan pemberitahuan untuk pengiriman blok paket dan seluruh pesan, dan mengirimkan permintaan untuk pengiriman ulang paket yang hilang. Jika berhasil menerima seluruh pesan, koneksi masuk ke status Lengkap, jika tidak, batas waktu akan keluar.

Lengkap – menutup koneksi jika berhasil menerima seluruh pesan.

Status ini diperlukan untuk menyusun pesan dan untuk kasus ketika konfirmasi pengiriman pesan hilang dalam perjalanan ke pengirim. Status ini diakhiri dengan batas waktu, tetapi koneksi dianggap berhasil ditutup.

Lebih dalam ke kode. unit kontrol transmisi

Salah satu elemen kunci dari Reliable UDP adalah blok kontrol transmisi. Tugas blok ini adalah untuk menyimpan koneksi saat ini dan elemen tambahan, mendistribusikan paket yang masuk ke koneksi yang sesuai, menyediakan antarmuka untuk mengirim paket ke koneksi, dan mengimplementasikan API protokol. Blok kontrol transmisi menerima paket dari lapisan UDP dan meneruskannya ke mesin negara untuk diproses. Untuk menerima paket, ini mengimplementasikan server UDP asinkron.
Beberapa anggota 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;    	
  //...
}

Implementasi server UDP asinkron:

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 transfer pesan, dibuat struktur yang berisi informasi tentang koneksi. Struktur seperti itu disebut catatan koneksi.
Beberapa anggota 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 dalam ke kode. negara bagian

Negara menerapkan mesin negara dari protokol UDP yang andal, tempat pemrosesan utama paket berlangsung. Kelas abstrak ReliableUdpState menyediakan antarmuka untuk status:

Implementasi protokol Udp yang Andal untuk .Net

Seluruh logika protokol diimplementasikan oleh kelas yang disajikan di atas, bersama dengan kelas tambahan yang menyediakan metode statis, seperti, misalnya, membuat header ReliableUdp dari catatan koneksi.

Selanjutnya, kami akan mempertimbangkan secara rinci penerapan metode antarmuka yang menentukan algoritme dasar protokol.

Metode DisposeByTimeout

Metode DisposeByTimeout bertanggung jawab untuk melepaskan sumber daya koneksi setelah waktu habis dan untuk menandakan pengiriman pesan yang berhasil/tidak berhasil.
ReliableUdpState.DisposeByTimeout:

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

Itu hanya diganti di negara bagian Lengkap.
Selesai.Buang Dengan Waktu Habis:

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

metode ProcessPackets

Metode ProcessPackets bertanggung jawab untuk pemrosesan tambahan paket atau paket. Dipanggil secara langsung atau melalui pengatur waktu tunggu paket.

Sanggup Perakitan metode ini diganti dan bertanggung jawab untuk memeriksa paket yang hilang dan beralih ke status Lengkap, dalam hal menerima paket terakhir dan melewati pemeriksaan yang berhasil
Perakitan.Paket Proses:

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

Sanggup Siklus Pengiriman metode ini dipanggil hanya pada pengatur waktu, dan bertanggung jawab untuk mengirim ulang pesan terakhir, serta mengaktifkan koneksi tutup pengatur waktu.
MengirimCycle.ProcessPaket:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // отправляем повторно последний пакет 
  // ( в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

Sanggup Lengkap metode menghentikan timer yang sedang berjalan dan mengirimkan pesan ke pelanggan.
Selesai.Paket Proses:

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

Metode Terima Paket

Sanggup Paket Pertama Diterima tugas utama metode ini adalah untuk menentukan apakah paket pesan pertama benar-benar tiba di antarmuka, dan juga mengumpulkan pesan yang terdiri dari 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);
  }
}

Sanggup Siklus Pengiriman metode ini diganti untuk menerima pengakuan pengiriman dan permintaan pengiriman ulang.
MengirimCycle.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));
}

Sanggup Perakitan dalam metode ReceivePacket, pekerjaan utama menyusun pesan dari paket yang masuk berlangsung.
Perakitan.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);
  }
}

Sanggup Lengkap satu-satunya tugas dari metode ini adalah mengirimkan pemberitahuan ulang atas keberhasilan pengiriman pesan.
Selesai.ReceivePacket:

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

Metode Kirim Paket

Sanggup Pengiriman Paket Pertama metode ini mengirimkan paket data pertama, atau jika pesan tidak memerlukan konfirmasi pengiriman, seluruh pesan.
Pengiriman Paket Pertama.Kirim Paket:

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

Sanggup Siklus Pengiriman dalam metode ini, satu blok paket dikirim.
MengirimCycle.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 dalam ke kode. Membuat dan Membangun Koneksi

Sekarang setelah kita melihat status dasar dan metode yang digunakan untuk menangani status, mari kita uraikan beberapa contoh cara kerja protokol dengan sedikit lebih mendetail.
Diagram transmisi data dalam kondisi normal:Implementasi protokol Udp yang Andal untuk .Net

Pertimbangkan secara detail pembuatannya catatan koneksi untuk menghubungkan dan mengirim paket pertama. Transfer selalu dimulai oleh aplikasi yang memanggil API kirim pesan. Selanjutnya, metode StartTransmission dari blok kontrol transmisi dipanggil, yang memulai transmisi data untuk pesan baru.
Membuat koneksi 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]);
}

Mengirim paket pertama (status 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);
}

Setelah mengirim paket pertama, pengirim memasuki status Siklus Pengiriman – tunggu konfirmasi pengiriman paket.
Sisi penerima, menggunakan metode EndReceive, menerima paket yang dikirim, membuat yang baru catatan koneksi dan meneruskan paket ini, dengan header yang telah diurai sebelumnya, ke metode ReceivePacket dari status untuk diproses Paket Pertama Diterima
Membuat koneksi di sisi 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 mengirim pemberitahuan (status 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 dalam ke kode. Menutup koneksi pada batas waktu

Penanganan batas waktu adalah bagian penting dari UDP yang Andal. Pertimbangkan contoh di mana node perantara gagal dan pengiriman data di kedua arah menjadi tidak mungkin.
Diagram untuk menutup koneksi dengan batas waktu:Implementasi protokol Udp yang Andal untuk .Net

Seperti dapat dilihat dari diagram, pengatur waktu kerja pengirim dimulai segera setelah mengirim satu blok paket. Ini terjadi dalam metode SendPacket negara bagian Siklus Pengiriman.
Mengaktifkan timer kerja (SendingCycle state):

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

Periode pengatur waktu diatur saat koneksi dibuat. ShortTimerPeriod default adalah 5 detik. Dalam contoh, disetel ke 1,5 detik.

Untuk koneksi masuk, pengatur waktu dimulai setelah menerima paket data terakhir yang masuk, ini terjadi dalam metode ReceivePacket negara bagian Perakitan
Mengaktifkan pengatur waktu kerja (Merakit status):

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

Tidak ada lagi paket yang sampai pada koneksi masuk sambil menunggu pengatur waktu yang berfungsi. Timer berbunyi dan memanggil metode ProcessPackets, di mana paket yang hilang ditemukan dan permintaan pengiriman ulang dikirim untuk pertama kalinya.
Mengirim permintaan pengiriman ulang (Merakit status):

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

Variabel TimerSecondTry diatur ke benar. Variabel ini bertanggung jawab untuk memulai kembali pengatur waktu yang berfungsi.

Di sisi pengirim, pengatur waktu kerja juga dipicu dan paket yang terakhir dikirim dikirim ulang.
Mengaktifkan timer tutup koneksi (status SendCycle):

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

Setelah itu, pengatur waktu tutup koneksi dimulai di koneksi keluar.
UdpState.StartCloseWaitTimer yang Andal:

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

Periode batas waktu tutup koneksi adalah 30 detik secara default.

Setelah beberapa saat, pengatur waktu yang berfungsi di sisi penerima menyala lagi, permintaan dikirim lagi, setelah itu pengatur waktu tutup koneksi dimulai untuk koneksi yang masuk

Saat penghitung waktu tutup aktif, semua sumber daya dari kedua rekaman koneksi dilepaskan. Pengirim melaporkan kegagalan pengiriman ke aplikasi upstream (lihat API UDP yang Andal).
Melepaskan resource rekaman koneksi:

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 dalam ke kode. Memulihkan transfer data

Diagram pemulihan transmisi data jika terjadi kehilangan paket:Implementasi protokol Udp yang Andal untuk .Net

Seperti yang sudah dibahas dalam menutup koneksi pada timeout, ketika timer kerja habis, penerima akan memeriksa paket yang hilang. Jika terjadi kehilangan paket, daftar jumlah paket yang tidak sampai ke penerima akan dikompilasi. Angka-angka ini dimasukkan ke dalam array LostPackets dari koneksi tertentu, dan permintaan pengiriman ulang dikirim.
Mengirim permintaan untuk mengirim ulang paket (Assembling state):

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 pengiriman ulang dan mengirim paket yang hilang. Perlu dicatat bahwa saat ini pengirim telah memulai koneksi, tutup pengatur waktu dan, ketika permintaan diterima, itu disetel ulang.
Mengirim ulang paket yang hilang (SendingCycle state):

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 dikirim ulang (paket #3 dalam diagram) diterima oleh koneksi masuk. Pemeriksaan dilakukan untuk melihat apakah jendela penerima penuh dan transmisi data normal dipulihkan.
Memeriksa hit di jendela terima (Assembling state):

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 andal

Untuk berinteraksi dengan protokol transfer data, ada kelas Udp Reliable terbuka, yang merupakan pembungkus blok kontrol transfer. Berikut adalah anggota 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()    
}

Pesan diterima dengan berlangganan. Tanda tangan delegasi untuk metode callback:

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

Pesan:

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

Untuk berlangganan jenis pesan tertentu dan/atau pengirim tertentu, dua parameter opsional digunakan: ReliableUdpMessageTypes messageType dan IPEndPoint ipEndPoint.

Jenis pesan:

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

Pesan dikirim secara asinkron; untuk ini, protokol mengimplementasikan model pemrograman asinkron:

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

Hasil pengiriman pesan akan benar - jika pesan berhasil sampai ke penerima dan salah - jika koneksi ditutup saat waktu habis:

public bool EndSendMessage(IAsyncResult asyncResult)

Kesimpulan

Banyak yang belum dijelaskan dalam artikel ini. Mekanisme pencocokan utas, pengecualian dan penanganan kesalahan, penerapan metode pengiriman pesan asinkron. Tetapi inti dari protokol, deskripsi logika untuk memproses paket, membuat koneksi, dan menangani timeout, harus jelas bagi Anda.

Versi yang didemonstrasikan dari protokol pengiriman andal cukup kuat dan fleksibel untuk memenuhi persyaratan yang ditentukan sebelumnya. Tetapi saya ingin menambahkan bahwa implementasi yang dijelaskan dapat ditingkatkan. Misalnya, untuk meningkatkan throughput dan secara dinamis mengubah periode pengatur waktu, mekanisme seperti jendela geser dan RTT dapat ditambahkan ke protokol, juga akan berguna untuk mengimplementasikan mekanisme untuk menentukan MTU antar node koneksi (tetapi hanya jika pesan besar dikirim) .

Terima kasih atas perhatiannya, saya tunggu kritik dan sarannya.

PS Bagi mereka yang tertarik dengan detailnya atau hanya ingin menguji protokolnya, tautan ke proyek di GitHube:
Proyek UDP yang andal

Tautan dan artikel yang bermanfaat

  1. Spesifikasi protokol TCP: dalam bahasa inggris и dalam bahasa Rusia
  2. Spesifikasi protokol UDP: dalam bahasa inggris и dalam bahasa Rusia
  3. Pembahasan protokol RUDP: draft-ietf-sigtran-dapat diandalkan-udp-00
  4. Protokol Data yang Andal: rfc908 и rfc1151
  5. Implementasi sederhana dari konfirmasi pengiriman melalui UDP: Ambil Kendali Total Jaringan Anda Dengan .NET Dan UDP
  6. Artikel yang menjelaskan mekanisme traversal NAT: Komunikasi Peer-to-Peer di Penerjemah Alamat Jaringan
  7. Implementasi model pemrograman asinkron: Menerapkan Model Pemrograman Asinkron CLR и Cara menerapkan pola desain IAsyncResult
  8. Mem-porting model pemrograman asinkron ke pola asinkron berbasis tugas (APM di TAP):
    TPL dan Pemrograman Asinkron .NET Tradisional
    Interop dengan Pola dan Jenis Asinkron Lainnya

Pembaruan: Terima kasih walikotaovp и sidristij untuk ide menambahkan tugas ke antarmuka. Kompatibilitas perpustakaan dengan sistem operasi lama tidak dilanggar, karena Framework ke-4 mendukung server XP dan 2003.

Sumber: www.habr.com

Tambah komentar