Udp протокол. Чем отличается протокол TCP от UDP, простым языком. Глубже в код. Закрытие соединения по тайм-ауту

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

User Datagram Protocol - UDP

Протокол UDP - это один из двух протоколов транспортного уровня, которые используются в стеке протоколов TCP/IP. UDP позволяет прикладной программе передавать свои сообщения по сети с минимальными издержками, связанными с преобразованием протоколов уровня приложения в протокол IP. Однако при этом, прикладная программа сама должна заботиться о подтверждении того, что сообщение доставлено по месту назначения. Заголовок UDP-датаграммы (сообщения) имеет вид, показанный на рисунке 2.10.

Рис. 2.10. Структура заголовка UDP-сообщения

Единица данных протокола UDP называется UDP-пакетом или пользовательской дейтаграммой (user datagram). UDP-пакет состоит из заголовка и поля данных, в котором размещается пакет прикладного уровня. Заголовок имеет простой формат и состоит из четырех двухбайтовых полей:

    UDP source port - номер порта процесса-отправителя,

    UDP destination port - номер порта процесса-получателя,

    UDP message length - длина UDP-пакета в байтах,

    UDP checksum - контрольная сумма UDP-пакета

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

Порты в заголовке определяют протокол UDP как мультиплексор, который позволяет собирать сообщения от приложений и отправлять их на уровень протоколов. При этом приложение использует определенный порт. Взаимодействующие через сеть приложения могут использовать разные порты, что и отражает заголовок пакета. Всего можно определить 216 разных портов. Первые 256 портов закреплены за, так называемыми "well known services", к которым относятся, например, 53 порт UDP, который закреплен за сервисом DNS.

Поле Length определяет общую длину сообщения. ПолеChecksum служит для контроля целостности данных. Приложение, которое использует протокол UDP должно само заботится о целостности данных, анализируя поля Checksum и Length. Кроме этого, при обмене данными по UDP прикладная программа сама должна заботится о контроле доставки данных адресату. Обычно это достигается за счет обмена подтверждениями о доставке между прикладными программами.

Наиболее известными сервисами, основанными на UDP, является служба доменных имен BIND и распределенная файловая система NFS. Если возвратиться к примеру traceroute, то в этой программе также используется транспорт UDP. Собственно, именно сообщение UDP и засылается в сеть, но при этом используется такой порт, который не имеет обслуживания, поэтому и порождается ICMP-пакет, который и детектирует отсутствие сервиса на принимающей машине, когда пакет наконец достигает машину-адресата.

Transfer Control Protocol - TCP

Если для приложения контроль качества передачи данных по сети имеет значение, то в этом случае используется протокол TCP. Этот протокол еще называют надежным, ориентированным на соединение и потокоориентированным протоколом. Прежде чем обсудить эти свойства протокола, рассмотрим формат передаваемой по сети датаграммы (рисунок 2.11). Согласно этой структуре, в TCP, как и в UDP, имеются порты. Первые 256 портов закреплены за WKS, порты от 256 до 1024 закреплены за Unix-сервисами, а остальные можно использовать по своему усмотрению. В поле Sequence Number определен номер пакета в последовательности пакетов, которая составляет все сообщение, за тем идет поле подтвержденияAsknowledgment Number и другая управляющая информация.

Рис. 2.11. Структура пакета TCP

    Порт источника (SOURS PORT) занимает 2 байта, идентифицирует процесс-отправитель;

    Порт назначения (DESTINATION PORT) занимает 2 байта, идентифицирует процесс-получатель;

    Последовательный номер (SEQUENCE NUMBER) занимает 4 байта, указывает номер байта, который определяет смещение сегмента относительно потока отправляемых данных;

    Подтвержденный номер (ACKNOWLEDGEMENT NUMBER) занимает 4 байта, содержит максимальный номер байта в полученном сегменте, увеличенный на единицу; именно это значение используется в качестве квитанции;

    Длина заголовка (HLEN) занимает 4 бита, указывает длину заголовка сегмента TCP, измеренную в 32-битовых словах. Длина заголовка не фиксирована и может изменяться в зависимости от значений, устанавливаемых в поле Опции;

    Резерв (RESERVED) занимает 6 битов, поле зарезервировано для последующего использования;

    Кодовые биты (CODE BITS) занимают 6 битов, содержат служебную информацию о типе данного сегмента, задаваемую установкой в единицу соответствующих бит этого поля:

    URG - срочное сообщение;

    ACK - квитанция на принятый сегмент;

    PSH - запрос на отправку сообщения без ожидания заполнения буфера;

    RST - запрос на восстановление соединения;

    SYN - сообщение используемое для синхронизации счетчиков переданных данных при установлении соединения;

    FIN - признак достижения передающей стороной последнего байта в потоке передаваемых данных.

    Окно (WINDOW) занимает 2 байта, содержит объявляемое значение размера окна в байтах;

    Контрольная сумма (CHECKSUM) занимает 2 байта, рассчитывается по сегменту;

    Указатель срочности (URGENT POINTER) занимает 2 байта, используется совместно с кодовым битом URG, указывает на конец данных, которые необходимо срочно принять, несмотря на переполнение буфера;

    Опции (OPTIONS) - это поле имеет переменную длину и может вообще отсутствовать, максимальная величина поля 3 байта; используется для решения вспомогательных задач, например, при выборе максимального размера сегмента;

    Заполнитель (PADDING) может иметь переменную длину, представляет собой фиктивное поле, используемое для доведения размера заголовка до целого числа 32-битовых слов.

Надежность TCP заключается в том, что источник данных повторяет их посылку, если только не получит в определенный промежуток времени от адресата подтверждение об их успешном получении. Этот механизм называется Positive Asknowledgement with Retransmission (PAR) . Как мы ранее определили, единица пересылки (пакет данных, сообщение и т.п.) в терминах TCP носит название сегмента. В заголовке TCP существует поле контррольной суммы. Если при пересылке данные повреждены, то по контрольной сумме модуль, вычленяющий TCP-сегменты из пакетов IP, может определить это. Поврежденный пакет уничтожается, а источнику ничего не посылается. Если данные не были повреждены, то они пропускаются на сборку сообщения приложения, а источнику отправляется подтверждение.

Ориентация на соединение определяется тем, что прежде чем отправить сегмент с данными, модули TCP источника и получателя обмениваются управляющей информацией. Такой обмен называется handshake (буквально "рукопожатие"). В TCP используется трехфазный hand-shake:

    Источник устанавливает соединение с получателем, посылая ему пакет с флагом "синхронизации последовательности номеров" (Synchronize Sequence Numbers - SYN). Номер в последовательности определяет номер пакета в сообщении приложения. Это не обязательно должен быть 0 или единица. Но все остальные номера будут использовать его в качестве базы, что позволит собрать пакеты в правильном порядке;

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

    Источник подтверждает, что принял сегмент получателя и отправляет первую порцию данных.

Графически этот процесс представлен на рисунке 2.12.

Рис. 2.12. Установка соединения TCP

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

Потоковый характер протокола определяется тем, что SYN определяет стартовый номер для отсчета переданных байтов, а не пакетов. Это значит, что если SYN был установлен в 0, и было передано 200 байтов, то номер, установленный в следующем пакете будет равен 201, а не 2.

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

Рис. 2.13. Механизм передачи данных по TCP

В данном примере окно установлено в 250 байтов шириной. Это означает, что текущий сегмент - сегмент со смещением относительно SYN, равном 250 байтам. Однако, после передачи всего окна модуль TCP источника получил подтверждение на получение только первых 100 байтов. Следовательно, передача будет начата со 101 байта, а не с 251.

Таким образом, мы рассмотрели все основные свойства протокола TCP. Осталось только назвать наиболее известные приложения, которые использует TCP для обмена данными. Это в первую очередь TELNET и FTP, а также протокол HTTP, который является сердцем World Wide Web.

Прервем немного разговор о протоколах и обратим свое внимание на такую важнейшую компоненту всей системы TCP/IP как IP-адреса.

Введение

UDP простой, ориентированный на передачу датаграмм, протокол транспортного уровня: за один раз процесс выдает одну UDP датаграмму, в результате чего передается одна IP датаграмма. Это отличается от поток-ориентированных протоколов, таких как TCP, где количество данных, которое выдается приложением, практически не имеет отношения к количеству отправленных IP датаграмм.

