.Net 向け Reliable Udp プロトコルの実装

インターネットはずっと前に変わりました。 インターネットの主要プロトコルの XNUMX つである UDP は、アプリケーションによってデータグラムやブロードキャストを配信するだけでなく、ネットワーク ノード間の「ピアツーピア」接続を提供するためにも使用されます。 このプロトコルはそのシンプルな設計により、これまで計画されていなかった用途が数多くありますが、配信保証がないなどのプロトコルの欠点は依然として消えていません。 この記事では、UDP を介した配信保証プロトコルの実装について説明します。
内容:エントリー
プロトコル要件
信頼性の高い UDP ヘッダー
プロトコルの一般原則
タイムアウトとプロトコルタイマー
信頼性の高い UDP 送信状態図
コードをさらに深く掘り下げます。 トランスミッションコントロールユニット
コードをさらに深く掘り下げます。 州

コードをさらに深く掘り下げます。 接続の作成と確立
コードをさらに深く掘り下げます。 タイムアウト時に接続を閉じる
コードをさらに深く掘り下げます。 データ転送の復元
信頼性の高い UDP API
まとめ
役立つリンクと記事

エントリー

インターネットの元のアーキテクチャは、各ノードがグローバルで一意の IP アドレスを持ち、他のノードと直接通信できる同種のアドレス空間を想定していました。 実際、インターネットは異なるアーキテクチャを持っています。グローバル IP アドレスの XNUMX つの領域と、NAT デバイスの背後に隠されたプライベート アドレスを持つ多くの領域があります。このアーキテクチャでは、グローバル アドレス空間内のデバイスだけが、グローバルにルーティング可能な一意の IP アドレスを持っているため、ネットワーク上の誰とでも簡単に通信できます。 プライベート ネットワーク上のノードは、同じネットワーク上の他のノードに接続でき、また、グローバル アドレス空間内の他の既知のノードにも接続できます。 この対話は主にネットワーク アドレス変換メカニズムによって実現されます。 Wi-Fi ルーターなどの NAT デバイスは、発信接続用の特別な変換テーブル エントリを作成し、パケット内の IP アドレスとポート番号を変更します。 これにより、プライベート ネットワークからグローバル アドレス空間内のホストへの発信接続が可能になります。 しかし同時に、受信接続用の別のルールが設定されていない限り、NAT デバイスは通常、すべての受信トラフィックをブロックします。

このインターネットのアーキテクチャは、クライアントがプライベート ネットワーク内に存在し、サーバーがグローバル アドレスを持つクライアント/サーバー通信には十分に適しています。 しかし、それは XNUMX つのノード間の直接接続に困難をもたらします。 異なる プライベートネットワーク。 XNUMX つのノード間の直接接続は、音声送信 (Skype)、コンピュータへのリモート アクセス (TeamViewer)、またはオンライン ゲームなどのピアツーピア アプリケーションにとって重要です。

異なるプライベート ネットワーク上のデバイス間でピアツーピア接続を確立するための最も効果的な方法の XNUMX つは、ホール パンチングと呼ばれます。 この手法は、UDP プロトコルに基づくアプリケーションで最も一般的に使用されます。

しかし、アプリケーションが保証されたデータ配信を必要とする場合、たとえばコンピュータ間でファイルを転送する場合、UDP は保証された配信プロトコルではなく、TCP とは異なり順序どおりにパケット配信を提供しないため、UDP を使用すると多くの困難が生じます。プロトコル。

この場合、パケット配信を保証するには、必要な機能を提供し、UDP 上で動作するアプリケーション層プロトコルを実装する必要があります。

異なるプライベート ネットワーク内のノード間で TCP 接続を確立するための TCP ホール パンチング技術があることにすぐに注意してください。しかし、多くの NAT デバイスがこの技術をサポートしていないため、通常、この技術は主要な接続方法とはみなされていません。そのようなノード。

この記事の残りの部分では、保証された配信プロトコルの実装のみに焦点を当てます。 UDP ホールパンチ技術の実装については、次の記事で説明します。

