Транспортные протоколы - UDP. Как работает User Datagram Protocol (UDP) Сообщение udp передается в поле данных

User Datagram Protocol (UDP) (протокол пользовательских дейтаграмм) - является протоколом стандарта TCP/IP , определенный в стандарте RFC 768, "User Datagram Protocol (UDP)". UDP используется вместо TCP для быстрой и ненадежной транспортировки данных между TCP/IP хостами.

UDP протокол обеспечивает обслуживание без установления соединения, таким образом UDP не гарантирует доставку или проверки последовательности для любой дейтаграммы. Хост, который нуждается в надежной связи должен использовать либо протокол TCP либо программу, которая будет сама следить за последовательностью дейтаграмм и подтверждать прием каждого пакета.

Чувствительные ко времени приложения часто используют UDP (видеоданные), так как предпочтительнее сбросить пакеты, чем ждать задержавшиеся пакеты, что может оказаться невозможным в системах реального времени. Также потеря одного или нескольких кадров, при передаче видеоданных по UDP, не так критична, в отличии от передачи бинарных файлов, где потеря одно пакета может привести к искажению всего файла. Еще одним преимуществом протокола UDP является то, что длина заголовка UDP составляет 4 байта, а у TCP протокола - 20 байт.

UDP сообщения инкапсулируются и передаются в IP дейтаграммы.

UDP заголовок

На рисунке показаны поля, присутствующие в UDP заголовке.

  • Порт отправителя - в этом поле указывается номер порта отправителя. Предполагается, что это значение задает порт, на который при необходимости будет посылаться ответ. В противном же случае, значение должно быть равным 0. Если хостом-источником является клиент, то номер порта будет, скорее всего, эфемерным. Если источником является сервер, то его порт будет одним из "хорошо известных".
  • Порт получателя - это поле обязательно и содержит порт получателя. Аналогично порту отправителя, если клиент - хост-получатель, то номер порта эфемерный, иначе (сервер - получатель) это "хорошо известный порт".
  • Длина дейтаграммы - поле, задающее длину всей дейтаграммы (заголовка и данных) в байтах. Минимальная длина равна длине заголовка - 8 байт. Теоретически, максимальный размер поля - 65535 байт для UDP-дейтаграммы (8 байт на заголовок и 65527 на данные). Фактический предел для длины данных при использовании IPv4 - 65507 (помимо 8 байт на UDP-заголовок требуется еще 20 на IP-заголовок).
  • Контрольная сумма - поле контрольной суммы используется для проверки заголовка и данных на ошибки. Если сумма не сгенерирована передатчиком, то поле заполняется нулями.

Рассмотрим структуру заголовка UDP с помощью сетевого анализатора Wireshark:

UDP порты

Так как на одном и том же компьютере могут быть запущены несколько программ, то для доставки UDP-пакета конкретной программе, используется уникальный идентификатор каждой программы или номер порта.

Номер порта - это условное 16-битное число от 1 до 65535, указывающее, какой программе предназначается пакет.

UDP порты обеспечивают возможность для отправки и получения сообщений UDP. UDP порт функционирует как одиночная очередь сообщений для получения всех дейтаграмм, предназначенных для программы, указанной номером порта протокола. Это означает, что UDP-программы могут получать более одного сообщения за раз.

Все номера портов UDP, которые меньше чем 1024 - зарезервированы и зарегистрированы в Internet Assigned Numbers Authority (IANA).
Номера портов UDP и TCP не пересекаются.

Каждый порт 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-приложений вроде систем баз данных или бухгалтерского учета . Так как и бизнес-приложения, и приложения в реальном времени важны для компаний, развитие качества решений проблемы некоторыми рассматривается в качестве важнейшего приоритета.

Сравнение UDP и TCP