На рисунке 11.1 показана инкапсуляция UDP датаграммы в IP датаграмму.

Рисунок 11.1 UDP инкапсуляция.

Официальная спецификация UDP приведена в RFC 768 [ Postel 1980].

UDP является ненадежным протоколом: он отправляет датаграммы, которые приложение пишет в IP уровень, однако не существует гарантии того, что они достигнут конечного пункта назначения. С точки зрения надежности может возникнуть предположение, что стоит избегать использовать UDP и всегда пользоваться надежными протоколами, такими как TCP. После того как TCP будет описан в , мы вернемся к этой теме и посмотрим, какие типы приложений могут использовать UDP.

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

UDP заголовок

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

Рисунок 11.2 UDP заголовок.

Номера портов (port numbers) указывают на отправляющий и принимающий процессы. На показано, что TCP и UDP используют номер порта назначения для демультиплексирования данных, поступающих от IP. Так как IP осуществляет демультиплексирование входящих IP датаграмм для TCP и UDP (с использованием значения протокола в IP заголовке), TCP просматривает номера портов TCP, а UDP - номера портов UDP. Номера портов TCP независимы от номеров портов UDP.

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

Поле длины UDP содержит длину в байтах UDP заголовка и UDP данных. Минимальное значение для этого поля составляет 8 байт. (Не произойдет ничего страшного, если будет отправлена UDP датаграмма с нулевой длиной данных.) Параметр UDP длины - избыточный. IP датаграмма содержит свою полную длину в байтах, поэтому длина UDP датаграммы это полная длина минус длина IP заголовка (которая указана в поле длины заголовка на ).

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

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

Контрольная сумма UDP рассчитывается точно так же, как (глава 3, раздел ) контрольная сумма IP заголовка (сумма 16-битных слов с переполнением), хотя существуют и отличия. Во-первых, UDP датаграмма может состоять из нечетного количества байт, тогда как при расчете контрольной суммы складываются 16-битные слова. При этом в конец датаграммы добавляются нулевые байты заполнения, если это необходимо для расчета контрольной суммы. (Байты заполнения не передаются.)

В UDP и TCP существуют 12-байтовые псевдозаголовки (в UDP датаграммах и TCP сегментах) только для расчета контрольной суммы. Псевдозаголовки содержат в себе определенные поля из IP заголовка. Все это сделано для двойной проверки того, что данные достигли того пункта назначения, которому предназначались (IP не принимает датаграммы, которые не адресованы для данного хоста, и не сможет передать UDP датаграммы, предназначенные для другого верхнего уровня). На рисунке 11.3 показан псевдозаголовок и UDP датаграмма.

Рисунок 11.3 Поля, используемые для расчета контрольной суммы UDP.

На этом рисунке мы специально показали датаграмму с нечетной длиной, в этом случае требуется дополнительный байт для расчета контрольной суммы. Обратите внимание на то, что длина UDP датаграммы, при расчете контрольной суммы, появляется дважды.

Если рассчитанная контрольная сумма равна 0, она хранится как все единичные биты (65535), эти значения эквивалентны в арифметике с поразрядным дополнением (дополнение единицы) (ones-complement). Если переданная контрольная сумма равна 0, это означает, что отправитель не рассчитал контрольную сумму.

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

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

Несмотря на то, что для контрольная сумма UDP - необязательный параметр, она должна рассчитываться всегда. В конце 1980-х годов некоторые производители компьютеров стали по умолчанию отключать расчет контрольной суммы UDP, чтобы увеличить скорость работы сетевой файловой системы (NFS - Network File System), которая использует UDP. Это может быть допустимым в одной локальной сети, где рассчитывается циклический избыточный код для фреймов на канальном уровне (фреймы Ethernet или Token ring), с помощью которого можно определить повреждение фрейма, когда датаграмма проходит через маршрутизаторы. Поверите Вы или нет, но существуют маршрутизаторы, у которых есть ошибки в программном или аппаратном обеспечении и которые изменяют биты в датаграммах, которые они маршрутизируют. Эти ошибки не могут быть выявлены в UDP датаграммах, если отключена опция контрольной суммы. Также необходимо отметить, что некоторые протоколы канального уровня (например, SLIP) не имеют каких-либо форм расчета контрольной суммы для данных в канале.

Требования к хостам Host Requirements RFC требуют, чтобы расчет контрольной суммы UDP был включен по умолчанию. Также они требуют, чтобы принятая контрольная сумма обязательно проверялась, если ее рассчитал отправитель (в том случае, если принятая контрольная сумма не нулевая). Некоторые реализации игнорируют это и проверяют принятую контрольную сумму в том случае, если включена опция расчета исходящей контрольной суммы.

Вывод команды tcpdump

Довольно сложно определить, включена ли опция расчета контрольной суммы UDP на конкретной системе. Обычно приложение не имеет доступа к полю контрольной суммы принятого UDP заголовка. Чтобы решить эту проблему, автор добавил еще одну опцию к программе tcpdump, после чего та стала выдавать полученные контрольные суммы UDP. Если полученное значение равно 0, это означает, что отправитель не рассчитал контрольную сумму.

На рисунке 11.4 показан вывод к трем системам и от тех же трех различных систем в нашей сети. Мы запустили программу sock (), послав единственную UDP датаграмму с 9 байтами данных на стандартный эхо сервер.

>

1 0.0 sun.1900 > gemini.echo: udp 9 (UDP cksum=6e90)
2 0.303755 (0.3038) gemini.echo > sun.1900: udp 9 (UDP cksum=0)

3 17.392480 (17.0887) sun.1904 > aix.echo: udp 9 (UDP cksum=6e3b)
4 17.614371 (0.2219) aix.echo > sun.1904: udp 9 (UDP cksum=6e3b)

5 32.092454 (14.4781) sun.1907 > solaris.echo: udp 9 (UDP cksum=6e74)
6 32.314378 (0.2219) solaris.echo > sun.1907: udp 9 (UDP cksum=6e74)

Рисунок 11.4 Вывод tcpdump, с помощью которого можно определить, включена ли опция контрольной суммы UDP на конкретном хосте.

Отсюда мы видим, что у двух из трех систем опция контрольной суммы UDP включена.

Также обратите внимание на то, что исходящая датаграмма имеет такую же контрольную сумму, как и входящая датаграмма (строки 3 и 4, 5 и 6). На рисунке 11.3, можно заметить, что два IP адреса поменяны местами, так же, как и два номера порта. Другие поля в псевдозаголовке и заголовке UDP остались те же, так как данные были отражены эхом. Это подтверждает, что контрольная сумма UDP (а в действительности, и все контрольные суммы в семействе протоколов TCP/IP) это простая 16-битовая сумма. С ее помощью невозможно определить ошибку, которая заключается в перемене мест двух 16-битных значений.

Немного статистики

[ Mogul 1992] предоставляет некоторую статистическую информацию о появлении ошибок контрольных сумм на довольно загруженном NFS сервере, который работал в течение 40 дней. На рисунке 11.5 приведены статистические данные.

Количество ошибок в контрольных суммах

Приблизительное количество пакетов

Ethernet
IP
UDP
TCP

Рисунок 11.5 Статистика поврежденных пакетов, определенных с использованием контрольных сумм.

В последней колонке приведено приблизительное количество пакетов, так как и другие протоколы используют Ethernet и IP уровень. Например, не все Ethernet фреймы используются IP датаграммами, ARP также использует Ethernet. Не все IP датаграммы используются UDP или TCP, так как ICMP также использует IP.

Обратите внимание на то, что выявлено значительно больше ошибок контрольных сумм TCP, чем UDP. Это скорее всего вызвано тем, что с помощью TCP обычно устанавливаются "дальние" соединения (которые проходят через много маршрутизаторов, мостов и так далее), тогда как UDP траффик обычно локальный.

Поэтому ошибки, приведенные в нижней строке, не всегда имеют отношение к канальному уровню (Ethernet, Token ring). При передаче данных следует всегда включать опцию контрольную сумму в оконечных точках. Однако, если передаваемые данные представляют определенную ценность, не стоит полностью доверять контрольным суммам UDP или TCP, так как это простые контрольные суммы, и они не гарантируют, что защитят данные от всех возможных ошибок.

Простой пример