プロトコル要件

  1. 正のフィードバック メカニズム (いわゆる肯定応答) を通じて実装された信頼性の高いパケット配信
  2. ビッグデータの効率的な転送の必要性。 プロトコルは不要なパケット中継を回避する必要があります
  3. 配信確認メカニズム (「純粋な」UDP プロトコルとして機能する機能) をキャンセルできる必要があります。
  4. 各メッセージを確認するコマンドモードを実装する機能
  5. プロトコルを介したデータ転送の基本単位はメッセージである必要があります

これらの要件は、「」で説明されている信頼性データ プロトコルの要件とほぼ一致しています。 rfc 908 и rfc 1151であり、このプロトコルを開発する際にはそれらの標準に依存しました。

これらの要件を理解するために、TCP プロトコルと UDP プロトコルを使用した XNUMX つのネットワーク ノード間のデータ転送のタイミングを見てみましょう。 どちらの場合も XNUMX つのパケットが失われるとします。
TCP を介した非対話型データの転送:.Net 向け Reliable Udp プロトコルの実装

図からわかるように、パケット損失が発生した場合、TCP は損失パケットを検出し、損失セグメントの番号を問い合わせることによって送信者に報告します。
UDP プロトコルによるデータ転送:.Net 向け Reliable Udp プロトコルの実装

UDP は損失検出手順を実行しません。 UDP プロトコルでの送信エラーの制御はすべてアプリケーションの責任です。

TCP プロトコルにおけるエラー検出は、エンド ノードとの接続を確立し、その接続の状態を保存し、各パケット ヘッダーで送信されたバイト数を示し、確認応答番号を使用して受信を通知することによって実現されます。

さらに、パフォーマンスを向上させる (つまり、確認応答を受信せずに複数のセグメントを送信する) ために、TCP プロトコルはいわゆる送信ウィンドウ、つまりセグメントの送信者が受信すると予想されるデータのバイト数を使用します。

TCP プロトコルの詳細については、次を参照してください。 rfc 793、UDP から rfc 768実際、それらはここで定義されています。

上記のことから、UDP 経由で信頼性の高いメッセージ配信プロトコルを作成するには、次のことが明らかです (以下、 信頼性の高いUDP)、TCP と同様のデータ転送メカニズムを実装する必要があります。 つまり:

  • 接続状態を保存する
  • セグメント番号付けを使用する
  • 特別な確認パッケージを使用する
  • 簡素化されたウィンドウメカニズムを使用してプロトコルのスループットを向上させます

さらに、次のものが必要です。

  • メッセージの開始を通知して、接続用のリソースを割り当てます。
  • メッセージの終了を通知して、受信したメッセージを上流のアプリケーションに渡し、プロトコル リソースを解放します。
  • 接続固有のプロトコルが「純粋な」UDP として機能する配信確認メカニズムを無効にすることを許可します。

信頼性の高い UDP ヘッダー

UDP データグラムが IP データグラムにカプセル化されていることを思い出してください。 Reliable UDP パケットは、UDP データグラムに適切に「ラップ」されます。
信頼性の高い UDP ヘッダーのカプセル化:.Net 向け Reliable Udp プロトコルの実装

Reliable UDP ヘッダーの構造は非常に単純です。

.Net 向け Reliable Udp プロトコルの実装

  • フラグ - パッケージ制御フラグ
  • MessageType - 特定のメッセージをサブスクライブするためにアップストリーム アプリケーションによって使用されるメッセージ タイプ
  • TransmissionId - 送信番号と受信者のアドレスおよびポートにより、接続を一意に識別します
  • PacketNumber - パケット番号
  • オプション - 追加のプロトコル オプション。 最初のパケットの場合、メッセージのサイズを示すために使用されます。

フラグは次のとおりです。

  • FirstPacket - メッセージの最初のパケット
  • NoAsk - メッセージでは確認応答メカニズムを有効にする必要はありません。
  • LastPacket - メッセージの最後のパケット
  • RequestForPacket - 確認パケットまたは失われたパケットのリクエスト

プロトコルの一般原則