TCP - ориентированный на соединение протокол, что означает необходимость "рукопожатия" для установки соединения между двумя хостами. Как только соединение установлено, пользователи могут отправлять данные в обоих направлениях.

  • Надежность - TCP управляет подтверждением, повторной передачей и тайм-аутом сообщений. Производятся многочисленные попытки доставить сообщение. Если оно потеряется на пути, сервер вновь запросит потерянную часть. В TCP нет ни пропавших данных, ни (в случае многочисленных тайм-аутов) разорванных соединений.
  • Упорядоченность - если два сообщения последовательно отправлены, первое сообщение достигнет приложения-получателя первым. Если участки данных прибывают в неверном порядке, TCP отправляет неупорядоченные данные в буфер до тех пор, пока все данные не могут быть упорядочены и переданы приложению.
  • Тяжеловесность - TCP необходимо три пакета для установки сокет-соединения перед тем, как отправить данные. TCP следит за надежностью и перегрузками.
  • Потоковость - данные читаются как поток байтов , не передается никаких особых обозначений для границ сообщения или сегментов.

UDP - более простой, основанный на сообщениях протокол без установления соединения. Протоколы такого типа не устанавливают выделенного соединения между двумя хостами. Связь достигается путем передачи информации в одном направлении от источника к получателю без проверки готовности или состояния получателя. Однако, основным преимуществом UDP над TCP являются приложения для голосовой связи через интернет-протокол (Voice over IP, VoIP), в котором любое "рукопожатие" помешало бы хорошей голосовой связи. В VoIP считается, что конечные пользователи в реальном времени предоставят любое необходимое подтверждение о получении сообщения.

  • Ненадежный - когда сообщение посылается, неизвестно достигнет ли оно своего назначения - оно может потеряться по пути. Нет таких понятий, как подтверждение, повторная передача, тайм-аут.
  • Неупорядоченность - если два сообщения отправлены одному получателю, то порядок их достижения цели не может быть предугадан.
  • Легковесность - никакого упорядочивания сообщений, никакого отслеживания соединений и т.д. Это небольшой транспортный уровень, разработанный на IP.
  • Датаграммы - пакеты посылаются по отдельности и проверяются на целостность только если они прибыли. Пакеты имеют определенные границы, которые соблюдаются после получения, то есть операция чтения на сокете-получателе выдаст сообщение таким, каким оно было изначально послано.
  • Нет контроля перегрузок - UDP сам по себе не избегает перегрузок. Для приложений с большой пропускной способностью возможно вызвать коллапс перегрузок, если только они не реализуют меры контроля на прикладном уровне.

Ссылки на RFC

  • RFC 768 – Протокол Пользовательских Датаграмм
  • RFC 2460 – Интернет протокол, спецификация версии 6 (IPv6)
  • RFC 2675 - IPv6 Jumbograms
  • RFC 4113 – Management Information Base for the UDP
  • RFC 5405 – Unicast UDP Usage Guidelines for Application Designers

См. также

Ссылки

  • Kurose, J. F.; Ross, K. W. (2010). Computer Networking: A Top-Down Approach (5th ed.). Boston, MA: Pearson Education. ISBN 978-0-13-136548-3 .
  • Forouzan, B.A. (2000). TCP/IP: Protocol Suite, 1st ed. New Delhi, India: Tata McGraw-Hill Publishing Company Limited.
  • [email protected]. "UDP Protocol Overview". Ipv6.com. Retrieved 17 August 2011.
  • Clark, M.P. (2003). Data Networks IP and the Internet, 1st ed. West Sussex, England: John Wiley & Sons Ltd.
  • Postel, J. (August 1980). RFC 768 : User Datagram Protocol. Internet Engineering Task Force. Retrieved from http://tools.ietf.org/html/rfc768
  • Deering S. & Hinden R. (December 1998). RFC 2460 : Internet Protocol, Version 6 (IPv6) Specification. Internet Engineering Task Force. Retrieved from http://tools.ietf.org/html/rfc2460
  • "The impact of UDP on Data Applications". Networkperformancedaily.com. Retrieved 17 August 2011.
  • Д. Комер. Межсетевой обмен с помощью TCP/IP. Глава 11. Протокол UDP.