Мы воспользуемся программой sock, чтобы сгенерировать несколько UDP датаграмм, которые мы просмотрим с использованием tcpdump:

>bsdi % sock -v -u -i -n4 svr4 discard
connected on 140.252.13.35.1108 to 140.252.13.34.9

bsdi % sock -v -u -i -n4 -w0 svr4 discard
connected on 140.252.13.35.1110 to 140.252.13.34.9

В первом случае запуска программы установлен отладочный режим ( -v), при этом можно просмотреть номера динамически назначаемых портов, указан UDP ( -u) вместо TCP по умолчанию и установлен режим источника, опция ( -i) , это означает, что мы будем посылать данные, а не будем читать из стандартного ввода или писать в стандартный вывод. Опция -n4 сообщает о необходимости выдать 4 датаграммы (вместо того, чтобы выдавать по умолчанию 1024) на хост назначения svr4. Сервис discard описан в разделе главы 1. Мы используем размер вывода по умолчанию в 1024 байта на одну запись.

Второй раз мы запустили программу, указав -w0, при этом будут выдаваться датаграммы нулевой длины. На рисунке 11.6 показан вывод команды tcpdump для двух примеров.

>

1 0.0 bsdi.1108 > svr4.discard: udp 1024
2 0.002424 (0.0024) bsdi.1108 > svr4.discard: udp 1024
3 0.006210 (0.0038) bsdi.1108 > svr4.discard: udp 1024
4 0.010276 (0.0041) bsdi.1108 > svr4.discard: udp 1024

5 41.720114 (41.7098) bsdi.1110 > svr4.discard: udp 0
6 41.721072 (0.0010) bsdi.1110 > svr4.discard: udp 0
7 41.722094 (0.0010) bsdi.1110 > svr4.discard: udp 0
8 41.723070 (0.0010) bsdi.1110 > svr4.discard: udp 0

Рисунок 11.6 Вывод команды tcpdump для случая, когда UDP датаграммы посылаются в одном направлении.

В выводе показаны четыре датаграммы размером 1024 байта, за которыми следуют четыре датаграммы нулевой длинны. Каждая датаграмма следует за предыдущей с интервалом в несколько миллисекунд. (Для того чтобы ввести вторую команду, потребовалась 41 секунда.)

До того как была отправлена первая датаграмма, соединения между отправителем и получателем не существовало. (В , где рассказывается о TCP, мы покажем, что перед тем как будет отправлен первый байт данных, должно быть установлено соединение.) Необходимо отметить, что получатель не выдает подтверждение, когда получает данные. Отправитель, в этом примере, не имеет представления о том, получены ли данные на удаленном конце.

И в завершение, обратите внимание, что номер порта источника UDP меняется каждый раз при запуске программы. Сначала порт был 1108, затем 1110. В разделе главы 1 мы показали, что номера динамически назначаемых портов, используемых клиентами, обычно находятся в диапазоне от 1024 до 5000.

Фрагментация IP

На рисунке 11.9 показан формат ICMP ошибки о недоступности для данного случая. Он отличается от формата, приведенного на , потому что биты 16-31 во втором 32-битном слове могут содержать MTU следующей пересылки, вместо того чтобы быть установленными в 0.

Рисунок 11.9 ICMP ошибка о недоступности, когда необходима фрагментация, однако установлен бит "не фрагментировать".

Если маршрутизатор не поддерживает этот новый формат ICMP ошибки, MTU следующей пересылки устанавливается в 0.

Новые требования к маршрутизаторам Router Requirements RFC [ Almquist 1993] указывают на то, что маршрутизатор должен генерировать эту новую форму, когда выдает ICMP сообщение о недоступности.

Проблема, которую мы обсудим, возникла при получении ICMP ошибки при попытке определить MTU SLIP канала с дозвоном между маршрутизатором netb и хостом sun. Мы знаем MTU этого канала от sun к netb, так как, во-первых, это указывается при конфигурации SLIP на хосте sun, во-вторых, мы видели MTU при запуске команды netstat в разделе главы 3. А сейчас мы хотим определить MTU в другом направлении. (В рассказывается как определить MTU с использованием SNMP.) Для каналов точка-точка нет необходимости, чтобы MTU был одним и тем же в обоих направлениях.

Для определения использована следующая техника. Мы запустили ping с хоста solaris на хост bsdi, увеличивая размер пакета данных до тех пор, пока к пакетам не была применена фрагментация. Процесс показан на рисунке 11.10.

Рисунок 11.10 Системы, которые были использованы для определения MTU SLIP канала между netb и sun.

На хосте sun была запущена программа tcpdump, которая позволила посмотреть, как осуществляется фрагментация в SLIP канале. Фрагментация не появлялась и все было отлично до тех пор, пока размер данных в пакете ping не возрос от 500 байт до 600. Входящие эхо запросы были видны (как будто фрагментации не было), однако эхо отклики исчезли.

Чтобы лучше разобраться в происходящем, программа tcpdump также была запущена на bsdi, после чего стало видно, что отправляется и что принимается. На рисунке 11.11 показан вывод.

>

1 0.0 solaris >
2 0.000000 (0.0000) bsdi >
3 0.000000 (0.0000) sun >
need to frag, mtu = 0 (DF)

4 0.738400 (0.7384) solaris > bsdi: icmp: echo request (DF)
5 0.748800 (0.0104) bsdi > solaris: icmp: echo reply (DF)
6 0.748800 (0.0000) sun > bsdi: icmp: solaris unreachable -
need to frag, mtu = 0 (DF)

Рисунок 11.11 Вывод программы tcpdump от ping на bsdi от solaris с IP датаграммой размером в 600 байт.

Во-первых, выражение (DF) в каждой строке означает, что в единицу установлен бит "не фрагментировать" в IP заголовке. Это означает, что Solaris 2.2 нормально устанавливает этот бит в единицу, что является частью механизма определения транспортного MTU.

Строка 1 указывает, что эхо запрос проходит через маршрутизатор netb к sun без фрагментации и с установленным битом DF, поэтому можно сделать вывод, что критичный размер MTU для SLIP хоста netb еще не достигнут.

Также, заметьте из строки номер 2, что DF флаг копируется в каждый эхо отклик. Как раз это и вызвало проблему. Эхо отклик того же размера, что и эхо запрос (чуть больше 600 байт), однако MTU исходящего SLIP интерфейса хоста sun равен 552. Эхо отклик должен быть фрагментирован, однако установлен флаг DF. Это заставляет sun генерировать ICMP ошибку о недоступности и отправлять ее к bsdi (где она уничтожается).

Именно поэтому мы не видели эхо отклики от solaris. Отклики не проходили через sun. На рисунке 11.12 показан путь прохождения пакетов.

Рисунок 11.12 Обмен пакетами для данного примера.

И в завершение, отметим, что выражение mtu=0 в строках 3 и 6 на рисунке 11.11 указывает на то, что sun не возвращает MTU для исходящего интерфейса в ICMP сообщении о недоступности, как показано на рисунке 11.9. (В разделе главы 25 мы решим эту проблему с использованием SNMP и убедимся в том, что MTU SLIP интерфейса netb равен 1500.)

Определение транспортного MTU с использованием Traceroute

Так как большинство систем не поддерживают функцию определения транспортного MTU, мы доработаем программу traceroute () так, чтобы она могла определять транспортный MTU. Мы отправим пакет с установленным битом "не фрагментировать" (don"t fragment). Размер первого отправляемого пакета будет равен MTU исходящего интерфейса. Когда вернется ICMP ошибка "не могу фрагментировать" (can"t fragment), мы уменьшим размер пакета. Если маршрутизатор, отправивший ICMP ошибку, поддерживает новую версию, которая включает MTU исходящего интерфейса в ICMP сообщение, мы используем полученное значение; иначе мы попробуем следующий меньший MTU. Как утверждает RFC 1191 [ Mogul and Deering 1990], существует ограниченное количество значений MTU, наша программа имеет таблицу возможных значений и просто перейдет на следующее меньшее значение.

Попробуем подобный алгоритм с хоста sun на хост slip, зная, что SLIP канал имеет MTU равный 296:

>

sun % traceroute.pmtu slip