Reliable UDP は XNUMX つのノード間のメッセージ送信の保証に重点を置いているため、相手側との接続を確立できなければなりません。 接続を確立するには、送信者は FirstPacket フラグを付けたパケットを送信します。これに対する応答は、接続が確立されたことを意味します。 すべての応答パケット、つまり確認パケットは、常に、PacketNumber フィールドの値を、正常に受信されたパケットの最大の PacketNumber 値より XNUMX 大きい値に設定します。 送信される最初のパケットのオプション フィールドはメッセージのサイズです。

同様のメカニズムが接続を終了するために使用されます。 LastPacket フラグは、メッセージの最後のパケットに設定されます。 応答パケットには、最後のパケットの番号 + 1 が表示されます。これは、受信側にとってメッセージが正常に配信されたことを意味します。
接続の確立と終了の図:.Net 向け Reliable Udp プロトコルの実装

接続が確立されると、データ転送が開始されます。 データはパケットのブロックで送信されます。 最後のブロックを除く各ブロックには、固定数のパケットが含まれます。 これは、受信/送信ウィンドウ サイズと同じです。 データの最後のブロックにはパケット数が少ない場合があります。 各ブロックを送信した後、送信側は配信確認または失われたパケットの再配信要求を待ち、応答を受信するために受信/送信ウィンドウを開いたままにします。 ブロック配信の確認を受信した後、受信/送信ウィンドウが移行し、次のデータ ブロックが送信されます。

受信側はパケットを受信します。 各パケットは、送信ウィンドウ内にあるかどうかを確認するためにチェックされます。 ウィンドウに入らないパケットと重複はフィルターで除外されます。 なぜならウィンドウのサイズが固定されており、受信者と送信者で同じである場合、パケットのブロックが損失なく配信される場合、ウィンドウは次のデータ ブロックのパケットを受信するためにシフトされ、配信確認が行われます。送信済。 作業タイマーで設定した時間内にウィンドウが埋まらない場合は、どのパケットが配信されていないかのチェックが開始され、再配信要求が送信されます。
再送信図:.Net 向け Reliable Udp プロトコルの実装

タイムアウトとプロトコルタイマー

接続を確立できない理由はいくつかあります。 たとえば、受信側がオフラインの場合です。 この場合、接続を確立しようとすると、タイムアウトによって接続が切断されます。 Reliable UDP 実装では、XNUMX つのタイマーを使用してタイムアウトを設定します。 XNUMX つ目の作業タイマーは、リモート ホストからの応答を待つために使用されます。 送信側で起動すると、最後に送信されたパケットが再送信されます。 受信側でタイマーが期限切れになると、損失パケットのチェックが実行され、再配信の要求が送信されます。

XNUMX 番目のタイマーは、ノード間の通信が不足した場合に接続を閉じるために必要です。 送信側では、作業タイマーの期限が切れた直後に開始され、リモート ノードからの応答を待ちます。 指定した期間内に応答がない場合、接続は終了し、リソースは解放されます。 受信側では、ワーク タイマーが XNUMX 回期限切れになった後、接続クローズ タイマーが開始されます。 これは、確認パケットの損失を防ぐために必要です。 タイマーが期限切れになると、接続も終了し、リソースが解放されます。

信頼性の高い UDP 送信状態図

プロトコルの原理は有限状態マシンに実装されており、その各状態がパケット処理の特定のロジックを担当します。
信頼性の高い UDP 状態図:

.Net 向け Reliable Udp プロトコルの実装

閉店 - は実際には状態ではなく、オートマトンの開始点と終了点です。 状態について 閉店 送信制御ブロックが受信され、非同期 UDP サーバーを実装し、パケットを適切な接続に転送し、状態処理を開始します。

最初のパケット送信 – メッセージが送信されたときの発信接続の初期状態。

この状態で、通常のメッセージの最初のパケットが送信されます。 送信確認のないメッセージの場合、これがメッセージ全体が送信される唯一の状態です。

送信サイクル – メッセージパケットの送信のための基底状態。

州からの移行 最初のパケット送信 メッセージの最初のパケットが送信された後に実行されます。 すべての確認応答と再送信要求はこの状態で行われます。 メッセージの配信が成功した場合とタイムアウトによる場合の XNUMX つのケースで終了できます。