Доброго времени суток, дорогие читатели.
По многочисленным просьбам сегодня я публикую для Вас статью, которая познакомит Вас с основами основ терминов компьютерной сети, а именно:

  • Сетевые протоколы - что это за страшные названия и с чем их едят
  • UDP, TCP, ICMP , - что, зачем и в чем разница
  • IP -адрес, - у всех есть, но не все знают нафига эта штука:-)
  • Маска адреса (подсеть)
  • Шлюз (gateway)
  • Несколько слов о таблицах маршрутизации
  • Порты, - что это на самом деле
  • MAC -адрес

Примерно так.

Статья, думаю, будет полезна всем от мала до велика, ибо содержит не столько набор странных непонятных действий или слов, сколько блок доступным языком изложенной информации, которая, как минимум, даст Вам понимание как вообще это всё работает и зачем это нужно. Поехали.

Сетевые протоколы TCP/IP, NWLink IPX/SPX, NetBEUI

Давайте начнем с того, что вообще такое сетевой протокол и с чем его едят.
Сетевой протокол - это набор программно реализованных правил общения между компьютерами. Этакий язык, на котором компьютеры разговаривают друг с другом и передают информацию. Ранее компьютеры были, так сказать, многоязычны и в старых версиях Windows использовался целый набор протоколов, - TCP/IP, NWLink IPX/SPX, NetBEUI . Ныне же пришли к общей договоренности, и стандартом стало использование исключительно протокола TCP/IP , а посему речь далее пойдет именно о нем.

Когда говорят о TCP/IP , то обычно подразумевают под этим именем множество различных.. правил или, скажем, стандартов, которые прописаны с использованием (или под использование) этого протокола. Так, например, есть правила, по которым осуществляется обмен сообщениями между почтовыми серверами и есть правила, по которым конечный пользователь получает в свой ящик письма. Имеются правила для проведения видео-конференций и правила для организации "телефонных" переговоров по Интернету. Фактически, это даже не то чтобы правила.. Скорее этакая грамматика, что ли. Ну знаете, в английском одна структура построения диалогов, в французском другая.. Вот и в TCP/IP нечто подобное, т.е. некая связка различных грамматических правил представляет собой как раз цельный протокол TCP/IP или, точнее говоря, стек протоколов TCP/IP .

Сетевые протоколы UDP, TCP, ICMP

В рамках протокола TCP/IP для передачи данных используются протоколы - TCP и UDP . Многие наверняка слышали, что есть порты как TCP , так и UDP , но не все знают в чем разница и что это вообще. И так..

Передача данных по протоколу TCP (Transmission Control Protocol - Протокол Управления Передачей) предусматривает наличие подтверждений получения информации. "-Ну, мол, - получил? -Получил!" Если же передающая сторона не получит в установленные сроки необходимого подтверждения, то данные будут переданы повторно. Поэтому протокол TCP относят к протоколам, предусматривающим соединение, а UDP (User Datagram Protocol - Протокол Пользовательских Датаграмм) - нет. UDP применяется в тех случаях, когда не требуется подтверждения приема (например, DNS-запросы или IP-телефония (яркий представитель которой, - Skype)). То есть разница заключается в наличии подтверждения приема. Казалось бы "Всего то!", но на практике это играет важную роль.

Есть еще так же протокол ICMP (Internet Control Message Protocol - межсетевой протокол управляющих сообщений), который используется для передачи данных о параметрах сети. Он включает в себя служебные типы пакетов, таки как ping, distination unreachable, TTL и пр.

Что такое IP-адрес

У всех он есть, но не все имеют представление что за адрес такой и почему вообще без него нельзя. Рассказываю.

IP -адрес - 32 -х битное число, используемое для идентификации компьютера в сети. Адрес принято записывать десятичными значениями каждого октета этого числа с разделением полученных значений точками. Например, 192.168.101.36