outgoing MTU = 1500
1 bsdi (140.252.13.35) 15 ms 6 ms 6 ms
2 bsdi (140.252.13.35) 6 ms
fragmentation required and DF set, trying new MTU = 1492
fragmentation required and DF set, trying new MTU = 1006
fragmentation required and DF set, trying new MTU = 576
fragmentation required and DF set, trying new MTU = 552
fragmentation required and DF set, trying new MTU = 544
fragmentation required and DF set, trying new MTU = 512
fragmentation required and DF set, trying new MTU = 508
fragmentation required and DF set, trying new MTU = 296
2 slip (140.252.13.65) 377 ms 377 ms 377 ms

В этом примере маршрутизатор bsdi не вернул MTU исходящего интерфейса в ICMP сообщении, поэтому мы перейдем на следующее меньшее значение MTU. Первая строка вывода для TTL равного 2 сообщает имя хоста bsdi, однако это происходит из-за того, оно было возвращено маршрутизатором в ICMP ошибке. Последняя строка вывода для TTL равного 2 это как раз то, что мы ожидали.

Не составляет труда модифицировать ICMP код на bsdi, чтобы получить MTU исходящего интерфейса. И если мы сделаем это и вернемся к нашей программе, то получим следующий вывод:

>

sun % traceroute.pmtu slip
traceroute to slip (140.252.13.65), 30 hops max
outgoing MTU = 1500
1 bsdi (140.252.13.35) 53 ms 6 ms 6 ms
2 bsdi (140.252.13.35) 6 ms
fragmentation required and DF set, next hop MTU = 296
2 slip (140.252.13.65) 377 ms 378 ms 377 ms

Здесь нам нет смысла перебирать восемь различных значений MTU, маршрутизатор сообщил нужное значение.

Мировой Internet

Модифицированная версия traceroute была запущена несколько раз на различные хосты по всему миру. С ее помощью было достигнуто 15 стран (включая Антарктику), при этом были использованы различные трансатлантические и транстихоокеанские каналы. Однако, до того как сделать это, мы увеличили MTU SLIP канала с дозвоном между нашей подсетью и маршрутизатором netb (рисунок 11.12) до 1500, как в Ethernet.

Из 18 раз, когда была запущена программа, только в двух случаях траспортный MTU был меньше чем 1500. Один трансатлантический канал имел MTU равный 572 (странное значение, которое даже не приведено в списке в RFC 1191), и маршрутизатор не вернул ICMP ошибку в новом формате. Еще один канал между двумя маршрутизаторами в Японии не обрабатывал фреймы размером 1500 байт, также маршрутизатор не вернул ICMP ошибку в новом формате. После того как MTU было уменьшено до 1006, все заработало.

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

Определение транспортного MTU при использовании UDP

Давайте рассмотрим взаимодействие между приложением, использующим UDP, и механизмом определения транспортного MTU. Нам необходимо посмотреть, что произойдет в том случае, когда приложение отправляет датаграмму, которая слишком велика для некоторого промежуточного канала.

Так как единственная система, которая поддерживает механизм определения транспортного MTU, это Solaris 2.x, мы используем ее как хост источник, чтобы отправить датаграмму размером 650 байт на slip. Так как хост slip находится позади SLIP канала с MTU равным 296, любая UDP датаграмма больше чем 268 байт (296 - 20 - 8) с установленным битом "не фрагментировать" должна вызвать ICMP ошибку "не могу фрагментировать" с маршрутизатора bsdi. На рисунке 11.13 показана топология и MTU каналов.

Рисунок 11.13 Системы, использованные для определения транспортного MTU с использованием UDP.

Следующая команда генерирует десять UDP датаграмм размером 650 байт с интервалом в 5 секунд:

>solaris % sock -u -i -n10 -w650 -p5 slip discard

На рисунке 11.14 показан вывод команды tcpdump. Когда этот пример был запущен, маршрутизатор bsdi был сконфигурирован таким образом, чтобы не возвращать MTU следующей пересылки как часть ICMP ошибки "не могу фрагментировать".

Первая посланная датаграмма с установленным битом DF (строка 1) генерирует ожидаемую ошибку от маршрутизатора bsdi (строка 2). Что интересно, следующая датаграмма, также посланная с установленным битом DF (строка 3), генерирует ту же самую ICMP ошибку (строка 4). Мы ожидали, что эта датаграмма будет послана с выключенным битом DF.

В строке 5 IP наконец понял, что датаграммы в этот пункт назначения не должны посылаться с установленным битом DF, после чего стал фрагментировать датаграммы на хосте источнике. Это поведение отличается от того, что было показано в ранних примерах, где IP отправлял датаграммы, которые он получал от UDP, и при этом маршрутизаторам с более маленькими MTU (bsdi в данном случае) позволялось осуществлять фрагментацию.

>

1 0.0 solaris.38196 > slip.discard: udp 650 (DF)
2 0.004218 (0.0042) bsdi > solaris: icmp:

3 4.980528 (4.9763) solaris.38196 > slip.discard: udp 650 (DF)
4 4.984503 (0.0040) bsdi > solaris: icmp:
slip unreachable - need to frag, mtu = 0 (DF)

5 9.870407 (4.8859) solaris.38196 > slip.discard: udp 650 (frag 47942:552@0+)
6 9.960056 (0.0896) solaris > slip: (frag 47942:106@552)

7 14.940338 (4.9803) solaris.38196 > slip.discard: udp 650 (DF)
8 14.944466 (0.0041) bsdi > solaris: icmp:
slip unreachable - need to frag, mtu = 0 (DF)

9 19.890015 (4.9455) solaris.38196 > slip.discard: udp 650 (frag 47944:552@0+)
10 19.950463 (0.0604) solaris > slip: (frag 47944:106@552)

11 24.870401 (4.9199) solaris.38196 > slip.discard: udp 650 (frag 47945:552@0+)
12 24.960038 (0.0896) solaris > slip: (frag 47945:106@552)

13 29.880182 (4.9201) solaris.38196 > slip.discard: udp 650 (frag 47946:552@0+)
14 29.940498 (0.0603) solaris > slip: (frag 47946:106@552)

15 34.860607 (4.9201) solaris.38196 > slip.discard: udp 650 (frag 47947:552@0+)
16 34.950051 (0.0894) solaris > slip: (frag 47947:106@552)

17 39.870216 (4.9202) solaris.38196 > slip.discard: udp 650 (frag 47948:552@0+)
18 39.930443 (0.0602) solaris > slip: (frag 47948:106@552)

19 44.940485 (5.0100) solaris.38196 > slip.discard: udp 650 (DF)
20 44.944432 (0.0039) bsdi > solaris: icmp:
slip unreachable - need to frag, mtu = 0 (DF)

Рисунок 11.14 Определение транспортного MTU с использованием UDP.

Так как ICMP сообщение "не могу фрагментировать" не содержит MTU следующей пересылки, это означает, что IP решил что всех устраивает MTU равный 576. Первый фрагмент (строка 5) содержит 544 байта UDP данных, 8 байт UDP заголовка и 20 байт IP заголовка, полный размер IP датаграммы составляет 572 байта. Второй фрагмент (строка 6) содержит оставшиеся 106 байт UDP данных и 20-байтный IP заголовок.

К сожалению, следующая датаграмма, строка 7, имеет установленный бит DF, поэтому она отбрасывается bsdi, после чего возвращается ICMP ошибка. Здесь произошло следующее: истек IP таймер, который сообщил IP о необходимости проверить, не увеличился ли транспортный MTU, путем повторной установки бита DF. Мы увидим, что это произойдет снова в строках 19 и 20. Сравнивая времена в строках 7 и 19, где DF бит устанавливается в единицу, мы видим, что проверка на увеличение транспортного MTU осуществляется каждые 30 секунд.

Этот 30-секундный таймер слишком мал. RFC 1191 рекомендует установить значение таймера в 10 минут. Величину таймера можно изменить с помощью параметра ip_ire_pathmtu_interval (приложение E, раздел ). В Solaris 2.2 не существует способа выключить определение транспортного MTU для одного UDP приложения или для всех UDP приложений. Оно может быть включено или выключено только для всей системы в целом с помощью изменения параметра ip_path_mtu_discovery. Как мы видим из данного примера, включение характеристики определения транспортного MTU, когда UDP приложения отправляют датаграммы, которые, возможно, будут фрагментированы, приведет к тому, что датаграмма может быть отброшена.