最初のパケットを受信しました – メッセージの受信者の初期状態。

送信の開始が正しいかどうかをチェックし、必要な構造を作成して、最初のパケットの受信確認を送信します。

単一のパケットで構成され、配信証明を使用せずに送信されたメッセージの場合、これが唯一の状態です。 このようなメッセージを処理した後、接続は閉じられます。

組み立て – メッセージパケットを受信するための基本状態。

パケットを一時ストレージに書き込み、パケット損失を確認し、パケットのブロックとメッセージ全体の配信に対する確認を送信し、失われたパケットの再配信要求を送信します。 メッセージ全体が正常に受信された場合、接続は次の状態になります。 記入済みのそうでない場合は、タイムアウトが終了します。

記入済みの – メッセージ全体を正常に受信した場合に接続を閉じます。

この状態は、メッセージの組み立てや、メッセージの配信確認が送信者に届く途中で失われた場合に必要です。 この状態はタイムアウトによって終了しますが、接続は正常に閉じられたとみなされます。

コードをさらに深く掘り下げます。 トランスミッションコントロールユニット

Reliable UDP の重要な要素の XNUMX つは、送信制御ブロックです。 このブロックのタスクは、現在の接続と補助要素を保存し、受信パケットを対応する接続​​に分散し、パケットを接続に送信するためのインターフェイスを提供し、プロトコル API を実装することです。 送信制御ブロックは、UDP 層からパケットを受信し、処理のためにステート マシンに転送します。 パケットを受信するために、非同期 UDP サーバーを実装します。
ReliableUdpConnectionControlBlock クラスの一部のメンバー:

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

非同期 UDP サーバーの実装:

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

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

メッセージ転送ごとに、接続に関する情報を含む構造が作成されます。 このような構造はと呼ばれます 接続記録.
ReliableUdpConnectionRecord クラスの一部のメンバー:

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

コードをさらに深く掘り下げます。 州

ステートは、パケットの主な処理が行われる Reliable UDP プロトコルのステート マシンを実装します。 抽象クラス ReliableUdpState は、状態のインターフェイスを提供します。

.Net 向け Reliable Udp プロトコルの実装

プロトコルのロジック全体は、上記のクラスと、接続レコードから ReliableUdp ヘッダーを構築するなどの静的メソッドを提供する補助クラスによって実装されます。

次に、プロトコルの基本アルゴリズムを決定するインターフェイス メソッドの実装について詳しく検討します。

DisposeByTimeout メソッド

DisposeByTimeout メソッドは、タイムアウト後に接続リソースを解放し、メッセージ配信の成功/失敗を通知する役割を果たします。
ReliableUdpState.DisposeByTimeout:

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

状態でのみオーバーライドされます 記入済みの.
Completed.DisposeByTimeout:

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

ProcessPackets メソッド

ProcessPackets メソッドは、XNUMX つまたは複数のパッケージの追加処理を担当します。 直接またはパケット待機タイマー経由で呼び出されます。

状態 組み立て このメソッドはオーバーライドされ、損失パケットのチェックと状態への移行を担当します。 記入済みの、最後のパケットを受信し、チェックに成功した場合
Assembling.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // если после двух попыток срабатываний WaitForPacketTimer 
    // не удалось получить пакеты - запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // успешная проверка 
  {
    // высылаем подтверждение о получении блока данных
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // вместо моментальной реализации ресурсов
    // запускаем таймер, на случай, если
    // если последний ack не дойдет до отправителя и он запросит его снова.
    // по срабатыванию таймера - реализуем ресурсы
    // в состоянии Completed метод таймера переопределен
    StartCloseWaitTimer(connectionRecord);
  }
  // это случай, когда ack на блок пакетов был потерян
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

状態 送信サイクル このメソッドはタイマーでのみ呼び出され、最後のメッセージを再送信し、接続終了タイマーを有効にする役割を果たします。
SendingCycle.ProcessPackets:

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