IP- адреса уникальны, - это значит, что каждый компьютер имеет свое собственное сочетание цифр, и в сети не может быть двух компьютеров с одинаковыми адресами. IP -адреса распределяются централизованно, интернет-провайдеры делают заявки в национальные центры в соответствии со своими потребностями. Полученные провайдерами диапазоны адресов распределяются дальше между клиентами. Клиенты, в свою очередь, сами могут выступать в роли провайдера и распределять полученные IP -адреса между субклиентами и т.д. При таком способе распределения IP -адресов компьютерная система точно знает "расположение" компьютера, имеющего уникальный IP -адрес; - ей достаточно переслать данные в сеть "владельца", а провайдер в свою очередь проанализирует пункт назначения и, зная, кому отдана эта часть адресов, отправит информацию следующему владельцу поддиапазона IP -адресов, пока данные не поступят на компьютер назначения.

Для построения же локальных сетей выделены спец.диапазоны адресов. Это адреса 10.x.x.x , 192.168.x.x , 10.x.x.x , c 172.16.x.x по 172.31.x.x , 169.254.x.x , где под x - имеется ввиду любое число это от 0 до 254 . Пакеты, передаваемые с указанных адресов, не маршрутизируется, иными словами, попросту не пересылаются через Интернет, а поэтому в различных локальных сетях компьютеры могут иметь совпадающие адреса из указанных диапазонов. Т.е., в компании ООО "Рога и копыта " и ООО "Вася и компания " могут находится два компьютера с адресами 192.168.0.244 , но не могут, скажем, с адресами 85.144.213.122 , полученными от провайдера интернета, т.к. в интернете не может быть два одинаковых IP -адреса. Для пересылки информации с таких компьютеров в Интернет и обратно используются спец.программы и устройства, которые заменяют локальные адреса реальными при работе с интернетом. Иными словами, данные в Сеть пересылаются с реального IP -адреса, а не с локального. Этот процесс происходит не заметно для пользователя и называется трансляцией адресов. Хочется так же упомянуть, что в рамках одной сети, скажем, компании, ООО "Рога и копыта ", не может быть два компьютера с одним локальным IP-адресом, т.е., в указанном выше примере имелось ввиду, что один компьютер с адресом 192.168.0.244 в одной компании, второй с таким же адресом - в другой. В одной же компании два компьютера с адресом 192.168.0.244 попросту не уживутся.

Вы наверняка слышали такие термины как внешний IP и внутренний IP , постоянный (статический IP) и переменный (динамический) IP . В двух словах о них:

  • внешний IP - это как раз тот самый IP , который выдает Вам провайдер, т.е. Ваш уникальный адрес в интернете, например, - 85.144.24.122
  • внутренний IP , - это локальный IP , т.е. Ваш IP в локальной сети, например, - 192.168.1.3
  • статический IP - это IP , который не меняется с каждым подключением, т.е. закреплен за Вами твердо и навсегда
  • динамический IP , - это плавающий IP -адрес, который меняется с каждым подключением

Тип Вашего IP (статический или динамический) зависит от настроек провайдера.

Что такое маска адреса (подсеть)

Понятие подсети введено, чтобы можно было выделить часть IP -адресов одной организации, часть другой и тд. Подсеть представляет собой диапазон IP-адресов, которые считаются принадлежащими одной локальной сети. При работе в локальной сети информация пересылается непосредственно получателю. Если данные предназначены компьютеры с IP-адресом, не принадлежащим локальной сети, то к ним применяются специальные правила для вычисления маршрута для пересылки из одной сети в другую.

