Интернет давно изменился. Один из основных протоколов Интернета – UDP используется приложениям не только для доставки дейтаграмм и широковещательных рассылок, но и для обеспечения «peer-to-peer» соединений между узлами сети. Ввиду своего простого устройства, у данного протокола появилось множество не запланированных ранее способов применения, правда, недостатки протокола, такие как отсутствие гарантированной доставки, никуда при этом не исчезли. В этой статье описывается реализация протокола гарантированной доставки поверх UDP.
Такая архитектура Интернета достаточно правильна для клиент-серверного взаимодействия, когда клиенты могут находиться в частных сетях, а серверы имею глобальный адрес. Но она создает трудности для прямого соединения двух узлов между различными частными сетями. Прямое соединение двух узлов важно для «peer-to-peer» приложений, таких как передача голоса (Skype), получение удаленного доступа к компьютеру (TeamViewer), или онлайн игры.
Один из наиболее эффективных методов для установления peer-to-peer соединения между устройствами находящимися в различных частных сетях называется «hole punching». Этот техника чаще всего используется с приложениями на основе UDP протокола.
Но если вашему приложению требуется гарантированная доставка данных, например, вы передаете файлы между компьютерами, то при использовании UDP появится множество трудностей, связанных с тем, что UDP не является протоколом гарантированной доставки и не обеспечивает доставку пакетов по порядку, в отличие от TCP протокола.
В таком случае, для обеспечения гарантированной доставки пакетов, требуется реализовать протокол прикладного уровня, обеспечивающий необходимую функциональность и работающий поверх UDP.
Сразу хочу заметить, что существует техника TCP hole punching, для установления TCP соединений между узлами в разных частных сетях, но ввиду отсутствия поддержки её многими NAT устройствами она обычно не рассматривается как основной способ соединения таких узлов.
Для понимания данных требований, давайте рассмотрим временные диаграммы передачи данных между двумя узлами сети по протоколам TCP и UDP. Пусть в обоих случаях у нас будет потерян один пакет.
Передача неинтерактивных данных по TCP:
Передача данных по протоколу UDP:
Обнаружение ошибок в TCP протоколе достигается благодаря установке соединения с конечным узлом, сохранению состояния этого соединения, указанию номера отправленных байт в каждом заголовке пакета, и уведомлениях о получениях с помощью номера подтверждения «acknowledge number».
Дополнительно, для повышения производительности (т.е. отправки более одного сегмента без получения подтверждения) TCP протокол использует так называемое окно передачи - число байт данных которые отправитель сегмента ожидает принять.
Более подробно с TCP протоколом можно ознакомиться в rfc 793 , с UDP в rfc 768 , где они, собственно говоря, и определены.
Из вышеописанного, понятно, что для создания надежного протокола доставки сообщений поверх UDP (в дальнейшем будем называть Reliable UDP ), требуется реализовать схожие с TCP механизмы передачи данных. А именно:
Инкапсуляция заголовка Reliable UDP:
Для завершения соединения используется похожий механизм. В последнем пакете сообщения устанавливается флаг LastPacket. В ответном пакете указывается номер последнего пакета + 1, что для приёмной стороны означает успешную доставку сообщения.
Диаграмма установление и завершение соединения:
Сторона-получатель принимает пакеты. Каждый пакет проверяется на попадание в окно передачи. Не попадающие в окно пакеты и дубликаты отсеиваются. Т.к. размер окна сторого фиксирован и одинаков у получателя и у отправителя, то в случае доставки блока пакетов без потерь, окно сдвигается для приема пакетов следующего блока данных и отправляется подтверждение о доставке. Если окно не заполнится за установленный рабочим таймером период, то будет запущена проверка на то, какие пакеты не были доставлены и будут отправлены запросы на повторную доставку.
Диаграмма повторной передачи:
Второй таймер – необходим для закрытия соединения в случае отсутствия связи между узлами. Для стороны-отправителя он запускается сразу после срабатывания рабочего таймера, и ожидает ответа от удаленного узла. В случае отсутствия ответа за установленный период – соединение завершается и ресурсы освобождаются. Для стороны-получателя, таймер закрытия соединения запускается после двойного срабатывания рабочего таймера. Это необходимо для страховки от потери пакета подтверждения. При срабатывании таймера, также завершается соединение и высвобождаются ресурсы.
Closed – в действительности не является состоянием, это стартовая и конечная точка для автомата. За состояние Closed принимается блок управления передачей, который, реализуя асинхронный UDP сервер, перенаправляет пакеты в соответствующие соединения и запускает обработку состояний.
FirstPacketSending – начальное состояние, в котором находится исходящее соединение при отправке сообщения.
В этом состоянии отправляется первый пакет для обычных сообщений. Для сообщений без подтверждения отправки, это единственное состояние – в нем происходит отправка всего сообщения.
SendingCycle – основное состояния для передачи пакетов сообщения.
Переход в него из состояния FirstPacketSending осуществляется после отправки первого пакета сообщения. Именно в это состояние приходят все подтверждения и запросы на повторные передачи. Выход из него возможен в двух случаях – в случае успешной доставки сообщения или по тайм-ауту.
FirstPacketReceived – начальное состояние для получателя сообщения.
В нем проверяется корректность начала передачи, создаются необходимые структуры, и отправляется подтверждение о приеме первого пакета.
Для сообщения, состоящего из единственного пакета и отправленного без использования подтверждения доставки – это единственное состояние. После обработки такого сообщения соединение закрывается.
Assembling – основное состояние для приема пакетов сообщения.
В нем производится запись пакетов во временное хранилище, проверка на отсутствие потерь пакетов, отправка подтверждений о доставке блока пакетов и сообщения целиком, и отправка запросов на повторную доставку потерянных пакетов. В случае успешного получения всего сообщения – соединение переходит в состояние Completed , иначе выполняется выход по тайм-ауту.
Completed – закрытие соединения в случае успешного получения всего сообщения.
Данное состояние необходимо для сборки сообщения и для случая, когда подтверждение о доставке сообщения было потеряно по пути к отправителю. Выход из этого состояния производится по тайм-ауту, но соединение считается успешно закрытым.
Некоторые члены класса ReliableUdpConnectionControlBlock:
internal class ReliableUdpConnectionControlBlock: IDisposable
{
// массив байт для указанного ключа. Используется для сборки входящих сообщений
public ConcurrentDictionary
Реализация асинхронного UDP сервера:
private void Receive()
{
EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
// создаем новый буфер, для каждого socket.BeginReceiveFrom
byte buffer = new byte;
// передаем буфер в качестве параметра для асинхронного метода
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
Некоторые члены класса ReliableUdpConnectionRecord:
internal class ReliableUdpConnectionRecord: IDisposable
{
// массив байт с сообщением
public byte IncomingStream { get; set; }
// ссылка на состояние конечного автомата
public ReliableUdpState State { get; set; }
// пара, однозначно определяющая connection record
// в блоке управления передачей
public Tuple
Всю логику работы протокола реализуют представленные выше классы, совместно со вспомогательным классом, предоставляющим статические методы, такие как, например, построения заголовка ReliableUdp из connection record.
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); }
В состоянии 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.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); }
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)); }
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; // устанавливаем наибольший пришедший пакет 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); } }
Диаграмма передачи данных в нормальных условиях:
Создание исходящего соединения:
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;
// создаем случайный номер transmissionId
m_randomCrypto.GetBytes(transmissionId);
Tuple
Отправка первого пакета (состояние 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); }
Создание соединения на принимающей стороне:
private void EndReceive(IAsyncResult ar)
{
// ...
// пакет получен
// парсим заголовок пакета
ReliableUdpHeader header;
if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
{
// пришел некорректный пакет - отбрасываем его
return;
}
// конструируем ключ для определения connection record’а для пакета
Tuple
Прием первого пакета и отправка подтверждения (состояние 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); } }
Диаграмма закрытия соединения по тайму-ауту:
Включение рабочего таймера (состояние SendingCycle):
public override void SendPacket(ReliableUdpConnectionRecord connectionRecord) { // отправляем блок пакетов // ... // перезапускаем таймер после отправки connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1); if (connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(-1, -1); }
У входящего соединения таймер запускается после получения последнего дошедшего пакета данных, это происходит в методе ReceivePacket состояния Assembling
Включение рабочего таймера (состояние 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); // ... }
Отправка запросов на повторную доставку (состояние 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); } }
Со стороны отправителя тоже срабатывает рабочий таймер и повторно отсылается последний отправленный пакет.
Включение таймера закрытия соединения (состояние 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); }
Через непродолжительное время, повторно срабатывает рабочий таймер на стороне получателя, вновь производится отправка запросов, после чего запускается таймер закрытия соединения у входящего соединения
По срабатыванию таймеров закрытия все ресурсы обоих connection record освобождаются. Отправитель сообщает о неудачной доставке вышестоящему приложению (см. API Reliable UDP
).
Освобождение ресурсов connection record"a:
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); } }
Диаграмма восстановления передачи данных при потере пакета:
Отправка запросов на повторную доставку пакетов (состояние Assembling):
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)); }
Проверка на попадание в окно приема (состояние Assembling):
public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte payload) { // ... // увеличиваем счетчик пакетов connectionRecord.PacketCounter++; // записываем в массив управления окном текущий номер пакета connectionRecord.WindowControlArray = 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); } // ... }
Типы сообщений:
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)
Продемонстрированная версия протокола надежной доставки достаточно устойчива и гибка, и соответствует определенным ранее требованиям. Но я хочу добавить, что описанная реализация может быть усовершенстована. К примеру, для увеличения пропускной способности и динамического изменения периодов таймеров в протокол можно добавить такие механизмы как sliding window и RTT , также будет полезным реализация механизма определения MTU между узлами соединения (но только в случае отправки больших сообщений).
Спасибо за внимание, жду Ваших комментариев и замечаний.
Implementing the CLR Asynchronous Programming Model и How to implement the IAsyncResult design pattern
Update: Спасибо mayorovp и sidristij за идею добавления task"а к интерфейсу. Совместимость библиотеки со старыми ОС не нарушается, т.к. 4-ый фреймворк поддерживает и XP и 2003 server.
Протокол UDP (User Datagram Protocol – протокол дейтаграмм пользователя) является не ориентированным на соединение транспортным протоколом с ненадежной доставкой данных. Т.е. он не обеспечивает подтверждение доставки пакетов, не сохраняет порядок входящих пакетов, может терять пакеты или дублировать их. Функционирование UDP похоже на IP, за исключением введения понятия портов. UDP обычно работает быстрее TCP за счет меньших «накладных расходов». Он применяется приложениями, которые не нуждаются в надежной доставке, либо реализуют их сами. Например, сервера имен (Name Servers), служба TFTP (Trivial File Transfer Protocol, тривиальный протокол передачи данных), SNMP (Simple Network Management Protocol, простой протокол управления сетью), системы аутентификации. Идентификатор UDP протокола в поле Protocol заголовка IP – число 17.
Любая прикладная программа, использующая UDP в качестве своей службы транспортного уровня, должна сама обеспечить механизмы подтверждения и систему последовательной нумерации, чтобы гарантировать доставку пакетов в том же порядке, в котором они были высланы.
Destination Port |
||||
Рис. Формат заголовка UDP-пакета
Номер порта отправителя – Source Port (16 бит) – содержит номер порта, с которого был отправлен пакет, когда это имеет значение (например, отправитель ожидает ответа). Если это поле не используется, оно заполняется нулями.
Номер порта назначения – Destination Port (16 бит) – содержит номер порта, на который будет доставлен пакет.
Длина – Length (16 бит) – содержит длину данной дейтаграммы в байтах, включая заголовок и данные.
Поле контрольной суммы – Checksum (16 бит) – представляет собой побитное дополнение 16-битной суммы 16-битных слов. В вычислении суммы участвуют: данные пакета с полями выравнивания по 16-битной границе (нулевые), заголовок UDP-пакета, псевдозаголовок (информация от IP-протокола).
Протокол TCP (Transmission Control Protocol – протокол управления передачей) является ориентированным на соединение транспортным протоколом с надежной доставкой данных. Поэтому он имеет жесткие алгоритмы обнаружения ошибок, разработанные для обеспечения целостности передаваемых данных.
Для обеспечения надежной доставки применяется последовательная нумерация и подтверждение. С помощью последовательной нумерации определяется порядок следования данных в пакетах и выявляются пропущенные пакеты. Последовательная нумерация с подтверждением позволяет организовать надежную связь, которая называется полным дуплексом (full duplex). Каждая сторона соединения обеспечивает собственную нумерацию для другой стороны.
TCP – является байтовым последовательным протоколом. В отличие от пакетных последовательных протоколов, он присваивает последовательный номер каждому передаваемому байту пакета, а не каждому пакету в отдельности.
Destination Port |
|||||||||||
Acknowledgement Number |
|||||||||||
Рис. Формат заголовка TCP-пакета
Протокол пользовательских дейтаграмм (UDP) — это самый простой коммуникационный протокол Transport Layer, доступный из набора протоколов TCP/IP. Это связано с минимальным механизмом связи. UDP считается ненадежным транспортным протоколом, но он использует IP-услуги, которые обеспечивают лучший механизм доставки усилий.
В UDP приемник не генерирует подтверждение принятого пакета и, в свою очередь, отправитель не ожидает подтверждения подтверждения отправленного пакета. Этот недостаток делает этот протокол ненадежным, а также проще при обработке.
Востребованность UDP
Может возникнуть вопрос, почему нам нужен ненадежный протокол для транспортировки данных? Мы развертываем UDP, где пакеты подтверждения имеют значительный объем полосы пропускания вместе с фактическими данными. Например, в случае потоковой передачи видео тысячи пакетов отправляются к своим пользователям. Признание всех пакетов затруднительно и может содержать огромное количество потерь пропускной способности. Лучший механизм доставки базового IP-протокола обеспечивает наилучшие усилия для доставки своих пакетов, но даже если некоторые пакеты в потоке видео теряются, это не катастрофично и легко может быть проигнорировано. Потеря нескольких пакетов в видео и голосовом трафике иногда остается незамеченной.
Возможности User Datagram Protocol
Заголовок UDP
UDP-заголовок так же прост, как и его функция.
Заголовок UDP содержит четыре основных параметра:
Где используется UDP?
Вот несколько приложений, в которых UDP используется для передачи данных:
UDP использует простую модель передачи, без неявных "рукопожатий" для обеспечения надежности, упорядочивания или целостности данных. Таким образом, UDP предоставляет ненадежный сервис, и датаграммы могут прийти не по порядку, дублироваться или вовсе исчезнуть без следа. UDP подразумевает, что проверка ошибок и исправление либо не необходимы, либо должны исполняться в приложении. Чувствительные ко времени приложения часто используют UDP, так как предпочтительнее сбросить пакеты, чем ждать задержавшиеся пакеты, что может оказаться невозможным в системах реального времени . При необходимости исправления ошибок на сетевом уровне интерфейса приложение может задействовать TCP или SCTP , разработанные для этой цели.
Природа UDP как протокола без сохранения состояния также полезна для серверов, отвечающих на небольшие запросы от огромного числа клиентов, например DNS и потоковые мультимедийные приложения вроде IPTV , Voice over IP , протоколы туннелирования IP и многие онлайн-игры .
UDP не предоставляет никаких гарантий доставки сообщения для протокола верхнего уровня и не сохраняет состояния отправленных сообщений. По этой причине UDP иногда называют Unreliable Datagram Protocol (англ. - Ненадежный протокол датаграмм).
Перед расчетом контрольной суммы UDP-сообщение дополняется в конце нулевыми битами до длины, кратной 16 битам (псевдозаголовок и добавочные нулевые биты не отправляются вместе с сообщением). Поле контрольной суммы в UDP-заголовке во время расчета контрольной суммы отправляемого сообщения принимается нулевым.
Для расчета контрольной суммы псевдозаголовок и UDP-сообщение разбивается на слова (1 слово = 2 байта (октета) = 16 бит). Затем рассчитывается поразрядное дополнение до единицы суммы всех слов с поразрядным дополнением. Результат записывается в соответствующее поле в UDP-заголовке.
Нулевое значение контрольной суммы зарезервировано, и означает что датаграмма не имеет контрольной суммы. В случае, если вычисленная контрольная сумма получилась равной нулю, поле заполняют двоичнымим единицами.
При получении сообщения получатель считает контрольную сумму заново (уже учитывая поле контрольной суммы), и, если в результате получится двоичное число из шестнадцати единиц (то есть 0xffff), то контрольная сумма считается сошедшейся. Если сумма не сходится (данные были повреждены при передаче), датаграмма уничтожается.
Для примера рассчитаем контрольную сумму нескольких 16-битных слов: 0x398a, 0xf802, 0x14b2, 0xc281 . Находим их сумму с поразрядным дополнением.
0x398a + 0xf802 = 0x1318c → 0x318d
0x318d + 0x14b2 = 0x0463f → 0x463f
0x463f + 0xc281 = 0x108c0 → 0x08c1
Теперь находим поразрядное дополнение до единицы полученного результата:
0x08c1 = 0000 1000 1100 0001 → 1111 0111 0011 1110 = 0xf73e или, иначе - 0xffff − 0x08c1 = 0xf73e . Это и есть искомая контрольная сумма.
При вычислении контрольной суммы опять используется псевдозаголовок, имитирующий реальный IPv6-заголовок:
Биты | 0 – 7 | 8 – 15 | 16 – 23 | 24 – 31 | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Адрес источника | |||||||||||||||||||||||||||||||
32 | ||||||||||||||||||||||||||||||||
64 | ||||||||||||||||||||||||||||||||
96 | ||||||||||||||||||||||||||||||||
128 | Адрес получателя | |||||||||||||||||||||||||||||||
160 | ||||||||||||||||||||||||||||||||
192 | ||||||||||||||||||||||||||||||||
224 | ||||||||||||||||||||||||||||||||
256 | Длина UDP | |||||||||||||||||||||||||||||||
288 | Нули | Следующий заголовок | ||||||||||||||||||||||||||||||
320 | Порт источника | Порт получателя | ||||||||||||||||||||||||||||||
352 | Длина | Контрольная сумма | ||||||||||||||||||||||||||||||
384+ | Данные |
Адрес источника такой же, как и в IPv6-заголовке. Адрес получателя - финальный получатель; если в IPv6-пакете не содержится заголовка маршрутизации (Routing), то это будет адрес получателя из IPv6-заголовка, в противном случае, на начальном узле, это будет адрес последнего элемента заголовка маршрутизации, а на узле-получателе - адрес получателя из IPv6-заголовка. Значение "Следующий заголовок" равно значению протокола - 17 для UDP. Длина UDP - длина UDP-заголовка и данных.
Из-за недостатка надежности, приложения UDP должны быть готовыми к некоторым потерям, ошибкам и дублированиям. Некоторые из них (например, TFTP) могут при необходимости добавить элементарные механизмы обеспечения надежности на прикладном уровне.
Но чаще такие механизмы не используются UDP-приложениями и даже мешают им. Потоковые медиа , многопользовательские игры в реальном времени и VoIP - примеры приложений, часто использующих протокол UDP. В этих конкретных приложениях потеря пакетов обычно не является большой проблемой. Если приложению необходим высокий уровень надежности, то можно использовать другой протокол (TCP) или erasure codes.
Более серьезной потенциальной проблемой является то, что в отличие от TCP, основанные на UDP приложения не обязательно имеют хорошие механизмы контроля и избежания перегрузок. Чувствительные к перегрузкам UDP-приложения, которые потребляют значительную часть доступной пропускной способности, могут поставить под угрозу стабильность в Интернете.
Сетевые механизмы были предназначены для того, чтобы свести к минимуму возможные эффекты от перегрузок при неконтролируемых, высокоскоростных нагрузках. Такие сетевые элементы, как маршрутизаторы, использующие пакетные очереди и техники сброса, часто являются единственным доступным инструментом для замедления избыточного UDP-трафика. DCCP (англ. Datagram Congestion Control Protocol - протокол контроля за перегрузками датаграмм) разработан как частичное решение этой потенциальной проблемы с помощью добавления конечному хосту механизмов для отслеживания перегрузок для высокоскоростных UDP-потоков вроде потоковых медиа.
Многочисленные ключевые Интернет-приложения используют UDP, в их числе - DNS (где запросы должны быть быстрыми и состоять только из одного запроса, за которым следует один пакет ответа), Простой Протокол Управления Сетями (SNMP), Протокол Маршрутной Информации (RIP), Протокол Динамической Конфигурации Узла (DHCP).
Голосовой и видеотрафик обычно передается с помощью UDP. Протоколы потокового видео в реальном времени и аудио разработаны для обработки случайных потерь пакетов так, что качество лишь незначительно уменьшается вместо больших задержек при повторной передаче потерянных пакетов. Поскольку и TCP, и UDP работают с одной и той же сетью, многие компании замечают, что недавнее увеличение UDP-трафика из-за этих приложений реального времени мешает производительности TCP-приложений вроде систем баз данных или бухгалтерского учета . Так как и бизнес-приложения, и приложения в реальном времени важны для компаний, развитие качества решений проблемы некоторыми рассматривается в качестве важнейшего приоритета.
TCP - ориентированный на соединение протокол, что означает необходимость "рукопожатия" для установки соединения между двумя хостами. Как только соединение установлено, пользователи могут отправлять данные в обоих направлениях.
UDP - более простой, основанный на сообщениях протокол без установления соединения. Протоколы такого типа не устанавливают выделенного соединения между двумя хостами. Связь достигается путем передачи информации в одном направлении от источника к получателю без проверки готовности или состояния получателя. Однако, основным преимуществом UDP над TCP являются приложения для голосовой связи через интернет-протокол (Voice over IP, VoIP), в котором любое "рукопожатие" помешало бы хорошей голосовой связи. В VoIP считается, что конечные пользователи в реальном времени предоставят любое необходимое подтверждение о получении сообщения.
Основные протоколы TCP/IP по уровням модели OSI (Список портов TCP и UDP) | |
---|---|
Физический | |
Канальный | |
Сетевой | |
Транспортный | |
Сеансовый | |
Представления | |
Описание и назначение
UDP - User Datagram Protocol/ Протокол для передачи датаграмм пользователя (RFC-768). Протокол UDP базируется на протоколе IP и предоставляет прикладным процессам транспортные услуги, немногим отличающиеся от услуг протокола IP. Протокол UDP обеспечивает негарантированную доставку данных, т.е. не требует подтверждения их получения; кроме того, данный протокол не требует установления соединения между источником и приемником информации.
Уровень (по модели OSI): Транспортный.
UDP один из ключевых элементов Transmission Control Protocol/Internet Protocol, набора сетевых протоколов для Интернета. С UDP компьютерные приложения могут посылать сообщения (в данном случае называемые датаграммами) другим хостам по IP-сети без необходимости предварительного сообщения для установки специальных каналов передачи или путей данных. Протокол был разработан Дэвидом П. Ридом в 1980 году и официально определён в RFC 768.
UDP использует простую модель передачи, без неявных «рукопожатий» для обеспечения надёжности, упорядочивания или целостности данных. Таким образом, UDP предоставляет ненадёжный сервис, и датаграммы могут прийти не по порядку, дублироваться или вовсе исчезнуть без следа. UDP подразумевает, что проверка ошибок и исправление либо не нужны, либо должны исполняться в приложении. Чувствительные ко времени приложения часто используют UDP, так как предпочтительнее сбросить пакеты, чем ждать задержавшиеся пакеты, что может оказаться невозможным в системах реального времени. При необходимости исправления ошибок на сетевом уровне интерфейса приложение может задействовать TCP или SCTP, разработанные для этой цели.
Природа UDP как протокола без сохранения состояния также полезна для серверов, отвечающих на небольшие запросы от огромного числа клиентов, например DNS и потоковые мультимедийные приложения вроде IPTV, Voice over IP, протоколы туннелирования IP и многие онлайн-игры.
UDP -- минимальный ориентированный на обработку сообщений протокол транспортного уровня, задокументированный в RFC 768. Соответственно выполняет все фунции транспортного уровня.
UDP не предоставляет никаких гарантий доставки сообщения для протокола верхнего уровня и не сохраняет состояния отправленных сообщений. По этой причине UDP иногда называют Unreliable Datagram Protocol (англ. -- Ненадёжный протокол датаграмм).
UDP обеспечивает многоканальную передачу (с помощью номеров портов) и проверку целостности (с помощью контрольных сумм) заголовка и существенных данных. Надёжная передача в случае необходимости должна реализовываться пользовательским приложением.
Рис.3.5.
Заголовок UDP состоит из четырёх полей, каждое по 2 байта (16 бит). Два из них необязательны к использованию в IPv4 (розовые ячейки в таблице), в то время как в IPv6 необязателен только порт отправителя.
Порт отправителя
В этом поле указывается номер порта отправителя. Предполагается, что это значение задаёт порт, на который при необходимости будет посылаться ответ. В противном же случае, значение должно быть равным 0. Если хостом-источником является клиент, то номер порта будет, скорее всего, эфемерным. Если источником является сервер, то его порт будет одним из «хорошо известных».
Порт получателя
Это поле обязательно и содержит порт получателя. Аналогично порту отправителя, если клиент -- хост-получатель, то номер порта эфемерный, иначе (сервер -- получатель) это «хорошо известный порт».
Длина датаграммы
Поле, задающее длину всей датаграммы (заголовка и данных) в байтах. Минимальная длина равна длине заголовка -- 8 байт. Теоретически, максимальный размер поля -- 65535 байт для UDP-датаграммы (8 байт на заголовок и 65527 на данные). Фактический предел для длины данных при использовании IPv4 -- 65507 (помимо 8 байт на UDP-заголовок требуется ещё 20 на IP-заголовок).
В Jumbogram"мах IPv6 пакеты UDP могут иметь больший размер. Максимальное значение составляет 4 294 967 295 байт (2^32 -- 1), из которых 8 байт соответствуют заголовку, а остальные 4 294 967 287 байт -- данным.
Контрольная сумма
Поле контрольной суммы используется для проверки заголовка и данных на ошибки. Если сумма не сгенерирована передатчиком, то поле заполняется нулями. Поле не является обязательным для IPv4.
Расчёт контрольной суммы
Метод для вычисления контрольной суммы определён в RFC 768.
Перед расчётом контрольной суммы UDP-сообщение дополняется в конце нулевыми битами до длины, кратной 16 битам (псевдозаголовок и добавочные нулевые биты не отправляются вместе с сообщением). Поле контрольной суммы в UDP-заголовке во время расчёта контрольной суммы отправляемого сообщения принимается нулевым.
Для расчёта контрольной суммы псевдозаголовок и UDP-сообщение разбивается на слова (1 слово = 2 байта (октета) = 16 бит). Затем рассчитывается поразрядное дополнение до единицы суммы всех слов с поразрядным дополнением. Результат записывается в соответствующее поле в UDP-заголовке.
Нулевое значение контрольной суммы зарезервировано и означает, что датаграмма не имеет контрольной суммы. В случае, если вычисленная контрольная сумма получилась равной нулю, поле заполняют двоичными единицами.
При получении сообщения получатель считает контрольную сумму заново (уже учитывая поле контрольной суммы), и, если в результате получится двоичное число из шестнадцати единиц (то есть 0xffff), то контрольная сумма считается сошедшейся. Если сумма не сходится (данные были повреждены при передаче), датаграмма уничтожается.
Псевдозаголовки
Если UDP работает над IPv4, контрольная сумма вычисляется при помощи псевдозаголовка, который содержит некоторую информацию из заголовка IPv4. Псевдозаголовок не является настоящим IPv4-заголовком, используемым для отправления IP-пакета. В таблице приведён псевдозаголовок, используемый только для вычисления контрольной суммы.
Рис. 3.6.
Адреса источника и получателя берутся из IPv4-заголовка. Значения поля «Протокол» для UDP равно 17 (0x11). Поле «Длина UDP» соответствует длине заголовка и данных.
Вычисление контрольной суммы для IPv4 необязательно, если она не используется, то значение равно 0.
При работе UDP над IPv6 контрольная сумма обязательна. Метод для её вычисления был опубликован в RFC 2460:
При вычислении контрольной суммы опять используется псевдозаголовок, имитирующий реальный IPv6-заголовок:
Рис. 3.7.
Адрес источника такой же, как и в IPv6-заголовке. Адрес получателя -- финальный получатель; если в IPv6-пакете не содержится заголовка маршрутизации (Routing), то это будет адрес получателя из IPv6-заголовка, в противном случае, на начальном узле, это будет адрес последнего элемента заголовка маршрутизации, а на узле-получателе -- адрес получателя из IPv6-заголовка. Значение «Следующий заголовок» равно значению протокола -- 17 для UDP. Длина UDP -- длина UDP-заголовка и данных.
Надёжность и решения проблемы перегрузок
Из-за недостатка надёжности приложения UDP должны быть готовы к некоторым потерям, ошибкам и дублированиям. Некоторые из них (например, TFTP) могут при необходимости добавить элементарные механизмы обеспечения надёжности на прикладном уровне.
Но чаще такие механизмы не используются UDP-приложениями и даже мешают им. Потоковые медиа, многопользовательские игры в реальном времени и VoIP -- примеры приложений, часто использующих протокол UDP. В этих конкретных приложениях потеря пакетов обычно не является большой проблемой. Если приложению необходим высокий уровень надёжности, то можно использовать другой протокол (TCP) или erasure codes.
Более серьёзной потенциальной проблемой является то, что в отличие от TCP, основанные на UDP приложения не обязательно имеют хорошие механизмы контроля и избегания перегрузок. Чувствительные к перегрузкам UDP-приложения, которые потребляют значительную часть доступной пропускной способности, могут поставить под угрозу стабильность в Интернете.
Сетевые механизмы были предназначены для того, чтобы свести к минимуму возможные эффекты от перегрузок при неконтролируемых, высокоскоростных нагрузках. Такие сетевые элементы, как маршрутизаторы, использующие пакетные очереди и техники сброса, часто являются единственным доступным инструментом для замедления избыточного UDP-трафика. DCCP (англ. Datagram Congestion Control Protocol -- протокол контроля за перегрузками датаграмм) разработан как частичное решение этой потенциальной проблемы с помощью добавления конечному хосту механизмов для отслеживания перегрузок для высокоскоростных UDP-потоков вроде потоковых медиа.
Приложения
Многочисленные ключевые Интернет-приложения используют UDP, в их числе -- DNS (где запросы должны быть быстрыми и состоять только из одного запроса, за которым следует один пакет ответа), Простой Протокол Управления Сетями (SNMP), Протокол Маршрутной Информации (RIP), Протокол Динамической Конфигурации Узла (DHCP).
Голосовой и видеотрафик обычно передается с помощью UDP. Протоколы потокового видео в реальном времени и аудио разработаны для обработки случайных потерь пакетов так, что качество лишь незначительно уменьшается вместо больших задержек при повторной передаче потерянных пакетов. Поскольку и TCP, и UDP работают с одной и той же сетью, многие компании замечают, что недавнее увеличение UDP-трафика из-за этих приложений реального времени мешает производительности TCP-приложений вроде систем баз данных или бухгалтерского учета. Так как и бизнес-приложения, и приложения в реальном времени важны для компаний, развитие качества решений проблемы некоторыми рассматривается в качестве важнейшего приоритета.
Сравнение UDP и TCP
TCP -- ориентированный на соединение протокол, что означает необходимость «рукопожатия» для установки соединения между двумя хостами. Как только соединение установлено, пользователи могут отправлять данные в обоих направлениях.
Надёжность -- TCP управляет подтверждением, повторной передачей и тайм-аутом сообщений. Производятся многочисленные попытки доставить сообщение. Если оно потеряется на пути, сервер вновь запросит потерянную часть. В TCP нет ни пропавших данных, ни (в случае многочисленных тайм-аутов) разорванных соединений.
Упорядоченность -- если два сообщения последовательно отправлены, первое сообщение достигнет приложения-получателя первым. Если участки данных прибывают в неверном порядке, TCP отправляет неупорядоченные данные в буфер до тех пор, пока все данные не могут быть упорядочены и переданы приложению.
Тяжеловесность -- TCP необходимо три пакета для установки сокет-соединения перед тем, как отправить данные. TCP следит за надёжностью и перегрузками.
Потоковость -- данные читаются как поток байтов, не передается никаких особых обозначений для границ сообщения или сегментов.
UDP -- более простой, основанный на сообщениях протокол без установления соединения. Протоколы такого типа не устанавливают выделенного соединения между двумя хостами. Связь достигается путем передачи информации в одном направлении от источника к получателю без проверки готовности или состояния получателя. Однако, основным преимуществом UDP над TCP являются приложения для голосовой связи через интернет-протокол (Voice over IP, VoIP), в котором любое «рукопожатие» помешало бы хорошей голосовой связи. В VoIP считается, что конечные пользователи в реальном времени предоставят любое необходимое подтверждение о получении сообщения.
Ненадёжный -- когда сообщение посылается, неизвестно, достигнет ли оно своего назначения -- оно может потеряться по пути. Нет таких понятий, как подтверждение, повторная передача, тайм-аут.
Неупорядоченность -- если два сообщения отправлены одному получателю, то порядок их достижения цели не может быть предугадан.
Легковесность -- никакого упорядочивания сообщений, никакого отслеживания соединений и т. д. Это небольшой транспортный уровень, разработанный на IP.
Датаграммы -- пакеты посылаются по отдельности и проверяются на целостность только если они прибыли. Пакеты имеют определенные границы, которые соблюдаются после получения, то есть операция чтения на сокете-получателе выдаст сообщение таким, каким оно было изначально послано.
Нет контроля перегрузок -- UDP сам по себе не избегает перегрузок. Для приложений с большой пропускной способностью возможно вызвать коллапс перегрузок, если только они не реализуют меры контроля на прикладном уровне.