状態 記入済みの このメソッドは実行中のタイマーを停止し、メッセージをサブスクライバーに送信します。
Completed.ProcessPackets:

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

ReceivePacket メソッド

状態 最初のパケットを受信しました このメソッドの主なタスクは、最初のメッセージ パケットが実際にインターフェイスに到着したかどうかを判断し、単一のパケットで構成されるメッセージを収集することです。
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
      header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
    if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
    {
      // отправляем пакет подтверждение          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // если не требуется механизм подтверждение
  // запускаем таймер который высвободит все структуры         
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
  else
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

状態 送信サイクル このメソッドは、配信確認と再送信要求を受け入れるようにオーバーライドされます。
SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

状態 組み立て ReceivePacket メソッドでは、受信パケットからメッセージを組み立てる主な作業が行われます。
Assembling.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // обработка пакетов с отключенным механизмом подтверждения доставки
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // сбрасываем таймер
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // записываем данные
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // если получили пакет с последним флагом - делаем завершаем          
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // расчет конечной границы окна
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // отбрасываем не попадающие в окно пакеты
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound))
    return;
  // отбрасываем дубликаты
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // записываем данные 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // если пришел последний пакет
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // если последний пакет уже имеется        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // проверяем пакеты          
    ProcessPackets(connectionRecord);
  }
}

状態 記入済みの このメソッドの唯一のタスクは、メッセージの配信が成功したことの再確認を送信することです。
Completed.ReceivePacket:

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

パケット送信メソッド

状態 最初のパケット送信 このメソッドは、データの最初のパケットを送信するか、メッセージが配信確認を必要としない場合はメッセージ全体を送信します。
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // если подтверждения не требуется - отправляем все пакеты
  // и высвобождаем ресурсы
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Здесь происходит отправка As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

状態 送信サイクル この方法では、パケットのブロックが送信されます。
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов      
  for (connectionRecord.PacketCounter = 0;
        connectionRecord.PacketCounter < connectionRecord.WindowSize &&
        connectionRecord.SndNext < connectionRecord.NumberOfPackets;
        connectionRecord.PacketCounter++)
  {
    ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
    connectionRecord.SndNext++;
  }
  // на случай большого окна передачи, перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

コードをさらに深く掘り下げます。 接続の作成と確立

基本的な状態と状態の処理に使用されるメソッドを確認したので、プロトコルがどのように動作するかをもう少し詳しく例をいくつか挙げてみましょう。
通常の状態でのデータ伝送図:.Net 向け Reliable Udp プロトコルの実装

創造を詳しく検討する 接続記録 接続して最初のパケットを送信します。 転送は常に、メッセージ送信 API を呼び出すアプリケーションによって開始されます。 次に、送信制御ブロックの StartTransmission メソッドが呼び出され、新しいメッセージのデータの送信が開始されます。
発信接続の作成:

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException( "", "You must use constructor with parameters or start listener before sending message" );
    }
    // запускаем обработку входящих пакетов
    StartListener(LocalEndpoint);
  }
  // создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // создаем случайный номер transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // создаем новую запись для соединения и проверяем, 
  // существует ли уже такой номер в наших словарях
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // если существует – то повторно генерируем случайный номер 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // если снова не удалось – генерируем исключение
      throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
  }
  // запустили состояние в обработку         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

最初のパケットの送信 (FirstPacketSending 状態):

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  // переходим в состояние SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

最初のパケットを送信した後、送信者は次の状態に入ります。 送信サイクル – 荷物の配達の確認を待ちます。
受信側は、EndReceiveメソッドを使用して、送信されたパケットを受信し、新しいパケットを作成します 接続記録 そして、このパケットを、事前に解析されたヘッダーとともに、処理のために状態の ReceivePacket メソッドに渡します。 最初のパケットを受信しました
受信側での接続の作成:

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