Максимальный размер датаграммы, воспринимаемый IP уровнем на solaris (576 байт), неверен. На рисунке 11.13 мы видели, что реальный MTU составляет 296 байт. Это означает, что фрагменты, генерируемые solaris, снова фрагментируются на bsdi. На рисунке 11.15 показан вывод tcpdump, полученный на хосте назначения (slip) для первой прибывшей датаграммы (строки 5 и 6 на рисунке 11.14).

>

1 0.0 solaris.38196 > slip.discard: udp 650 (frag 47942:272@0+)
2 0.304513 (0.3045) solaris > slip: (frag 47942:272@272+)
3 0.334651 (0.0301) solaris > slip: (frag 47942:8@544+)
4 0.466642 (0.1320) solaris > slip: (frag 47942:106@552)

Рисунок 11.15 Первая датаграмма, прибывшая на хост slip от solaris.

В этом примере хост solaris не должен фрагментировать исходящие датаграммы, однако должен выключить бит DF и позволить маршрутизатору с меньшим MTU осуществить фрагментацию.

Сейчас мы запустим тот же самый пример, однако изменим поведение маршрутизатора bsdi так, чтобы тот возвращал MTU следующей пересылки в ICMP сообщении "не могу фрагментировать". На рисунке 11.16 показаны первые шесть строк вывода tcpdump.

>

1 0.0 solaris.37974 > slip.discard: udp 650 (DF)
2 0.004199 (0.0042) bsdi > solaris: icmp:

3 4.950193 (4.9460) solaris.37974 > slip.discard: udp 650 (DF)
4 4.954325 (0.0041) bsdi > solaris: icmp:
slip unreachable - need to frag, mtu = 296 (DF)

5 9.779855 (4.8255) solaris.37974 > slip.discard: udp 650 (frag 35278:272@0+)
6 9.930018 (0.1502) solaris > slip: (frag 35278:272@272+)
7 9.990170 (0.0602) solaris > slip: (frag 35278:114@544)

Рисунок 11.16 Определение транспортного MTU с использованием UDP.

И снова мы видим, что две первые датаграммы отправлены с установленным битом DF, на обе получены ICMP ошибки. Сейчас в ICMP ошибке указывается MTU следующей пересылки, который равен 296.

В строках 5, 6 и 7 мы видим, что хост источник осуществляет фрагментацию, как на рисунке 11.14. Если известен MTU следующей пересылки, генерируются только три фрагмента, по сравнению с четырьмя фрагментами, которые генерируются маршрутизатором bsdi на рисунке 11.15.

Взаимодействие между UDP и ARP

Используя UDP, мы можем рассмотреть очень интересное взаимодействие между UDP и типичной реализацией ARP.

Мы используем программу sock, чтобы сгенерировать одну UDP датаграмму с 8192 байтами данных. Мы ожидаем, что в этом случае будет сгенерировано шесть Ethernet фрагментов (см. главы 11). Также, перед запуском программы, мы убедимся в том, что ARP кэш пуст, поэтому перед тем как будет отправлен первый фрагмент, должен произойти обмен ARP запросом и откликом.

>

bsdi % arp -a проверяем, что ARP кэш пуст
bsdi % sock -u -i -n1 -w8192 svr4 discard

Мы ожидаем, что первая датаграмма вызовет отправку ARP запроса. Следующие пять фрагментов, которые генерируются IP, ставят перед нами два вопроса, на которые мы можем ответить, только воспользовавшись tcpdump: будут ли готовы к отправке оставшиеся фрагменты, перед тем как будет получен ARP отклик, если так, что будет делать ARP с этими несколькими пакетами направляемыми на конкретный пункт назначения, пока ожидается ARP отклик? На рисунке 11.17 показан вывод программы tcpdump.

>

1 0.0 arp who-has svr4 tell bsdi
2 0.001234 (0.0012) arp who-has svr4 tell bsdi
3 0.001941 (0.0007) arp who-has svr4 tell bsdi
4 0.002775 (0.0008) arp who-has svr4 tell bsdi
5 0.003495 (0.0007) arp who-has svr4 tell bsdi
6 0.004319 (0.0008) arp who-has svr4 tell bsdi
7 0.008772 (0.0045) arp reply svr4 is-at 0:0:c0:c2:9b:26
8 0.009911 (0.0011) arp reply svr4 is-at 0:0:c0:c2:9b:26
9 0.011127 (0.0012) bsdi > svr4: (frag 10863:800@7400)
10 0.011255 (0.0001) arp reply svr4 is-at 0:0:c0:c2:9b:26
11 0.012562 (0.0013) arp reply svr4 is-at 0:0:c0:c2:9b:26
12 0.013458 (0.0009) arp reply svr4 is-at 0:0:c0:c2:9b:26
13 0.014526 (0.0011) arp reply svr4 is-at 0:0:c0:c2:9b:26
14 0.015583 (0.0011) arp reply svr4 is-at 0:0:c0:c2:9b:26

Рисунок 11.17 Обмен пакетами при отправке по Ethernet UDP датаграммы размером 8192 байта.

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

Затем, когда получен первый ARP отклик (строка 7), отправляется только последний фрагмент (строка 9)! Это означает, что первые пять фрагментов были отброшены. В действительности, это пример обычного функционирования ARP. Большинство реализаций держат только последний пакет, который должен быть отправлен на хост назначения, пока ожидается ARP отклик.

Требования к хостам Host Requirements RFC требуют от реализаций, чтобы они предотвращали лавинообразную рассылку ARP запросов (повторная отправка ARP запросов для одного и того же IP адреса с большой частотой). Рекомендуемая максимальная частота составляет один раз в секунду. Здесь мы видим шесть ARP запросов в течение 4,3 миллисекунды. Требования к хостам Host Requirements RFC требуют, чтобы ARP сохранил по крайней мере один пакет, и это должен быть самый последний пакет. Это как раз то, что мы видели здесь.

Следующая необъяснимая аномальность заключается в том, что svr4 отправил назад семь ARP откликов, а не шесть.

И последнее, про что хочется сказать, tcpdump работал еще 5 минут после того, как вернулся последний ARP отклик, ожидая увидеть, как svr4 пошлет назад ICMP ошибку "время истекло в течение повторной сборки" (time exceeded during reassembly). ICMP сообщение так и не появилось. (Мы показали формат этого сообщения на . Поле код, установленное в 1, указывает на то, что время истекло в течение повторной сборки датаграммы.)

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

Существуют две причины, по которым мы не увидели ICMP сообщение. Во-первых, большинство реализаций Berkeley никогда не генерируют эту ошибку! Эти реализации устанавливают таймер и отбрасывают все фрагменты, когда таймер истечет, однако ICMP ошибка не генерируется. Во-вторых, первый фрагмент - фрагмент со смещением равным 0, содержащий UDP заголовок, не был принят. (Это был первый из пяти пакетов, отброшенных ARP.) Реализация не требует генерировать ICMP ошибку, если первый фрагмент не был принят. Причина заключается в том, что приемник ICMP ошибки не может сказать, который пользовательский процесс отправил датаграмму, которая была отброшена, потому что недоступен заголовок транспортного уровня. А высший уровень (либо TCP приложение, либо UDP приложение) отработает тайм-аут и повторит передачу.

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

Максимальный размер UDP датаграммы

Теоретически максимальный размер IP датаграммы может составлять 65535 байт, что ограничивается 16-битным полем полной длины в IP заголовке (см. ). При длине IP заголовка равной 20 байтам и длине UDP заголовка равной 8 байтам в UDP датаграмме для пользовательских данных остается максимум 65507 байт. В большинстве реализаций, однако, используются датаграммы значительно меньшего размера.

Обычно играют роль два ограничения. Во-первых, программа приложение может быть ограничена программным интерфейсом. Сокеты API (глава 1, раздел ) предоставляют функцию, которая может быть вызвана приложением, чтобы установить размер буферов ввода и вывода. Для UDP сокета этот размер напрямую связан с максимальным размером UDP датаграммы, которая может быть прочитана и записана UDP. В настоящее время большинство систем предоставляют по умолчанию максимальный размер UDP датаграммы, которая может быть прочитана или записана, равный 8192 байтам. (Эта значение установлено в 8192, потому что именно столько по умолчанию читается и записывается системой NFS.)

