[Из песочницы] Реализация Reliable Udp протокола для .Net
Интернет давно изменился. Один из основных протоколов Интернета — UDP используется приложениям не только для доставки дейтаграмм и широковещательных рассылок, но и для обеспечения «peer-to-peer» соединений между узлами сети. Ввиду своего простого устройства, у данного протокола появилось множество не запланированных ранее способов применения, правда, недостатки протокола, такие как отсутствие гарантированной доставки, никуда при этом не исчезли. В этой статье описывается реализация протокола гарантированной доставки поверх UDP.Содержание:
Вступление Первоначальная архитектура Интернета подразумевала однородное адресное пространство, в котором каждый узел имел глобальный и уникальный IP адрес, и мог напрямую общаться с другими узлами. Сейчас Интернет, по факту, имеет другую архитектуру — одну область глобальных IP адресов и множество областей с частным адресами, скрытых за NAT устройствами.В такой архитектуре, только устройства находящиеся в глобальном адресном пространстве могут с легкостью взаимодействовать с кем-либо в сети, поскольку имеют уникальный, глобальный маршрутизируемый IP адрес. Узел, находящийся в частной сети, может соединяться с другими узлами в этой же сети, а также соединяться с другими, хорошо известными узлами в глобальном адресном пространстве. Такое взаимодействие достигается во многом благодаря механизму преобразования сетевых адресов. NAT устройства, например, Wi-Fi маршрутизаторы, создают специальные записи в таблицах трансляций для исходящих соединений и модифицируют IP адреса и номера портов в пакетах. Это позволяет устанавливать из частной сети исходящее соединение с узлами в глобальном адресном пространстве. Но в то же время, NAT устройства обычно блокируют весь входящий трафик, если не установлены отдельные правила для входящих соединений.Такая архитектура Интернета достаточно правильна для клиент-серверного взаимодействия, когда клиенты могут находиться в частных сетях, а серверы имею глобальный адрес. Но она создает трудности для прямого соединения двух узлов между различными частными сетями. Прямое соединение двух узлов важно для «peer-to-peer» приложений, таких как передача голоса (Skype), получение удаленного доступа к компьютеру (TeamViewer), или онлайн игры.
Один из наиболее эффективных методов для установления peer-to-peer соединения между устройствами находящимися в различных частных сетях называется «hole punching». Этот техника чаще всего используется с приложениями на основе UDP протокола.
Но если вашему приложению требуется гарантированная доставка данных, например, вы передаете файлы между компьютерами, то при использовании UDP появится множество трудностей, связанных с тем, что UDP не является протоколом гарантированной доставки и не обеспечивает доставку пакетов по порядку, в отличие от TCP протокола.
В таком случае, для обеспечения гарантированной доставки пакетов, требуется реализовать протокол прикладного уровня, обеспечивающий необходимую функциональность и работающий поверх UDP.
Сразу хочу заметить, что существует техника TCP hole punching, для установления TCP соединений между узлами в разных частных сетях, но ввиду отсутствия поддержки её многими NAT устройствами она обычно не рассматривается как основной способ соединения таких узлов.
Далее в этой статье я буду рассматривать только реализацию протокола гарантированной доставки. Реализация техники UDP hole punching будет описана в следующих статьях.
Требования к протоколу Надежная доставка пакетов, реализованная через механизм положительной обратной связи (так называемый positive acknowledgment) Необходимость эффективной передачи больших данных, т.е. протокол должен избегать лишних ретрансляций пакетов Должна быть возможность отмены механизма подтверждения доставки (возможность функционировать как «чистый» UDP протокол) Возможность реализации командного режима, с подтверждением каждого сообщения Базовой единицей передачи данных по протоколу должно быть сообщение Эти требования во многом совпадают с требованиями к Reliable Data Protocol, описанными в rfc 908 и rfc 1151, и я основывался на этих стандартах при разработке данного протокола.Для понимания данных требований, давайте рассмотрим временные диаграммы передачи данных между двумя узлами сети по протоколам TCP и UDP. Пусть в обоих случаях у нас будет потерян один пакет.
Передача неинтерактивных данных по TCP: Как видно из диаграммы, в случае потери пакетов, TCP обнаружит потерянный пакет и сообщит об этом отправителю, запросив номер потерянного сегмента.Передача данных по протоколу UDP: UDP не предпринимает никаких шагов по обнаружению потерь. Контроль ошибок передачи в UDP протоколе полностью возлагается на приложение.Обнаружение ошибок в TCP протоколе достигается благодаря установке соединения с конечным узлом, сохранению состояния этого соединения, указанию номера отправленных байт в каждом заголовке пакета, и уведомлениях о получениях с помощью номера подтверждения «acknowledge number».
Дополнительно, для повышения производительности (т.е. отправки более одного сегмента без получения подтверждения) TCP протокол использует так называемое окно передачи — число байт данных которые отправитель сегмента ожидает принять.
Более подробно с TCP протоколом можно ознакомиться в rfc 793, с UDP в rfc 768, где они, собственно говоря, и определены.
Из вышеописанного, понятно, что для создания надежного протокола доставки сообщений поверх UDP (в дальнейшем будем называть Reliable UDP), требуется реализовать схожие с TCP механизмы передачи данных. А именно:
сохранять состояние соединения использовать нумерацию сегментов использовать специальные пакеты подтверждения использовать упрощенный механизм окна, для увеличения пропускной способности протокола Дополнительно, требуется: сигнализировать о начале сообщения, для выделения ресурсов под соединение сигнализировать об окончании сообщения, для передачи полученного сообщения вышестоящему приложению и высвобождения ресурсов протокола позволить протоколу для конкретных соединений отключать механизм подтверждений доставки, чтобы функционировать как «чистый» UDP Заголовок Reliable UDP Вспомним, что UDP дейтаграмма инкапсулируется в IP дейтаграмму. Пакет Reliable UDP соответственно «заворачивается» в UDP дейтаграмму.Инкапсуляция заголовка Reliable UDP: Структура заголовка Reliable UDP достаточно простая:
Flags — управляющие флаги пакета MessageType — тип сообщения, используется вышестоящими приложениями, для подписки на определенные сообщения TransmissionId — номер передачи, вместе с адресом и портом получателя уникально определяет соединение PacketNumber — номер пакета Options — дополнительные опции протокола. В случае первого пакета используется для указания размера сообщения Флаги бывают следующие: FirstPacket — первый пакет сообщения NoAsk — сообщение не требует включения механизма подтверждения LastPacket — последний пакет сообщения RequestForPacket — пакет подтверждения или запрос на потерянный пакет Общие принципы работы протокола Так как Reliable UDP ориентирован на гарантированную передачу сообщения между двумя узлами, он должен уметь устанавливать соединение с другой стороной. Для установки соединения сторона-отправитель посылает пакет с флагом FirstPacket, ответ на который будет означать установку соединения. Все ответные пакеты, или, по-другому, пакеты подтверждения, всегда выставляют значение поля PacketNumber на единицу больше, чем самое большое значение PacketNumber у успешно пришедших пакетов. В поле Options для первого отправленного пакета записывается размер сообщения.Для завершения соединения используется похожий механизм. В последнем пакете сообщения устанавливается флаг LastPacket. В ответном пакете указывается номер последнего пакета + 1, что для приёмной стороны означает успешную доставку сообщения.
Диаграмма установление и завершение соединения: Когда соединение установлено, начинается передача данных. Данные передаются блоками пакетов. Каждый блок, кроме последнего, содержит фиксированное количество пакетов. Оно равно размеру окна приема/передачи. Последний блок данных может иметь меньшее количество пакетов. После отправки каждого блока, сторона-отправитель ожидает подтверждения о доставке, либо запроса на повторную доставку потерянных пакетов, оставляя открытым окно приема/передачи для получения ответов. После получения подтверждения о доставке блока, окно прием/передачи сдвигается и отправляется следующий блок данных.Сторона-получатель принимает пакеты. Каждый пакет проверяется на попадание в окно передачи. Не попадающие в окно пакеты и дубликаты отсеиваются. Т.к. размер окна сторого фиксирован и одинаков у получателя и у отправителя, то в случае доставки блока пакетов без потерь, окно сдвигается для приема пакетов следующего блока данных и отправляется подтверждение о доставке. Если окно не заполнится за установленный рабочим таймером период, то будет запущена проверка на то, какие пакеты не были доставлены и будут отправлены запросы на повторную доставку.
Диаграмма повторной передачи: Тайм-ауты и таймеры протокола Существует несколько причин, по которым не может быть установлено соединение. Например, если принимающая сторона вне сети. В таком случае, при попытке установить соединение, соединение будет закрыто по тайм-ауту. В реализации Reliable UDP используются два таймера для установки тайм-аутов. Первый, рабочий таймер, служит для ожидания ответа от удаленного хоста. Если он срабатывает на стороне-отправителе, то выполняется повторная отправка последнего отправленного пакета. Если же таймер срабатывает у получателя, то выполняется проверка на потерянные пакеты и отправляются запросы на повторную доставку.Второй таймер — необходим для закрытия соединения в случае отсутствия связи между узлами. Для стороны-отправителя он запускается сразу после срабатывания рабочего таймера, и ожидает ответа от удаленного узла. В случае отсутствия ответа за установленный период — соединение завершается и ресурсы освобождаются. Для стороны-получателя, таймер закрытия соединения запускается после двойного срабатывания рабочего таймера. Это необходимо для страховки от потери пакета подтверждения. При срабатывании таймера, также завершается соединение и высвобождаются ресурсы.
Диаграмма состояний передачи Reliable UDP Принципы работы протокола реализованы в конечном автомате, каждое состояние которого отвечает за определенную логику обработки пакетов.Диаграмма состояний Reliable UDP:
Closed — в действительности не является состоянием, это стартовая и конечная точка для автомата. За состояние Closed принимается блок управления передачей, который, реализуя асинхронный UDP сервер, перенаправляет пакеты в соответствующие соединения и запускает обработку состояний.
FirstPacketSending — начальное состояние, в котором находится исходящее соединение при отправке сообщения.
В этом состоянии отправляется первый пакет для обычных сообщений. Для сообщений без подтверждения отправки, это единственное состояние — в нем происходит отправка всего сообщения.
SendingCycle — основное состояния для передачи пакетов сообщения.
Переход в него из состояния FirstPacketSending осуществляется после отправки первого пакета сообщения. Именно в это состояние приходят все подтверждения и запросы на повторные передачи. Выход из него возможен в двух случаях — в случае успешной доставки сообщения или по тайм-ауту.
FirstPacketReceived — начальное состояние для получателя сообщения.
В нем проверяется корректность начала передачи, создаются необходимые структуры, и отправляется подтверждение о приеме первого пакета.
Для сообщения, состоящего из единственного пакета и отправленного без использования подтверждения доставки — это единственное состояние. После обработки такого сообщения соединение закрывается.
Assembling — основное состояние для приема пакетов сообщения.
В нем производится запись пакетов во временное хранилище, проверка на отсутствие потерь пакетов, отправка подтверждений о доставке блока пакетов и сообщения целиком, и отправка запросов на повторную доставку потерянных пакетов. В случае успешного получения всего сообщения — соединение переходит в состояние Completed, иначе выполняется выход по тайм-ауту.
Completed — закрытие соединения в случае успешного получения всего сообщения.
Данное состояние необходимо для сборки сообщения и для случая, когда подтверждение о доставке сообщения было потеряно по пути к отправителю. Выход из этого состояния производится по тайм-ауту, но соединение считается успешно закрытым.
Глубже в код. Блок управления передачей
Один из ключевых элементов Reliable UDP — блок управления передачей. Задача данного блока — хранение текущих соединений и вспомогательных элементов, распределение пришедших пакетов по соответствующим соединениям, предоставление интерфейса для отправки пакетов соединению и реализация API протокола. Блок управления передачей принимает пакеты от UDP уровня и перенаправляет их на обработку в конечный автомат. Для приема пакетов в нем реализован асинхронный UDP сервер.Некоторые члены класса ReliableUdpConnectionControlBlock:
internal class ReliableUdpConnectionControlBlock: IDisposable
{
// массив байт для указанного ключа. Используется для сборки входящих сообщений
public ConcurrentDictionary
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
Всю логику работы протокола реализуют представленные выше классы, совместно со вспомогательным классом, предоставляющим статические методы, такие как, например, построения заголовка ReliableUdp из connection record.
Далее будут рассмотрены в подробностях реализации методов интерфейса, определяющих основные алгоритмы работы протокола.
Метод 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.Completed.DisposeByTimeout: protected override void DisposeByTimeout (object record) { ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record; // сообщаем об успешном получении сообщения SetAsCompleted (connectionRecord); } Метод ProcessPackets Метод ProcessPackets отвечает за дополнительную обработку пакета или пакетов. Вызывается напрямую, либо через таймер ожидания пакетов.В состоянии Assembling метод переопределен и отвечает за проверку потерянных пакетов и переход в состояние Completed, в случае получения последнего пакета и прохождения успешной проверки
Assembling.ProcessPackets:
public override void ProcessPackets (ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone!= 0)
return;
if (! ReliableUdpStateTools.CheckForNoPacketLoss (connectionRecord, connectionRecord.IsLastPacketReceived!= 0))
{
// есть потерянные пакеты, отсылаем запросы на них
foreach (int seqNum in connectionRecord.LostPackets)
{
if (seqNum!= 0)
{
ReliableUdpStateTools.SendAskForLostPacket (connectionRecord, seqNum);
}
}
// устанавливаем таймер во второй раз, для повторной попытки передачи
if (! connectionRecord.TimerSecondTry)
{
connectionRecord.WaitForPacketsTimer.Change (connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// если после двух попыток срабатываний WaitForPacketTimer
// не удалось получить пакеты — запускаем таймер завершения соединения
StartCloseWaitTimer (connectionRecord);
}
else if (connectionRecord.IsLastPacketReceived!= 0)
// успешная проверка
{
// высылаем подтверждение о получении блока данных
ReliableUdpStateTools.SendAcknowledgePacket (connectionRecord);
connectionRecord.State = connectionRecord.Tcb.States.Completed;
connectionRecord.State.ProcessPackets (connectionRecord);
// вместо моментальной реализации ресурсов
// запускаем таймер, на случай, если
// если последний ack не дойдет до отправителя и он запросит его снова.
// по срабатыванию таймера — реализуем ресурсы
// в состоянии Completed метод таймера переопределен
StartCloseWaitTimer (connectionRecord);
}
// это случай, когда ack на блок пакетов был потерян
else
{
if (! connectionRecord.TimerSecondTry)
{
ReliableUdpStateTools.SendAcknowledgePacket (connectionRecord);
connectionRecord.WaitForPacketsTimer.Change (connectionRecord.ShortTimerPeriod, -1);
connectionRecord.TimerSecondTry = true;
return;
}
// запускаем таймер завершения соединения
StartCloseWaitTimer (connectionRecord);
}
}
В состоянии SendingCycle этот метод вызывается только по таймеру, и отвечает за повторную отправку последнего сообщения, а также за включение таймера закрытия соединения.SendingCycle.ProcessPackets:
public override void ProcessPackets (ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.IsDone!= 0)
return;
// отправляем повторно последний пакет
// (в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)
ReliableUdpStateTools.SendPacket (connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload (connectionRecord, connectionRecord.SndNext — 1));
// включаем таймер CloseWait — для ожидания восстановления соединения или его завершения
StartCloseWaitTimer (connectionRecord);
}
В состоянии Completed метод останавливает рабочий таймер и передает сообщение подписчикам.Completed.ProcessPackets:
public override void ProcessPackets (ReliableUdpConnectionRecord connectionRecord)
{
if (connectionRecord.WaitForPacketsTimer!= null)
connectionRecord.WaitForPacketsTimer.Dispose ();
// собираем сообщение и передаем его подписчикам
ReliableUdpStateTools.CreateMessageFromMemoryStream (connectionRecord);
}
Метод ReceivePacket
В состоянии FirstPacketReceived основная задача метода — определить действительно ли первый пакет сообщения пришел на интерфейс, а также собрать сообщение состоящее из единственного пакета.FirstPacketReceived.ReceivePacket:
public override void ReceivePacket (ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (! header.Flags.HasFlag (ReliableUdpHeaderFlags.FirstPacket))
// отбрасываем пакет
return;
// комбинация двух флагов — FirstPacket и LastPacket — говорит что у нас единственное сообщение
if (header.Flags.HasFlag (ReliableUdpHeaderFlags.FirstPacket) &
header.Flags.HasFlag (ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.CreateMessageFromSinglePacket (connectionRecord, header, payload.Slice (ReliableUdpHeader.Length, payload.Length));
if (! header.Flags.HasFlag (ReliableUdpHeaderFlags.NoAsk))
{
// отправляем пакет подтверждение
ReliableUdpStateTools.SendAcknowledgePacket (connectionRecord);
}
SetAsCompleted (connectionRecord);
return;
}
// by design все packet numbers начинаются с 0;
if (header.PacketNumber!= 0)
return;
ReliableUdpStateTools.InitIncomingBytesStorage (connectionRecord, header);
ReliableUdpStateTools.WritePacketData (connectionRecord, header, payload);
// считаем кол-во пакетов, которые должны прийти
connectionRecord.NumberOfPackets = (int)Math.Ceiling ((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
// записываем номер последнего полученного пакета (0)
connectionRecord.RcvCurrent = header.PacketNumber;
// после сдвинули окно приема на 1
connectionRecord.WindowLowerBound++;
// переключаем состояние
connectionRecord.State = connectionRecord.Tcb.States.Assembling;
// если не требуется механизм подтверждение
// запускаем таймер который высвободит все структуры
if (header.Flags.HasFlag (ReliableUdpHeaderFlags.NoAsk))
{
connectionRecord.CloseWaitTimer = new Timer (DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
else
{
ReliableUdpStateTools.SendAcknowledgePacket (connectionRecord);
connectionRecord.WaitForPacketsTimer = new Timer (CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
}
В состоянии SendingCycle этот метод переопределен для приема подтверждений о доставке и запросов повторной передачи.SendingCycle.ReceivePacket:
public override void ReceivePacket (ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
if (connectionRecord.IsDone!= 0)
return;
if (! header.Flags.HasFlag (ReliableUdpHeaderFlags.RequestForPacket))
return;
// расчет конечной границы окна
// берется граница окна + 1, для получения подтверждений доставки
int windowHighestBound = Math.Min ((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
// проверка на попадание в окно
if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
return;
connectionRecord.WaitForPacketsTimer.Change (connectionRecord.ShortTimerPeriod, -1);
if (connectionRecord.CloseWaitTimer!= null)
connectionRecord.CloseWaitTimer.Change (-1, -1);
// проверить на последний пакет:
if (header.PacketNumber == connectionRecord.NumberOfPackets)
{
// передача завершена
Interlocked.Increment (ref connectionRecord.IsDone);
SetAsCompleted (connectionRecord);
return;
}
// это ответ на первый пакет c подтверждением
if ((header.Flags.HasFlag (ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
{
// без сдвига окна
SendPacket (connectionRecord);
}
// пришло подтверждение о получении блока данных
else if (header.PacketNumber == windowHighestBound)
{
// сдвигаем окно прием/передачи
connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
// обнуляем массив контроля передачи
connectionRecord.WindowControlArray.Nullify ();
// отправляем блок пакетов
SendPacket (connectionRecord);
}
// это запрос на повторную передачу — отправляем требуемый пакет
else
ReliableUdpStateTools.SendPacket (connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload (connectionRecord, header.PacketNumber));
}
В состоянии Assembling в методе 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 единственная задача метода — отослать повторное подтверждение об успешной доставке сообщения.Completed.ReceivePacket:
public override void ReceivePacket (ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
// повторная отправка последнего пакета в связи с тем,
// что последний ack не дошел до отправителя
if (header.Flags.HasFlag (ReliableUdpHeaderFlags.LastPacket))
{
ReliableUdpStateTools.SendAcknowledgePacket (connectionRecord);
}
}
Метод SendPacket
В состоянии FirstPacketSending этот метод осуществляет отправку первого пакета данных, или, если сообщение не требует подтверждение доставки — все сообщение.FirstPacketSending.SendPacket:
public override void SendPacket (ReliableUdpConnectionRecord connectionRecord)
{
connectionRecord.PacketCounter = 0;
connectionRecord.SndNext = 0;
connectionRecord.WindowLowerBound = 0;
// если подтверждения не требуется — отправляем все пакеты
// и высвобождаем ресурсы
if (connectionRecord.IsNoAnswerNeeded)
{
// Здесь происходит отправка As Is
do
{
ReliableUdpStateTools.SendPacket (connectionRecord, ReliableUdpStateTools.CreateUdpPayload (connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader (connectionRecord)));
connectionRecord.SndNext++;
} while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
SetAsCompleted(connectionRecord);
return;
}
// создаем заголовок пакета и отправляем его
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
// увеличиваем счетчик
connectionRecord.SndNext++;
// сдвигаем окно
connectionRecord.WindowLowerBound++;
connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
// Запускаем таймер
connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}
В состоянии SendingCycle в этом методе происходит отправка блока пакетов.SendingCycle.SendPacket:
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
// отправляем блок пакетов
for (connectionRecord.PacketCounter = 0;
connectionRecord.PacketCounter < connectionRecord.WindowSize &&
connectionRecord.SndNext < connectionRecord.NumberOfPackets;
connectionRecord.PacketCounter++)
{
ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
connectionRecord.SndNext++;
}
// на случай большого окна передачи, перезапускаем таймер после отправки
connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
if ( connectionRecord.CloseWaitTimer != null )
{
connectionRecord.CloseWaitTimer.Change( -1, -1 );
}
}
Глубже в код. Создание и установление соединений
Теперь, когда мы познакомились с основными состояниями и методами, используемыми для обработки состояний, можно разобрать немного подробнее несколько примеров работы протокола.Диаграмма передачи данных в нормальных условиях:
Рассмотрим подробно создание connection record для соединения и отправку первого пакета. Инициатором передачи всегда выступает приложение, вызывающее 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
Включение рабочего таймера (состояние Assembling): 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, в котором были обнаружены потерянные пакеты и первый раз отправлены запросы на повторную доставку.Отправка запросов на повторную доставку (состояние Assembling): 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 (connection