最初のパケットを受信し、確認応答を送信します (FirstPacketReceived 状態):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // ...
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  // инициализируем массив для хранения частей сообщения
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // записываем данные пакет в массив
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*если не требуется механизм подтверждение*/)
  // ...
  else
  {
    // отправляем подтверждение
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

コードをさらに深く掘り下げます。 タイムアウト時に接続を閉じる

タイムアウト処理は、Reliable UDP の重要な部分です。 中間ノードに障害が発生し、双方向のデータ配信が不可能になった例を考えてみましょう。
タイムアウトによって接続を閉じる図:.Net 向け Reliable Udp プロトコルの実装

図からわかるように、送信者の作業タイマーは、パケットのブロックを送信した直後に開始されます。 これは、状態の SendPacket メソッドで発生します。 送信サイクル.
作業タイマーを有効にする (SendingCycle 状態):

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

タイマー期間は、接続の作成時に設定されます。 デフォルトの ShortTimerPeriod は 5 秒です。 例では1,5秒に設定しています。

受信接続の場合、最後の受信データ パケットを受信した後にタイマーが開始されます。これは、状態の ReceivePacket メソッドで発生します。 組み立て
作業タイマーを有効にする (組み立て状態):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // перезапускаем таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

動作タイマーの待機中に、受信接続にパケットが到着しなくなりました。 タイマーが作動し、ProcessPackets メソッドが呼び出され、失われたパケットが発見され、再配信要求が初めて送信されました。
再配達要求の送信 (組み立て状態):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*проверка на потерянные пакеты */)
  {
    // отправляем запросы на повторную доставку
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
    connectionRecord.TimerSecondTry = true;
    return;
    }
  // если после двух попыток срабатываний WaitForPacketTimer 
  // не удалось получить пакеты - запускаем таймер завершения соединения
  StartCloseWaitTimer(connectionRecord);
  }
  else if (/*пришел последний пакет и успешная проверка */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // если ack на блок пакетов был потерян
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // повторно отсылаем ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

TimerSecondTry 変数は次のように設定されます。 true。 この変数は、作業タイマーを再起動する役割を果たします。

送信側でも、作業タイマーがトリガーされ、最後に送信されたパケットが再送信されます。
接続終了タイマーを有効にする (SendingCycle 状態):

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

その後、発信接続で接続クローズ タイマーが開始されます。
ReliableUdpState.StartCloseWaitTimer:

protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
  else
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}

接続終了タイマーのタイムアウト期間は、デフォルトでは 30 秒です。

しばらくすると、受信側の作業タイマーが再び起動し、リクエストが再度送信され、その後、受信接続に対して接続クローズ タイマーが開始されます。

クローズ タイマーが起動すると、両方の接続レコードのすべてのリソースが解放されます。 送信者は配信失敗を上流アプリケーションに報告します (「信頼性の高い UDP API」を参照してください。).
接続レコードのリソースを解放します。

public void Dispose()
{
  try
  {
    System.Threading.Monitor.Enter(this.LockerReceive);
  }
  finally
  {
    Interlocked.Increment(ref this.IsDone);
    if (WaitForPacketsTimer != null)
    {
      WaitForPacketsTimer.Dispose();
    }
    if (CloseWaitTimer != null)
    {
      CloseWaitTimer.Dispose();
    }
    byte[] stream;
    Tcb.IncomingStreams.TryRemove(Key, out stream);
    stream = null;
    Tcb.OutcomingStreams.TryRemove(Key, out stream);
    stream = null;
    System.Threading.Monitor.Exit(this.LockerReceive);
  }
}

コードをさらに深く掘り下げます。 データ転送の復元

パケット損失が発生した場合のデータ送信回復図:.Net 向け Reliable Udp プロトコルの実装

タイムアウト時の接続の終了ですでに説明したように、作業タイマーが期限切れになると、受信側は損失パケットがないかチェックします。 パケット損失が発生した場合、受信者に到達しなかったパケット数のリストが作成されます。 これらの番号は特定の接続の LostPackets 配列に入力され、再配信のリクエストが送信されます。
パッケージの再配達リクエストの送信 (組み立て状態):

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

送信者は再配信要求を受け入れ、失われたパケットを送信します。 この時点で、送信者はすでに接続終了タイマーを開始しており、要求を受信するとリセットされることに注意してください。
失われたパケットの再送信 (SendingCycle 状態):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // сброс таймера закрытия соединения 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