Маска - это параметр, который сообщает программному обеспечению о том, сколько компьютеров объединено в данную группу (подсеть). Маска адреса имеет такую же структуру как и сам IP-адрес: это набор из четырех групп чисел, каждое из которых может быть в диапазоне от 0 до 255 . При этом, чем меньше значение маски, тем больше компьютеров объединено в данную подсеть. Для сетей небольших компаний маска обычно имеет вид 255.255.255.x (например, 255.255.255.224). Маска сети присваивается компьютеру одновременно с IP-адресом. Так, например, сеть 192.168.0.0 с маской 255.255.255.0 может содержать в себе компьютеры с адресами от 192.168.0.1 до 192.168.254 192.168.0.0 с маской 255.255.255.128 допускает адреса от 192.168.0.1 до 192.168.0.127 . Думаю, смысл понятен. Как правило сети с небольшим возможным числом компьютеров используются провайдерами с целью экономии IP-адресов. Например, клиенту, может быть назначен адрес с маской 255.255.255.252 . Такая подсеть содержит в себе только два компьютера.

После того как компьютер получил IP-адрес и ему стало известно значение маски подсети, программа может начать работу в данной локальной подсети. Однако же, чтобы обмениваться информацией с другими компьютерами в глобальной сети, необходимо знать правила, куда пересылать информацию для внешней сети. Для этого служит такая характеристика как адрес шлюза (Gateway).

Что такое Шлюз (Gateway)

Шлюз - это устройство (компьютер или маршрутизатор), которое обеспечивает пересылку информации между различными IP-подсетями. Если программа определяет (по IP и маске), что адрес назначения не входит в состав локальной подсети, то она отправляет эти данные на устройство, выполняющее функции шлюза. В настройках протокола указывают IP-адрес такого устройства.

Для работы только в локальной сети шлюз может не указываться.

Для индивидуальных пользователей, подключающихся к Интернету, или для небольших предприятий, имеющих единственный канал подключения, в системе должен быть только один адрес шлюза, - это адрес того устройства, которое имеет подключение к Интернету. При наличии нескольких маршрутов будет существовать несколько шлюзов. В этом случае для определения пути передачи данных используется таблица маршрутизации.

Что такое таблицы маршрутизации

И вот мы плавно добрались и до них. И так.. Что же за таблицы такие.

Организация или пользователь может иметь несколько точек подключения к Интернету (например, резервные каналы на случай, если у первого провайдера что-то выйдет из строя, а интернет таки очень нужен) или содержать в своей структуре несколько IP -сетей. В этом случае, чтобы система знала каким путем (через какой шлюз) посылать ту или иную информацию, используются таблицы маршрутизации. В таблицах маршрутизации для каждого шлюза указываются те подсети Интернета, для которых через них должна передаваться информация. При этом для нескольких шлюзов можно задать одинаковые диапазоны, но с разной стоимостью передачи данных: например, информация, будет пересылаться по каналу, имеющему самую низкую стоимость, а в случае выхода его из строя по тем или иным причинам, автоматически будет использоваться следующее доступное наиболее дешевое соединение.

Что такое сетевые порты

При передаче данных кроме IP -адресов отправителя и получателя пакет информации содержит в себе номера портов. Пример: 192.168.1.1:80 , - в данном случае 80 - это номер порта. Порт - это некое число, которое используется при приеме и передаче данных для идентификации процесса (программы), который должен обработать данные. Так, если пакет послан на 80 -й порт, то это свидетельствует, что информация предназначена серверу HTTP .

Номера портов с 1 -го до 1023 -й закреплены за конкретными программами (так называемые well-known-порты). Порты с номерами 1024 -65 535 могут быть использованы в программах собственной разработки. При этом возможные конфликты должны решаться самими программами путем выбора свободного порта. Иными словами, порты будут распределяться динамически: возможно, что при следующем старте программа выберет иное значение порта, если, конечно, Вы вручную через настройки не задавали ей порт.

Что есть MAC-адрес

Дело в том, что пересылаемые пакеты в сети адресуются компьютерам не по их именам и не на IP -адрес. Пакет предназначается устройству с конкретным адресом, который и называется MAC -адресом.

MAC-адрес - это уникальный адрес сетевого устройства, который заложен в него изготовителем оборудования, т.е. это этакий проштампованный номер Вашей сетевой карты. Первая половина MAC -адрес представляет собой идентификатор изготовителя, вторая - уникальный номер данного устройства.