Следующее ограничение определяется реализацией ядра TCP/IP. Могут существовать характеристики реализации (или ошибки), которые ограничивают размер UDP датаграммы значением меньшим, чем 65535 байт.

Автор экспериментировал с различными размерами UDP датаграмм, используя программу sock. С использованием loopback интерфейса под SunOS 4.1.3, максимальный размер UDP датаграммы был 32767 байт. Использовать большее значение не удавалось. При передаче по Ethernet от BSD/386 к SunOS 4.1.3, максимальный размер IP датаграммы, которую мог принять Sun, составлял 32786 (при этом пользовательских данных было 32758 байт). С использованием loopback интерфейса в Solaris 2.2 максимальный размер IP датаграммы, которая могла быть отправлена и принята, составлял 65535 байт. При передаче от Solaris 2.2 к AIX 3.2.2 удалось передать IP датаграмму максимального размера в 65535 байт.

Усечение датаграмм

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

К сожалению, ответ зависит от программного интерфейса и реализации.

Традиционные версии Berkeley сокет API обрезают датаграммы, отбрасывая любые непоместившиеся данные. Будет ли приложение поставлено в известность, зависит от версии. ( 4.3 BSD Reno и более поздние версии могут уведомить приложение о том, что датаграмма была обрезана.) API сокеты под SVR4 (включая Solaris 2.x) не обрезают датаграммы. Любые непоместившиеся данные последовательно считываются. Приложение не уведомляется о нескольких циклах считывания и ему будет передана одна UDP датаграмма. TLI API не отбрасывают данные. Вместо этого возвращается флаг, указывающий на то, что данных больше, чем можно считать за один раз, поэтому приложение начинает последовательно считывать оставшуюся датаграмму.

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

ICMP ошибка подавления источника

Воспользовавшись UDP, можно сгенерировать ICMP ошибку "подавление источника" (source quench). Эта ошибка может быть сгенерирована системой (маршрутизатором или хостом), когда она принимает датаграммы быстрее, чем эти датаграммы могут быть обработаны. Обратите внимание на выражение "могут быть". Система не требует послать подавление источника, даже если буферы переполнены и датаграммы отбрасываются.

На рисунке 11.18 показан формат ICMP ошибки подавления источника. Мы имеем идеальную возможность сгенерировать подобную ошибку в нашей тестовой сети. Мы можем посылать датаграммы с bsdi на маршрутизатор sun по Ethernet, причем эти датаграммы должны быть смаршрутизированы через SLIP канал. Так как SLIP канал примерно в тысячу раз медленнее чем Ethernet, мы легко можем переполнить буфер. Следующая команда посылает 100 датаграмм размером 1024 байта с хоста bsdi через маршрутизатор sun на solaris. Мы отправляем датаграммы на стандартный discard сервис, где они будут игнорированы:

>bsdi % sock -u -i -w1024 -n100 solaris discard

Рисунок 11.18 ICMP ошибка подавления источника.

На рисунке 11.19 показан вывод команды tcpdump, соответствующий этой команде.

>

1 0.0 bsdi.1403 > solaris.discard: udp 1024
26 строк не показано
27 0.10 (0.00) bsdi.1403 > solaris.discard: udp 1024
28 0.11 (0.01) sun > bsdi: icmp: source quench

29 0.11 (0.00) bsdi.1403 > solaris.discard: udp 1024
30 0.11 (0.00) sun > bsdi: icmp: source quench
142 строки не показано
173 0.71 (0.06) bsdi.1403 > solaris.discard: udp 1024
174 0.71 (0.00) sun > bsdi: icmp: source quench

Рисунок 11.19 ICMP подавление источника от маршрутизатора sun.

Из этого вывода мы удалили множество строк. Первые 26 датаграмм приняты без ошибок: мы показали вывод только для первой. Начиная с 27-й датаграммы, каждый раз, когда отправляется датаграмма, мы получаем ошибку подавление источника. Всего было 26+(74х2)=174 строк вывода.

Из нашего расчета пропускной способности последовательной линии, приведенного в разделе главы 2, видно, что на передачу датаграммы размером 1024 байта со скоростью 9600 бит/сек потребуется больше одной секунды. (В нашем примере для этого потребуется больше времени, так как датаграмма размером 20+8+1024 байт будет фрагментирована, потому что MTU SLIP канала от sun к netb составляет 552 байта.) Однако, из показателей времени, приведенных на рисунке 11.19, мы видим, что маршрутизатор sun получил все 100 датаграмм за время меньше чем одна секунда, перед тем как первая была отправлена в SLIP канал. При этом понятно, что мы использовали множество его буферов.

Несмотря на то, что RFC 1009 требует, чтобы маршрутизатор генерировал ошибки подавления источника, когда переполняются его буферы, новые требования к маршрутизаторам Router Requirements RFC [ Almquist 1993] меняют это положение и говорят, что маршрутизатор не должен генерировать ошибки подавления источника.

Следующий момент, на который необходимо обратить внимание в примере, заключается в том, что программа sock никогда не получала уведомлений о том, что источник подавлен, или если и получала их, то игнорировала. Это говорит о том, что реализации BSD обычно игнорируют полученные сообщения о подавлении источника в случае использования протокола UDP. (В случае TCP, при получении уведомления передача данных по соединению, для которого сгенерировано подавление источника, замедляется. Мы это обсудим в разделе главы 21.) Проблема заключается в том, что процесс, который сгенерировал данные, которые, в свою очередь, вызвали подавление источника, может уже завершиться, когда будет принято сообщение о подавлении источника. И действительно, если мы используем программу time в Unix, чтобы оценить, как долго работает программа sock, то узнаем, что она проработала всего лишь около 0,5 секунды. Однако на рисунке 11.19 мы видели, что некоторые сообщения о подавлении источника были получены через 0,71 секунды после отправки первой датаграммы, то есть уже после того, как процесс прекратил работу. Что произойдет, если наша программа выдаст 100 датаграмм и завершится. Не все 100 датаграмм будут посланы - некоторые из них будут стоять в выходной очереди.

Этот пример доказывает, что UDP - ненадежный протокол и показывает важность контроля за потоком данных (flow control). Несмотря на то, что программа sock успешно выдала в сеть 100 датаграмм, только 26 достигли пункта назначения. Остальные 74 скорее всего были отброшены промежуточным маршрутизатором. Если приложение не поддерживает какую-либо форму уведомлений, отправитель не знает, принял ли получатель данные.

Сервер UDP

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

Обычно когда клиент стартует, он сразу же устанавливает соединение с одним сервером. Сервера, с другой стороны, стартуют и затем "засыпают", ожидая прибытия запроса от клиента. В случае UDP, сервер "просыпается", когда прибывает датаграмма от клиента, эта датаграмма может содержать запрос в какой-либо форме.

Мы не будем рассматривать аспекты программирования клиентов и серверов ( описывает все более подробно), однако рассмотрим характеристики протокола UDP, которые оказывают влияние на разработку и реализацию сервера, использующего UDP. (Мы обсудим подробности TCP сервера в разделе главы 18.) Некоторые характеристики мы обсудим в зависимости от реализаций UDP, которые будут использоваться, а также рассмотрим характеристики, которые являются общими для большинства реализаций.

IP адрес клиента и номер порта

От клиента прибывает UDP датаграмма. IP заголовок содержит IP адреса источника и назначения, а UDP заголовок содержит номера портов UDP источника и назначения. Когда приложение получает UDP датаграмму, операционная система должна сообщить ему, кто послал сообщение - IP адрес источника и номер порта.

Эта характеристика позволяет одному UDP серверу обрабатывать несколько клиентов. Каждый отклик отправляется тому клиенту, который послал запрос.

IP адрес назначения

Некоторым приложениям необходимо знать, кому предназначена датаграмма, то есть IP адрес назначения. Например, требования к хостам Host Requirements RFC определяют, что TFTP сервер должен игнорировать принятые датаграммы, которые рассылаются на широковещательный адрес. (Мы опишем широковещательную адресацию в , а TFTP в .)

Это означает, что операционная система должна передать IP адрес назначения из принятой UDP датаграммы в приложение. К сожалению, не все реализации предоставляют эту возможность.

Сокеты API предоставляют эту возможность с использованием опции IP_RECVDSTADDR. Из систем, которые описываются в тексте, только BSD/386, 4.4BSD и AIX 3.2.2 поддерживают эту опцию. SVR4, SunOS 4.x и Solaris 2.x не поддерживают.