再送信されたパケット (図のパケット #3) は、着信接続によって受信されます。 受信ウィンドウがいっぱいかどうかがチェックされ、通常のデータ送信が復元されます。
受信ウィンドウでのヒットの確認 (アセンブル状態):

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

信頼性の高い UDP API

データ転送プロトコルと対話するために、転送制御ブロックのラッパーであるオープンな Reliable Udp クラスがあります。 クラスの最も重要なメンバーは次のとおりです。

public sealed class ReliableUdp : IDisposable
{
  // получает локальную конечную точку
  public IPEndPoint LocalEndpoint    
  // создает экземпляр ReliableUdp и запускает
  // прослушивание входящих пакетов на указанном IP адресе
  // и порту. Значение 0 для порта означает использование
  // динамически выделенного порта
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // подписка на получение входящих сообщений
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // отписка от получения сообщений
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // асинхронно отправить сообщение 
  // Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
  public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // начать асинхронную отправку сообщения
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // получить результат асинхронной отправки
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // очистить ресурсы
  public void Dispose()    
}

メッセージは購読によって受信されます。 コールバック メソッドのデリゲート署名:

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

メッセージ:

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

特定のメッセージ タイプや特定の送信者をサブスクライブするには、ReliableUdpMessageTypes messageType と IPEndPoint ipEndPoint の XNUMX つのオプション パラメーターが使用されます。

メッセージの種類:

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

メッセージは非同期で送信されます。このため、プロトコルは非同期プログラミング モデルを実装しています。

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

メッセージの送信結果は、メッセージが受信者に正常に届いた場合は true、接続がタイムアウトによって閉じられた場合は false になります。

public bool EndSendMessage(IAsyncResult asyncResult)

まとめ

この記事では多くのことは説明されていません。 スレッドマッチングメカニズム、例外およびエラー処理、非同期メッセージ送信メソッドの実装。 ただし、プロトコルの核心であるパケットの処理、接続の確立、タイムアウトの処理のロジックの説明は明確であるはずです。

信頼性の高い配信プロトコルのデモ版は堅牢であり、以前に定義された要件を満たすのに十分な柔軟性を備えています。 ただし、ここで説明した実装は改善できる可能性があることを付け加えておきたいと思います。 たとえば、スループットを向上させ、タイマー期間を動的に変更するには、スライディング ウィンドウや RTT などのメカニズムをプロトコルに追加できます。また、接続ノード間の MTU を決定するメカニズムを実装することも役立ちます (ただし、大きなメッセージが送信される場合に限ります)。 。

ご清聴ありがとうございます。ご意見、ご感想をお待ちしております。

PS 詳細に興味がある場合、または単にプロトコルをテストしたい場合は、GitHube 上のプロジェクトへのリンクを参照してください。
信頼できるUDPプロジェクト

役立つリンクと記事

  1. TCPプロトコル仕様: 英語で и нарусском
  2. UDPプロトコル仕様: 英語で и нарусском
  3. RUDP プロトコルのディスカッション: ドラフト-ietf-sigtran-信頼できる-udp-00
  4. 信頼性の高いデータプロトコル: rfc 908 и rfc 1151
  5. UDP を介した配信確認の簡単な実装: .NET と UDP でネットワークを完全に制御
  6. NAT トラバーサル メカニズムについて説明した記事: ネットワークアドレス変換器を介したピアツーピア通信
  7. 非同期プログラミング モデルの実装: CLR 非同期プログラミング モデルの実装 и IAsyncResult デザイン パターンを実装する方法
  8. 非同期プログラミング モデルをタスクベースの非同期パターン (TAP の APM) に移植します。
    TPLと従来の.NET非同期プログラミング
    他の非同期パターンおよびタイプとの相互運用性

更新: ありがとう マヨロフプ и シドリスティ インターフェイスにタスクを追加するというアイデアについて。 ライブラリと古いオペレーティング システムとの互換性は損なわれていません。 4 番目のフレームワークは、XP サーバーと 2003 サーバーの両方をサポートします。

出所: habr.com

コメントを追加します