Как правило MAC -адрес бывает требуется для идентификации, скажем, у провайдера (если провайдер использует привязку по мак-адресу вместо логина-пароля) или при настройке маршрутизатора.

Где посмотреть все сетевые настройки

Чуть не забыл сказать пару слов о том где можно поглядеть и поменять всё это.

Набора сетевых протоколов для Интернета . С UDP компьютерные приложения могут посылать сообщения (в данном случае называемые датаграммами) другим хостам по IP-сети без необходимости предварительного сообщения для установки специальных каналов передачи или путей данных. Протокол был разработан Дэвидом П. Ридом в 1980 году и официально определён в RFC 768 .

UDP использует простую модель передачи, без неявных «рукопожатий» для обеспечения надёжности, упорядочивания или целостности данных. Таким образом, UDP предоставляет ненадёжный сервис, и датаграммы могут прийти не по порядку, дублироваться или вовсе исчезнуть без следа. UDP подразумевает, что проверка ошибок и исправление либо не нужны, либо должны исполняться в приложении. Чувствительные ко времени приложения часто используют UDP, так как предпочтительнее сбросить пакеты, чем ждать задержавшиеся пакеты, что может оказаться невозможным в системах реального времени . При необходимости исправления ошибок на сетевом уровне интерфейса приложение может задействовать TCP или SCTP , разработанные для этой цели.

Природа UDP как протокола без сохранения состояния также полезна для серверов, отвечающих на небольшие запросы от огромного числа клиентов, например DNS и потоковые мультимедийные приложения вроде IPTV , Voice over IP , протоколы туннелирования IP и многие онлайн-игры .

Энциклопедичный YouTube

    1 / 5

    ✪ Порты и перенаправление\открытие портов. Инструкция и объяснения на пальцах!

  • Субтитры

Служебные порты

UDP не предоставляет никаких гарантий доставки сообщения для вышестоящего протокола и не сохраняет состояния отправленных сообщений. По этой причине UDP иногда называют Unreliable Datagram Protocol (англ. - Ненадёжный протокол датаграмм).

Контрольная сумма

Поле контрольной суммы используется для проверки заголовка и данных на ошибки. Если сумма не сгенерирована передатчиком, то поле заполняется нулями. Поле не является обязательным для IPv4.

Расчёт контрольной суммы

Метод для вычисления контрольной суммы определён в RFC 1071 .

Перед расчётом контрольной суммы, если длина UDP-сообщения в байтах нечётна, то UDP-сообщение дополняется в конце нулевым байтом (псевдозаголовок и добавочный нулевой байт не отправляются вместе с сообщением, они используются только при расчёте контрольной суммы). Поле контрольной суммы в UDP-заголовке во время расчёта контрольной суммы принимается нулевым.

Для расчёта контрольной суммы псевдозаголовок и UDP-сообщение разбивается на двухбайтные слова. Затем рассчитывается сумма всех слов в арифметике обратного кода (то есть кода, в котором отрицательное число получается из положительного инверсией всех разрядов числа и существует два нуля: 0х0000 (обозначается +0) и 0xffff (обозначается −0)). Результат записывается в соответствующее поле в UDP-заголовке.

Значение контрольной суммы, равное 0х0000 (+0 в обратном коде), зарезервировано и означает, что для посылки контрольная сумма не вычислялась. В случае, если контрольная сумма вычислялась и получилась равной 0х0000, то в поле контрольной суммы заносят значение 0xffff (-0 в обратном коде).

При получении сообщения получатель считает контрольную сумму заново (уже учитывая поле контрольной суммы), и, если в результате получится −0 (то есть 0xffff), то контрольная сумма считается сошедшейся. Если сумма не сходится (данные были повреждены при передаче, либо контрольная сумма неверно посчитана на передающей стороне), то решение о дальнейших действиях принимает принимающая сторона. Как правило, в большинстве современных устройств, работающих с UDP/IP-пакетами имеются настройки, позволяющие либо игнорировать такие пакеты, либо пропускать их на дальнейшую обработку, невзирая на неправильность контрольной суммы.