Входная очередь UDP

В разделе главы 1 мы говорили, что большинство UDP серверов могут обслуживать все запросы к клиентам с использованием одного UDP порта (заранее известные порты серверов).

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

Однако существует вероятность, в случае если очередь переполнена, что модуль UDP в ядре отбросит входящие датаграммы. Мы можем пронаблюдать это с помощью следующего эксперимента. Стартуем нашу программу sock на хосте bsdi, запустив таким образом UDP сервер:

>

bsdi % sock -s -u -v -E -R256 -r256 -P30 6666
from 140.252.13.33, to 140.252.13.63: 1111111111 от sun на широковещательный адрес
from 140.252.13.34, to 140.252.13.35: 4444444444444 от svr4 на персональный адрес

Мы использовали следующие флаги: -s, запускает программу в роли сервера, -u для UDP, -v, печатает IP адрес клиента, и -E печатает IP адрес назначения (в данном случае система это позволяет). В дополнение, мы установили приемный буфер UDP для этого порта в 256 байт ( -R), вместе с размером, который может быть прочитан каждым приложением ( -r). Флаг -P30 сообщает о необходимости подождать 30 секунд после очистки UDP порта, перед считыванием первой датаграммы. Это дает нам время стартовать клиентов на двух других хостах, послать некоторые датаграммы и посмотреть, как работает очередь приема.

После того как сервер стартован и прошла 30-секундная пауза, мы стартуем одного клиента на хосте sun и посылаем три датаграммы:

>

sun % sock -u -v 140.252.13.63 6666 на широковещательный адрес Ethernet
connected on 140.252.13.33.1252 to 140.252.13.63.6666
1111111111 11 байт данных (с символом новой строки)
222222222 10 байт данных (с символом новой строки)
33333333333 12 байт данных (с символом новой строки)

Адрес назначения это широковещательный адрес (140.252.13.63). Затем мы стартовали еще одного клиента на хосте svr4 и послали еще три датаграммы:

>

svr4 % sock -u -v bsdi 6666
connected on 0.0.0.0.1042 to 140.252.13.35.6666
4444444444444 14 байт данных (с символом новой строки)
555555555555555 16 байт данных (с символом новой строки)
66666666 9 байт данных (с символом новой строки)

Первое, на что необходимо обратить внимание в интерактивном выводе, показанном ранее на bsdi, что только две датаграммы были приняты приложением: первая от sun, состоящая из всех единиц, и первая от svr4, состоящая из всех четверок. Остальные четыре датаграммы были отброшены.

Вывод команды tcpdump на рисунке 11.20 показывает, что все шесть датаграмм были доставлены к хосту назначения. Датаграммы прибыли от двух клиентов в обратном порядке: первая от sun, затем от svr4 и так далее. Также мы можем заметить, что все шесть были доставлены примерно за 12 секунд, в течение 30-секундного периода пока сервер "спал".

>

1 0.0 sun.1252 > 140.252.13.63.6666: udp 11
2 2.499184 (2.4992) svr4.1042 > bsdi.6666: udp 14
3 4.959166 (2.4600) sun.1252 > 140.252.13.63.6666: udp 10
4 7.607149 (2.6480) svr4.1042 > bsdi.6666: udp 16
5 10.079059 (2.4719) sun.1252 > 140.252.13.63.6666: udp 12
6 12.415943 (2.3369) svr4.1042 > bsdi.6666: udp 9

Рисунок 11.20 Вывод tcpdump для UDP датаграмм, посланных двумя клиентами.

Также необходимо отметить, что c опцией -E сервер может узнать IP адрес назначения каждой датаграммы. Сервер может выбирать, что сделать с первой принятой датаграммой, которая была отправлена на широковещательный адрес.

В этом примере необходимо обратить внимание еще на некоторые особенности. Во-первых, приложение не сообщило, когда была переполнена входная очередь. Лишние датаграммы UDP просто отбросил. Также в выводе tcpdump мы видим, что ничего не было отправлено клиенту обратно, чтобы сообщить ему о том, что датаграммы были отброшены. Не было послано ничего похожего на ICMP сообщение подавления источника, абсолютно ничего. И в заключение, хочется отметить, что входная очередь UDP функционирует по принципу FIFO (первый вошел, первый вышел), тогда как, что мы видели в разделе этой главы, входная очередь ARP - LIFO (последний зашел, первый вышел).

Ограничение локального IP адреса

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

>sun % sock -u -s 7777

Затем мы воспользуемся командой netstat, чтобы посмотреть состояние этой конечной точки:

>

sun % netstat -a -n -f inet
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address Foreign Address (state)
udp 0 0 *.7777 *.*

В этом выводе мы удалили много строк и оставили только те, которые нам интересны. Флаг -a сообщает о всех конечных точках сети. Флаг -n печатает IP адреса в десятичном представлении, вместо того чтобы использовать DNS и конвертировать адреса в имена, а также печатает номера портов вместо имен сервисов. Опция -f inet сообщает только о точках TCP и UDP.

Локальный адрес напечатан как *.7777, где звездочка означает, что в качестве локального IP адреса может быть подставлен любой адрес.

Когда сервер создает свою конечную точку, он может указать один из локальных IP адресов хоста, включая один из его широковещательных адресов в качестве локального IP адреса конечной точки. При этом входящая UDP датаграмма будет передана на конечную точку только в том случае, если IP адрес назначения совпадет с указанным локальным адресом. С помощью программы sock можно указать IP адрес перед номером порта, и этот IP адрес становится локальным IP адресом для конечной точки. Например,

>sun % sock -u -s 140.252.1.29 7777

из датаграмм, прибывающих на SLIP интерфейс, выбирает датаграммы с адресом 140.252.1.29. Вывод команды netstat будет выглядеть следующим образом:

>


udp 0 0 140.252.1.29.7777 *.*

Если мы постараемся послать на этот сервер датаграмму с хоста bsdi, адрес которого 140.252.13.35, по Ethernet, вернется ICMP ошибка о недоступности порта. Сервер никогда не увидит эту датаграмму. На рисунке 11.21 это показано более подробно.

>

1 0.0 bsdi.1723 > sun.7777: udp 13
2 0.000822 (0.0008) sun > bsdi: icmp: sun udp port 7777 unreachable

Рисунок 11.21 Отказ обработки UDP датаграммы, вызванный несовпадением локального адреса сервера.

Существует возможность запустить другие сервера для этого же порта, каждый с собственным локальным IP адресом. Однако, приложение должно разрешить системе повторно использовать тот же самый номер порта.

Должна быть указана опция сокета в API SO_REUSEADDR. Это делается нашей программой sock с помощью опции -A.

На хосте sun мы можем стартовать пять различных серверов на одном и том же UDP порте (8888):

>

sun % sock -u -s 140.252.1.29 8888 для канала SLIP
sun % sock -u -s -A 140.252.13.33 8888 для Ethernet
sun % sock -u -s -A 127.0.0.1 8888 для loopback интерфейса
sun % sock -u -s -A 140.252.13.63 8888 для широковещательных запросов Ethernet
sun % sock -u -s -A 8888 для всего остального (метасимволы в IP адресе)

Ожидалось, что первый из серверов будет запущен с флагом -A, который сообщает системе о том, что можно повторно использовать тот же самый номер порта. Вывод команды netstat показывает следующие пять серверов:

>

Proto Recv-Q Send-Q Local Address Foreign Address (state)
udp 0 0 *.8888 *.*
udp 0 0 140.252.13.63.8888 *.*
udp 0 0 127.0.0.1 8888 *.*
udp 0 0 140.252.13.33 8888 *.*
udp 0 0 140.252.1.29 8888 *.*

В этом сценарии только датаграммы, направляющиеся на адрес 140.252.1.255, будут попадать на сервер с символами подстановки, используемыми в качестве локального IP адреса, потому что другие четыре сервера охватывают все возможные адреса.

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

Ограничение внешних IP адресов

Во всех выводах команды netstat, которую мы показывали ранее, удаленные IP адреса и удаленные номера портов показаны как *.*. Это означает, что конечная точка воспримет входящие UDP датаграммы с любого IP адреса и любого номера порта. В большинстве реализаций конечным точкам UDP позволяется ограничивать удаленные адреса.