Пример расчёта контрольной суммы

Для примера рассчитаем контрольную сумму нескольких 16-битных слов: 0x398a, 0xf802, 0x14b2, 0xc281 .

Для этого можно сначала сложить попарно числа, рассматривая их как 16-разрядные беззнаковые числа с последующим приведением к дополнительному коду путём прибавления единицы к результату, если при сложении произошёл перенос в старший (17-й) разряд (то есть де-факто, этой операцией мы переводим отрицательное число из дополнительного в обратный код). Или, что равноценно, можно считать, что перенос прибавляется к младшему разряду числа.

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 code ru en).

Более серьёзной потенциальной проблемой является то, что в отличие от 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 - более простой, основанный на сообщениях протокол без установления соединения. Протоколы такого типа не устанавливают выделенного соединения между двумя хостами. Связь достигается путём передачи информации в одном направлении от источника к получателю без проверки готовности или состояния получателя. В приложениях для голосовой связи через интернет-протокол (Voice over IP, TCP/IP) UDP имеет преимущество над TCP, в котором любое «рукопожатие» помешало бы хорошей голосовой связи. В VoIP считается, что конечные пользователи в реальном времени предоставят любое необходимое подтверждение о получении сообщения.

  • Ненадёжный - когда сообщение посылается, неизвестно, достигнет ли оно своего назначения - оно может потеряться по пути. Нет таких понятий, как подтверждение, повторная передача, тайм-аут.
  • Неупорядоченность - если два сообщения отправлены одному получателю, то порядок их достижения цели не может быть предугадан.
  • Легковесность - никакого упорядочивания сообщений, никакого отслеживания соединений и т. д. Это небольшой транспортный уровень, разработанный на IP.
  • Датаграммы - пакеты посылаются по отдельности и проверяются на целостность только если они прибыли. Пакеты имеют определенные границы, которые соблюдаются после получения, то есть операция чтения на сокете-получателе выдаст сообщение таким, каким оно было изначально послано.
  • Нет контроля перегрузок - UDP сам по себе не избегает перегрузок. Для приложений с большой пропускной способностью возможно вызвать коллапс перегрузок, если только они не реализуют меры контроля на прикладном уровне.

Интернет давно изменился. Один из основных протоколов Интернета – 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 устройствами она обычно не рассматривается как основной способ соединения таких узлов.

Требования к протоколу

  1. Надежная доставка пакетов, реализованная через механизм положительной обратной связи (так называемый positive acknowledgment)
  2. Необходимость эффективной передачи больших данных, т.е. протокол должен избегать лишних ретрансляций пакетов
  3. Должна быть возможность отмены механизма подтверждения доставки (возможность функционировать как «чистый» UDP протокол)
  4. Возможность реализации командного режима, с подтверждением каждого сообщения
  5. Базовой единицей передачи данных по протоколу должно быть сообщение
Эти требования во многом совпадают с требованиями к 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, byte> IncomingStreams { get; private set;} // массив байт для указанного ключа. Используется для отправки исходящих сообщений. public ConcurrentDictionary, byte> OutcomingStreams { get; private set; } // connection record для указанного ключа. private readonly ConcurrentDictionary, ReliableUdpConnectionRecord> m_listOfHandlers; // список подписчиков на сообщения. private readonly List 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; // передаем буфер в качестве параметра для асинхронного метода 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 key = new Tuple(connectedClient, header.TransmissionId); // получаем существующую connection record или создаем новую ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType)); // запускаем пакет в обработку в конечный автомат record.State.ReceivePacket(record, header, bytes); }


Для каждой передачи сообщения создается структура, содержащая сведения о соединении. Такая структура называется connection record .

Некоторые члены класса ReliableUdpConnectionRecord:

internal class ReliableUdpConnectionRecord: IDisposable { // массив байт с сообщением public byte IncomingStream { get; set; } // ссылка на состояние конечного автомата public ReliableUdpState State { get; set; } // пара, однозначно определяющая connection record // в блоке управления передачей public Tuple 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 предоставляет интерфейс для состояния:

Всю логику работы протокола реализуют представленные выше классы, совместно со вспомогательным классом, предоставляющим статические методы, такие как, например, построения заголовка 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; // устанавливаем наибольший пришедший пакет 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; // создаем случайный номер transmissionId m_randomCrypto.GetBytes(transmissionId); Tuple key = new Tuple(endPoint, BitConverter.ToInt32(transmissionId, 0)); // создаем новую запись для соединения и проверяем, // существует ли уже такой номер в наших словарях if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult))) { // если существует – то повторно генерируем случайный номер m_randomCrypto.GetBytes(transmissionId); key = new Tuple(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.State.SendPacket(m_listOfHandlers); }


Отправка первого пакета (состояние 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); }


После отправки первого пакета отправитель переходит в состояние SendingCycle – ожидать подтверждения о доставке пакета.
Сторона-получатель, с помощью метода EndReceive, принимает отправленный пакет, создает новую connection record и передает данный пакет, с предварительно распарсенным заголовком, в обработку методу ReceivePacket состояния FirstPacketReceived

Создание соединения на принимающей стороне:

private void EndReceive(IAsyncResult ar) { // ... // пакет получен // парсим заголовок пакета ReliableUdpHeader header; if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header)) { // пришел некорректный пакет - отбрасываем его return; } // конструируем ключ для определения connection record’а для пакета Tuple key = new Tuple(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. Рассмотрим пример, в котором на промежуточным узле произошел сбой и доставка данных в обоих направления стала невозможной.

Диаграмма закрытия соединения по тайму-ауту:



Как видно из диаграммы, рабочий таймер у отправителя включается сразу после отправки блока пакетов. Это происходит в методе SendPacket состояния SendingCycle .

Включение рабочего таймера (состояние 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 состояния 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); // ... }


Во входящем соединении за время ожидания рабочего таймера не пришло больше пакетов. Таймер сработал и вызывал метод 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 (connectionRecord.CloseWaitTimer != null) connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1); else connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1); }


Период ожидания таймера закрытия соединения равен 30 секундам по-умолчанию.

Через непродолжительное время, повторно срабатывает рабочий таймер на стороне получателя, вновь производится отправка запросов, после чего запускается таймер закрытия соединения у входящего соединения

По срабатыванию таймеров закрытия все ресурсы обоих 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); } }

Глубже в код. Восстановление передачи данных

Диаграмма восстановления передачи данных при потере пакета:



Как уже обсуждалось в закрытии соединения по тайм-ауту, по истечению рабочего таймера у получателя произойдет проверка на потерянные пакеты. В случае наличия потерь пакетов будет составлен список номер пакетов, не дошедших до получателя. Данные номера заносятся в массив LostPackets конкретного соединения и выполняется отправка запросов на повторную доставку.

Отправка запросов на повторную доставку пакетов (состояние 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)); }


Повторно отправленный пакет (packet#3 на диаграмме) принимается входящим соединением. Выполняется проверка на заполнение окна приема и обычная передача данных восстанавливается.

Проверка на попадание в окно приема (состояние 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); } // ... }

API Reliable UDP

Для взаимодействия с протоколом передачи данных имеется открытый класс 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 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.

Типы сообщений:
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

  • Перенос асинхронной модели программирования в асинхронный шаблон, основанный на задачах (APM в TAP):
    TPL and Traditional .NET Asynchronous Programming
    Interop with Other Asynchronous Patterns and Types
  • Update: Спасибо mayorovp и sidristij за идею добавления task"а к интерфейсу. Совместимость библиотеки со старыми ОС не нарушается, т.к. 4-ый фреймворк поддерживает и XP и 2003 server.