Другими словами, конечная точка может воспринимать только UDP датаграммы от указанного IP адреса и номера порта. Наша программа sock использует опцию -f, чтобы указать удаленный IP адрес и номер порта:

>sun % sock -u -s -f 140.252.13.35.4444 5555

При этом удаленный IP адрес устанавливается в 140.252.13.35 (наш хост bsdi), а удаленный номер порта в 4444. Заранее-известный порт сервера 5555. Если мы запустим netstat, то увидим, что локальный IP адрес также установлен, даже если мы не указывали его непосредственно:

>

Proto Recv-Q Send-Q Local Address Foreign Address (state)
udp 0 0 140.252.13.33.5555 140.252.13.35.4444

Это побочный эффект указания удаленного IP адреса и удаленного номера порта в системах Berkeley: если локальный IP адрес не был выбран при установке удаленного адреса, локальный адрес выбирается автоматически. В качестве локального IP адреса устанавливается IP адрес интерфейса, который выбирается с помощью IP маршрутизации для достижения указанного удаленного IP адреса. И действительно, в этом примере IP адрес sun для Ethernet, который подключен к удаленному адресу, это 140.252.13.33.

На рисунке 11.22 приведены три типа адресов и портов, которые UDP сервер может установить для самого себя.

Рисунок 11.22 Указание локального и удаленного IP адресов и номера порта для UDP сервера.

Во всех случаях lport это заранее-известный порт сервера, а localIP должен быть IP адресом локального интерфейса. Порядок, в котором расположены три строки на рисунке 11.22, показывает тот порядок, в котором UDP модуль старается определить, на которую локальную конечную точку принята входящая датаграмма. Наиболее жесткая связь адреса с портом (первая строка) выбирается в первую очередь, а менее жесткая (последняя строка, где и IP адрес, и номер порта указаны в виде метасимволов) выбирается последней.

Множественный прием на порт

Несмотря на то, что это не описано в RFC, большинство реализаций позволяют только одному приложению в одно и то же время быть связанным с одним локальным адресом и номером UDP порта. Когда UDP датаграмма прибывает на хост назначения на свой IP адрес и номер порта, одна копия доставляется в единственную конечную точку. IP адрес конечной точки может быть представлен в виде символов подстановки, как было показано ранее.

Например, в SunOS 4.1.3 мы стартовали один сервер на порт 9999 с локальным IP адресом в виде символов подстановки:

>sun % sock -u -s 9999

Если затем попробовать стартовать еще один сервер с тем же локальным адресом в виде символов подстановки и с тем же портом, это не сработает, даже если мы укажем опцию -A:

>

sun % sock -u -s 9999 так получиться не должно
can"t bind local address: Address already in use

sun % sock -u -s -A 9999 поэтому мы указали флаг -A
can"t bind local address: Address already in use

Для систем, которые поддерживают групповую адресацию (см. ), все обстоит иначе. Несколько конечных точек могут использовать один и тот же локальный адрес и номер UDP порта, однако приложение должно сообщить API, что это допустимо (флаг -A использует опцию сокета SO_REUSEADDR).

4.4BSD, которая поддерживает групповую адресацию, требует, чтобы приложение установило другую опцию сокета ( SO_REUSEPORT), чтобы позволить нескольким конечным точкам делить один и тот же порт. Более того, каждая конечная точка должна указать эту опцию, включая первую конечную точку, которая использует этот порт.

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

Краткие выводы

UDP это простой протокол. Официальная спецификация RFC 768 [ Postel 1980] состоит всего лишь из трех страниц. Сервисы, которые он предоставляет пользовательским процессам, находящиеся над и позади IP, это номера портов и необязательные контрольные суммы. Мы использовали UDP, чтобы просмотреть расчет контрольных сумм и посмотреть, как осуществляется фрагментация.

Затем мы рассмотрели ICMP ошибку о недоступности, которая является частью новой характеристики определения транспортного MTU (см. главу 2, раздел ). Мы рассмотрели определение транспортного MTU с использованием Traceroute и UDP. Также рассмотрен процесс взаимодействия UDP и ARP.

Мы убедились, что ICMP ошибка подавления источника может быть послана системой, которая принимает IP датаграммы быстрее, чем может обработать. Существует возможность легко генерировать эти ICMP ошибки с использованием UDP.

Упражнения

  1. В разделе этой главы мы вызвали фрагментацию в Ethernet, записав UDP датаграмму с размером пользовательских данных в 1473 байта. Какой наименьший размер пользовательских данных может вызвать фрагментацию в Ethernet, если используется инкапсуляция IEEE 802 (глава 2, раздел )?
  2. Прочитайте RFC 791 [ Postel 1981a] и скажите, почему все фрагменты кроме последнего должны иметь длину кратную 8 байтам.
  3. Представьте себе Ethernet и UDP датаграмму с 8192 байтами пользовательских данных. Сколько фрагментов будет передано и какова будет длина смещения для каждого фрагмента?
  4. Продолжая предыдущий пример, представьте себе, что эти датаграммы затем передаются в SLIP канал с MTU равным 552. Вам необходимо помнить, что количество данных в каждом фрагменте (все кроме IP заголовка) должно быть кратно 8 байтам. Сколько фрагментов передано и каковы смещение и длина каждого фрагмента?
  5. Приложение, использующее UDP, посылает датаграмму, которая фрагментирована на 4 части. Представьте себе, что фрагменты 1 и 2 достигли своего пункта назначения, тогда как фрагменты 3 и 4 были потеряны. Приложение отрабатывает тайм-аут, а затем, через 10 секунд, повторяет передачу UDP датаграммы. Эта датаграмма фрагментируется точно так же, как и при первой передаче (то же смещение и та же длина). Теперь представьте, что фрагменты 1 и 2 потеряны, однако фрагменты 3 и 4 достигли своего пункта назначения. Таймер повторной сборки на принимающем хосте установлен в 60 секунд, поэтому когда фрагменты 3 и 4 прибыли на конечный пункт назначения, фрагменты 1 и 2 из первой передачи еще не были отброшены. Может ли получатель собрать IP датаграмму из этих четырех фрагментов?
  6. Как Вы можете узнать, что фрагменты на рисунке 11.15 действительно соответствуют строкам 5 и 6 на рисунке 11.14?
  7. ), жесткая и свободная маршрутизация от источника (глава 8, раздел ). Как Вы считаете, как при фрагментации обрабатываются эти опции? Сопоставьте Ваш ответ с RFC 791.
  8. На мы сказали, что входящие 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.

    Протокол 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-пакета

    Назначение полей udp пакета:

    Номер порта отправителя – Source Port (16 бит) – содержит номер порта, с которого был отправлен пакет, когда это имеет значение (например, отправитель ожидает ответа). Если это поле не используется, оно заполняется нулями.

    Номер порта назначения – Destination Port (16 бит) – содержит номер порта, на который будет доставлен пакет.

    Длина – Length (16 бит) – содержит длину данной дейтаграммы в байтах, включая заголовок и данные.

    Поле контрольной суммы – Checksum (16 бит) – представляет собой побитное дополнение 16-битной суммы 16-битных слов. В вычислении суммы участвуют: данные пакета с полями выравнивания по 16-битной границе (нулевые), заголовок UDP-пакета, псевдозаголовок (информация от IP-протокола).

    Протокол tcp

    Протокол TCP (Transmission Control Protocol – протокол управления передачей) является ориентированным на соединение транспортным протоколом с надежной доставкой данных. Поэтому он имеет жесткие алгоритмы обнаружения ошибок, разработанные для обеспечения целостности передаваемых данных.

    Для обеспечения надежной доставки применяется последовательная нумерация и подтверждение. С помощью последовательной нумерации определяется порядок следования данных в пакетах и выявляются пропущенные пакеты. Последовательная нумерация с подтверждением позволяет организовать надежную связь, которая называется полным дуплексом (full duplex). Каждая сторона соединения обеспечивает собственную нумерацию для другой стороны.

    TCP – является байтовым последовательным протоколом. В отличие от пакетных последовательных протоколов, он присваивает последовательный номер каждому передаваемому байту пакета, а не каждому пакету в отдельности.

    Destination Port

    Acknowledgement Number

    Рис. Формат заголовка TCP-пакета