Реализация Reliable Udp протокола для .Net / Хабр

Вступление

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

NAT

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

механизму преобразования сетевых адресов

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

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

Один из наиболее эффективных методов для установления peer-to-peer соединения между устройствами находящимися в различных частных сетях называется «hole punching». Этот техника чаще всего используется с приложениями на основе UDP протокола.

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

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

Сразу хочу заметить, что существует техника TCP hole punching, для установления TCP соединений между узлами в разных частных сетях, но ввиду отсутствия поддержки её многими NAT устройствами она обычно не рассматривается как основной способ соединения таких узлов.

Далее в этой статье я буду рассматривать только реализацию протокола гарантированной доставки. Реализация техники UDP hole punching будет описана в следующих статьях.

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<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // начать асинхронную отправку сообщения
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // получить результат асинхронной отправки
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // очистить ресурсы
  public void Dispose()    
}


Получение сообщения осуществляется по подписке. Сигнатура делегата для метода обратного вызова:

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

Сообщение:

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

Для подписки на конкретный тип сообщений и/или на конкретного отправителя используются два необязательных параметра: ReliableUdpMessageTypes messageType и IPEndPoint ipEndPoint.

Типы сообщений:

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)

Глава 9 протокол udp / tcp/ip архитектура, протоколы, реализация (включая ip версии 6 и ip security)

  • 9.1 Введение
  • 9.1.1 Широковещательные и многоадресные рассылки
  • 9.2 Порты приложений
  • 9.3 Адреса socket
  • 9.4 Механизмы протокола UDP
  • 9.4.1 Заголовок UDP
  • 9.4.2 Контрольная сумма
  • 9.4.3 Другие функции UDP
  • 9.4.4 Пример сообщения UDP
  • 9.5 Нагрузки в UDP
  • 9.6 Дополнительная литература
  • После знакомства с физическим перемещением битов в носителе и маршрутизацией датаграмм в Интернете, настало время рассмотреть службы для приложений, связанные с пересылкой данных. Начнем с протокола пользовательских датаграмм (User Datagram Protocol — UDP). Это достаточно простой протокол, позволяющий приложениям обмениваться отдельными сообщениями.

    Для каких целей используются эти службы? Существует множество приложений, построенных совершенно естественным способом поверх UDP. Так можно, например, реализовать простую систему просмотра базы данных. Кроме того, мы уже упоминали о системе DNS, сформированной на основе UDP (см. рис. 9.1).

    Реализация Reliable Udp протокола для .Net / Хабр

    Рис. 9.1. Вопрос и ответ DNS

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

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

    Иногда это приводит к дублированию запросов на сервере. Если приложение включит в свое сообщение идентификатор транзакции, сервер сможет распознать дублирование и исключить дополнительную копию сообщения. За эти действия ответственно само приложение, а не UDP.

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

    Что происходит после прибытия данных в хост назначения? Как выполняется их доставка в нужное приложение (процесс)?

    На рис. 9.2 видно, что для каждого уровня существует идентификатор протокола, указывающий операции, выполняемые над данными. На втором уровне тип Ethernet X’08-00 в заголовке кадра показывает, что кадр нужно передать в IP. На третьем уровне поле протокола в заголовке IP указывает протокол четвертого уровня, куда нужно переслать данные (например, 6 для TCP или 17 для UDP).

    Реализация Reliable Udp протокола для .Net / Хабр

    Рис. 9.2. Пересылка данных до уровня приложений

    Хост может участвовать одновременно в нескольких коммуникациях. Так как же из общего потока выделяется датаграмма UDP и доставляется на нужный уровень приложения? Такой процесс пересылки данных в требуемый процесс часто называют демультиплексированием. Ответ состоит в том, что каждой конечной коммуникационной точке UDP присвоен 16-разрядный идентификатор, называемый номером порта. Термин “порт” не очень удачен для данного идентификатора. Порт для клиентской и серверной частей приложения не имеет никакого отношения к портам оборудования и физическому пути пересылки данных).

    Порты с номерами от 0 до 1023 зарезервированы для стандартных служб. Такие порты называются общеизвестными (well-known). Их использование позволяет клиенту идентифицировать службу, к которой он хочет получить доступ. Например, доступ к DNS (которая основана на UDP) производится через общеизвестный порт 53.

    Кто назначает общеизвестные порты? Как не трудно догадаться, этим занимается IANA. Номера портов для определенных приложений регистрируются этой организацией и публикуются в документе RFC Assigned Numbers (присвоенные номера). Сокращенный список портов UDP из текущего документа RFC Assigned Numbers показан в таблице 9.1.

    Похожее:  Meri kei intouch ru личный кабинет

    Таблица 9.1 Примеры общеизвестных портов UDP

    Несколько общеизвестных служб обеспечивает модули для тестирования, отладки и измерений. Например, echo (эхо) с портом 7, соответствуя своему имени, возвращает любую посланную на этот порт датаграмму. Служба Discard (отмена) порта 9, наоборот, удаляет из сети любую посланную на этот порт датаграмму. Character generator (генератор символов) отвечает на любое сообщение датаграммой, содержащей от 0 до 512 байт. Количество байт выбирается случайным образом.

    Служба quote of the day (цитата дня) отвечает на любую датаграмму определенным сообщением, например, в некоторых системах программа fortune выводит при регистрации “мудрые” советы (в данном примере приведена фраза Уинстона Черчилля: “Человек может случайно споткнуться об истину, но в большинстве случаев не замечает ее и сосредоточенно продолжает дальнейший поиск”.):

    > fortuneChurchill’s Commentary on Man: Man will occasionally stumble over the truth, but most of the time he will pick himself up and continue on.

    Служба daytime (время дня) отвечает на любые датаграммы сообщением, содержащим текущую дату и время в формате ASCII. Такой формат можно прочитать на экране без дополнительных преобразований. Иначе ведет себя служба Network Time Protocol (NTP), обеспечивающая надежный метод синхронизации компьютеров сети.

    Сервер BOOTP и клиент этой службы используются для неконфигурируемых устройств. Рабочая станция может получить для себя IP-адрес, свою маску адреса, узнать местоположение маршрутизатора по умолчанию, адреса наиболее важных серверов сети и, при необходимости, имя и местоположение на сервере boot загружаемого программного файла конфигурации. Программное обеспечение в рабочую станцию поступает через протокол Trivial File Transfer Protocol (см. главу 14).

    Мы уже знаем, что сервер имен доступен через порт 53 и команду nslookup. Порты 161 и 162 используются протоколом Simple Network Management Protocol.

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

    Оставшиеся номера портов (выше 1023) предоставляются клиентам от программного обеспечения хоста по мере необходимости. Выделение предусматривает следующие шаги:

    1. Пользователь запускает клиентскую программу (например, nslookup).

    2. Клиентский процесс исполняет системную подпрограмму, имеющую смысл: “Я хочу выполнить коммуникацию UDP. Предоставьте мне порт”.

    3. Системная подпрограмма выбирает неиспользованный порт из пула доступных портов и предоставляет его клиентскому процессу.

    Можно видеть, что TCP также идентифицирует источник и назначение своим 16-разрядным идентификатором порта. Например, порт 21 применяется для доступа к службе пересылки файлов, а порт 23 — для службы регистрации telnet.

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

    Используемая для коммуникации комбинация IP-адреса и порта называется адресом socket (дословно — гнездо, разъем). Отметим, что адрес socket обеспечивает для сервера или клиента всю информацию, необходимую для идентификации партнера по коммуникации.

    Заголовок IP содержит IP-адреса источника и назначения. Заголовки UDP и TCP содержат номера портов источника и назначения. Следовательно, каждое сообщение UDP или TCP несет в себе адрес socket для источника и назначения.

    Ниже приведен результат выполнения команды netstat -па, выводящей локальные и удаленные адреса socket для текущих активных коммуникаций с системой tigger. Адреса socket записаны в форме IP-адрес.номер_порта.

    > netstat -naActive Internet connections (including servers)Proto Recv-Q Send-Q Local Address       Foreign Address     (state)Tcp      0      0   127.0.0.1.1340      127.0.0.1.111       TIME_WAITTcp      0      0   128.121.50.145.25   128.252.223.5.1526  SYN_RCVDTcp      0      0   128.121.50.145.25   148.7.9.160.65.3368 ESTABLISHEDTcp      0    438   128.121.50.145.23   130.132.57.246.2219 ESTABLISHEDTcp      0      0   128.121.50.145.25   192.5.5.1.4022      TIME_WAITTcp      0      0   128.121.50.145.25   141.218.1.100.3968  TIME_WAITTcp      0      0   128.121.50.145.25   35.8.2.2.3722       TIME_WAITTcp      0      0   128.121.50.145.1338 165.247.48.4.25     ESTABLISHEDTcp      0      0   128.121.50.145.25   128.173.4.8.3626    ESTABLISHEDTcp      0      0   128.121.50.145.25   192.48.96.14.3270   ESTABLISHED. . .Udp      0      0   *.7                 *.*Udp      0      0   *.9                 *.*Udp      0      0   *.37                *.*Udp      0      0   *.19                *.*Udp      0      0   *.111               *.*. . .

    Например, выделенный рамкой элемент показывает сеанс регистрации TCP из порта клиента 2219 с IP-адресом 130.132.57.246 на стандартный порт telnet с номером 23 и адресом 128.121.50.145. Строки, подобные *.7 и *.9, представляют службы UDP на tigger, ожидающие запросов от клиентов.

    Какой механизм необходим для запуска протокола User Datagram Protocol? Прежде всего, UDP должен быть присвоен уникальный идентификатор протокола (17). Это значение будет помещаться в поле протокола IP с названием Protocol во всех исходящих сообщениях UDP. Входящие сообщения со значением 17 в поле протокола IP доставляются в UDP. Протокол UDP формирует сообщение, добавляя простой заголовок к данным от приложения. В этом заголовке указываются номера портов источника и назначения.

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

    Реализация Reliable Udp протокола для .Net / Хабр

    Рис. 9.3. Заголовок UDP

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

    В UDP контрольная сумма вычисляется как комбинация специально сформированного псевдозаголовка (pseudo header), содержащего некоторую информацию IP, заголовка UDP и данных из сообщения.

    Формат псевдозаголовка и его участие в вычислении контрольной суммы показаны на рис. 9.4. Отметим, что адрес источника, адрес назначения и поле протокола заимствуются из заголовка IP.

    Реализация Reliable Udp протокола для .Net / Хабр

    Рис. 9.4. Поля псевдозаголовка для контрольной суммы UDP

    Использование контрольной суммы в коммуникации не является обязательным. Когда она не применяется, поле имеет нулевое значение. Если же контрольная сумма была вычислена и равна нулю, такое значение представляется последовательностью единиц.

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

    Рис. 9.5 содержит совмещенный вывод IP и UDP частей запроса и соответствующих им ответов. Этот результат получен в мониторе локальной сети Sniffer компании Network General. Запрос содержал требование вывода статуса информации и был послан хостом на сетевую станцию управления. Часть для данных в сообщениях запроса и ответа не приведена.

    Реализация Reliable Udp протокола для .Net / Хабр

    Рис. 9.5. Заголовки IP и UDP для запроса и ответа

    Запрос был послан из IP-адреса 128.1.1.1 и порта UDP с номером 1227 на IP-адрес назначения 128.1.1.10 и 161-й порт UDP (запросы сетевого обслуживания всегда направляются на порт UDP с номером 161).

    В обоих заголовках IP поле протокола имеет значение 17, что указывает на использование протокола UDP. Контрольная сумма UDP не вычисляется для запроса, но присутствует в ответе.

    Анализатор Sniffer распознает, что порт 161 назначен для сетевого обслуживания.

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

    Если на службу приходит больше датаграмм, чем она может обработать, то дополнительные сообщения просто отбрасываются. Этот факт можно обнаружить с помощью секции UDP Socket Overflows (переполнение в socket протокола UDP) отчета сетевой статистики. Например, приведенный ниже отчет создан командой netstat:

    > netstat -s. . .udp: 0 incomplete headers 0 bad data length fields 0 bad checksums 17 socket overflows

    Протокол User Datagram Protocol определен в RFC 768. RFC от 862 до 865 обсуждают UDP-службы, echo, discard, character generator и quote of the day. RFC 867 описывает утилиту daytime, a RFC 1119 представляет вторую версию службы network time. Протокол BOOTP рассматривается в главе 11, а о дополнительных службах UDP будет упомянуто в следующих главах.

    Похожее:  НижегородЭнергоГазРасчет - Тарифы

    Главная |
    В избранное |
    Наш E-MAIL |
    Прислать материал |
    Нашёл ошибку |
    Наверх

    Глубже в код. блок управления передачей

    Один из ключевых элементов Reliable UDP – блок управления передачей. Задача данного блока – хранение текущих соединений и вспомогательных элементов, распределение пришедших пакетов по соответствующим соединениям, предоставление интерфейса для отправки пакетов соединению и реализация API протокола.

    Реализация асинхронного UDP сервера:

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


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

    connection record

    Глубже в код. создание и установление соединений

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

    Рассмотрим подробно создание

    connection record

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

    Создание исходящего соединения:

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


    После отправки первого пакета отправитель переходит в состояние

    SendingCycle

    – ожидать подтверждения о доставке пакета.

    Сторона-получатель, с помощью метода EndReceive, принимает отправленный пакет, создает новую

    connection record

    и передает данный пакет, с предварительно распарсенным заголовком, в обработку методу ReceivePacket состояния

    FirstPacketReceived

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

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

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

    Кодирование ftp-клиента

    Ниже представлен код FTP-клиента, реализованного на WPF с использованием собственной реализации класса FtpWebRequest в виде класса Client:

    // Реализация класса FtpWebRequest
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Net;
    
    namespace FtpClient
    {
    	public class Client {
    		private string password;
    		private string userName;
    		private string uri;
    		private int bufferSize = 1024;
     
    		public bool Passive = true;
    		public bool Binary = true;
    		public bool EnableSsl = false;
    		public bool Hash = false;
    		
    		public Client(string uri, string userName, string password) {
    			this.uri = uri;
    			this.userName = userName;
    			this.password = password;
    		}
     
    		public string ChangeWorkingDirectory(string path) {
    			uri = combine(uri, path);
     
    			return PrintWorkingDirectory();
    		}
     
    		public string DeleteFile(string fileName) {
    			var request = createRequest(combine(uri, fileName), WebRequestMethods.Ftp.DeleteFile);
    			
    			return getStatusDescription(request);
    		}
     
    		public string DownloadFile(string source, string dest) {
    			var request = createRequest(combine(uri, source), WebRequestMethods.Ftp.DownloadFile);
    			
    			byte[] buffer = new byte[bufferSize];
     
    			using (var response = (FtpWebResponse)request.GetResponse()) {
    				using (var stream = response.GetResponseStream()) {
    					using (var fs = new FileStream(dest, FileMode.OpenOrCreate)) {
    						int readCount = stream.Read(buffer, 0, bufferSize);
     
    						while (readCount > 0) {
    							if (Hash)
    								Console.Write("#");
     
    							fs.Write(buffer, 0, readCount);
    							readCount = stream.Read(buffer, 0, bufferSize);
    						}
    					}
    				}
     
    				return response.StatusDescription;
    			}
    		}
     
    		public DateTime GetDateTimestamp(string fileName) {
    			var request = createRequest(combine(uri, fileName), WebRequestMethods.Ftp.GetDateTimestamp);
    			
    			using (var response = (FtpWebResponse)request.GetResponse()) {
    				return response.LastModified;
    			}
    		}
     
    		public long GetFileSize(string fileName) {
    			var request = createRequest(combine(uri, fileName), WebRequestMethods.Ftp.GetFileSize);
    			
    			using (var response = (FtpWebResponse)request.GetResponse()) {
    				return response.ContentLength;
    			}
    		}
     
    		public string[] ListDirectory() {
    			var list = new List<string>();
     
    			var request = createRequest(WebRequestMethods.Ftp.ListDirectory);
    			
    			using (var response = (FtpWebResponse)request.GetResponse()) {
    				using (var stream = response.GetResponseStream()) {
    					using (var reader = new StreamReader(stream, true)) {
    						while (!reader.EndOfStream) {
    							list.Add(reader.ReadLine());
    						}
    					}
    				}
    			}
     
    			return list.ToArray();
    		}
     
    		public string[] ListDirectoryDetails() {
    			var list = new List<string>();
     
    			var request = createRequest(WebRequestMethods.Ftp.ListDirectoryDetails);
    			
    			using (var response = (FtpWebResponse)request.GetResponse()) {
    				using (var stream = response.GetResponseStream()) {
    					using (var reader = new StreamReader(stream, true)) {
    						while (!reader.EndOfStream) {
    							list.Add(reader.ReadLine());
    						}
    					}
    				}
    			}
     
    			return list.ToArray();
    		}
     
    		public string MakeDirectory(string directoryName) {
    			var request = createRequest(combine(uri, directoryName), WebRequestMethods.Ftp.MakeDirectory);
    			
    			return getStatusDescription(request);
    		}
     
    		public string PrintWorkingDirectory() {
    			var request = createRequest(WebRequestMethods.Ftp.PrintWorkingDirectory);
     
    			return getStatusDescription(request);
    		}
     
    		public string RemoveDirectory(string directoryName) {
    			var request = createRequest(combine(uri, directoryName), WebRequestMethods.Ftp.RemoveDirectory);
    			
    			return getStatusDescription(request);
    		}
     
    		public string Rename(string currentName, string newName) {
    			var request = createRequest(combine(uri, currentName), WebRequestMethods.Ftp.Rename);
    			
    			request.RenameTo = newName;
     
    			return getStatusDescription(request);
    		}
     
    		public string UploadFile(string source, string destination) {
    			var request = createRequest(combine(uri, destination), WebRequestMethods.Ftp.UploadFile);
    			
    			using (var stream = request.GetRequestStream()) {
    				using (var fileStream = System.IO.File.Open(source, FileMode.Open)) {
    					int num;
     
    					byte[] buffer = new byte[bufferSize];
     
    					while ((num = fileStream.Read(buffer, 0, buffer.Length)) > 0) {
    						if (Hash)
    							Console.Write("#");
     
    						stream.Write(buffer, 0, num);
    					}
    				}
    			}
     
    			return getStatusDescription(request);
    		}
     
    		public string UploadFileWithUniqueName(string source) {
    			var request = createRequest(WebRequestMethods.Ftp.UploadFileWithUniqueName);
    			
    			using (var stream = request.GetRequestStream()) {
    				using (var fileStream = System.IO.File.Open(source, FileMode.Open)) {
    					int num;
     
    					byte[] buffer = new byte[bufferSize];
     
    					while ((num = fileStream.Read(buffer, 0, buffer.Length)) > 0) {
    						if (Hash)
    							Console.Write("#");
     
    						stream.Write(buffer, 0, num);
    					}
    				}
    			}
     
    			using (var response = (FtpWebResponse)request.GetResponse()) {
    				return Path.GetFileName(response.ResponseUri.ToString());
    			}
    		}
     
    		private FtpWebRequest createRequest(string method) {
    			return createRequest(uri, method);
    		}
     
    		private FtpWebRequest createRequest(string uri, string method) {
    			var r = (FtpWebRequest)WebRequest.Create(uri);
     
    			r.Credentials = new NetworkCredential(userName, password);
    			r.Method = method;
    			r.UseBinary = Binary;
    			r.EnableSsl = EnableSsl;
    			r.UsePassive = Passive;
     
    			return r;
    		}
     
    		private string getStatusDescription(FtpWebRequest request) {
    			using (var response = (FtpWebResponse)request.GetResponse()) {
    				return response.StatusDescription;
    			}
    		}
     
    		private string combine(string path1, string path2) {
    			return Path.Combine(path1, path2).Replace("\", "/");
    		}
    	}
    }
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    using System.Net;
    using System.IO;
    using System.Text.RegularExpressions;
    
    namespace FtpClient
    {
        public partial class MainWindow : Window
        {
            string prevAdress = "ftp://";
    
            public MainWindow()
            {
                InitializeComponent();
            }
    
            private void btn_connect_Click_1(object sender, RoutedEventArgs e)
            {
                try
                {
                    // Создаем объект подключения по FTP
                    Client client = new Client(txt_adres.Text, txt_login.Text, txt_password.Password);
    
                    // Регулярное выражение, которое ищет информацию о папках и файлах 
                    // в строке ответа от сервера
                    Regex regex = new Regex(@"^([d-])([rwxt-]{3}){3}s d{1,}s .*?(d{1,})s (w s d{1,2}s (?:d{4})?)(d{1,2}:d{2})?s (. ?)s?$",
                        RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
    
                    // Получаем список корневых файлов и папок
                    // Используется LINQ to Objects и регулярные выражения
                    List<FileDirectoryInfo> list = client.ListDirectoryDetails()
                                                         .Select(s =>
                                                         {
                                                             Match match = regex.Match(s);
                                                             if (match.Length > 5)
                                                             {
                                                                 // Устанавливаем тип, чтобы отличить файл от папки (используется также для установки рисунка)
                                                                 string type = match.Groups[1].Value == "d" ? "DIR.png" : "FILE.png";
    
                                                                 // Размер задаем только для файлов, т.к. для папок возвращается
                                                                 // размер ярлыка 4кб, а не самой папки
                                                                 string size = "";
                                                                 if (type == "FILE.png")
                                                                     size = (Int32.Parse(match.Groups[3].Value.Trim()) / 1024).ToString()   " кБ";
    
                                                                 return new FileDirectoryInfo(size, type, match.Groups[6].Value, match.Groups[4].Value, txt_adres.Text);
                                                             }
                                                             else return new FileDirectoryInfo();
                                                         }).ToList();
    
                    // Добавить поле, которое будет возвращать пользователя на директорию выше
                    list.Add(new FileDirectoryInfo("","DEFAULT.png","...","",txt_adres.Text));
                    list.Reverse();
    
                    // Отобразить список в ListView
                    lbx_files.DataContext = list;
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.ToString()   ": n"   ex.Message);
                }
            }
    
            private void folder_Click(object sender, MouseButtonEventArgs e)
            {
                if (e.ClickCount >= 2)
                {
                    FileDirectoryInfo fdi = (FileDirectoryInfo)(sender as StackPanel).DataContext;
                    if (fdi.Type == "DIR.png")
                    {
                        prevAdress = fdi.adress;
                        txt_adres.Text = fdi.adress   fdi.Name   "/";
                        btn_connect_Click_1(null, null);
                    }
                }
    
            }
        }
    }
    // Вспомогательный класс
    public class FileDirectoryInfo
        {
            string fileSize;
            string type;
            string name;
            string date;
            public string adress;
    
            public string FileSize
            {
                get { return fileSize; }
                set { fileSize = value; }
            }
    
            public string Type
            {
                get { return type; }
                set { type = value; }
            }
    
            public string Name
            {
                get { return name; }
                set { name = value; }
            }
    
            public string Date
            {
                get { return date; }
                set { date = value; }
            }
    
            public FileDirectoryInfo() { }
    
            public FileDirectoryInfo(string fileSize, string type, string name, string date, string adress)
            {
                FileSize = fileSize;
                Type = type;
                Name = name;
                Date = date;
                this.adress = adress;
            }
    
        }

    Обратите внимание, что это простая реализация FTP-клиента, которая просто представляет возможность просматривать файлы и папки на сервере, используя графический интерфейс пользователя WPF. При желании можно расширить функционал данного клиента, используя методы класса Client для загрузки/скачивания файлов, изменения директорий и т.д.

    Похожее:  БГУ - Байкальский государственный университет

    Метод 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

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

    В состоянии

    Completed

    метод останавливает рабочий таймер и передает сообщение подписчикам.

    Метод 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

    этот метод переопределен для приема подтверждений о доставке и запросов повторной передачи.


    В состоянии

    Assembling

    в методе ReceivePacket происходит основная работа по сборке сообщения из поступающих пакетов.

    Assembling.ReceivePacket:

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

    В состоянии

    Completed

    единственная задача метода — отослать повторное подтверждение об успешной доставке сообщения.

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

    1. Надежная доставка пакетов, реализованная через механизм положительной обратной связи (так называемый positive acknowledgment )
    2. Необходимость эффективной передачи больших данных, т.е. протокол должен избегать лишних ретрансляций пакетов
    3. Должна быть возможность отмены механизма подтверждения доставки ( возможность функционировать как «чистый» UDP протокол)
    4. Возможность реализации командного режима, с подтверждением каждого сообщения
    5. Базовой единицей передачи данных по протоколу должно быть сообщение


    Эти требования во многом совпадают с требованиями к Reliable Data Protocol, описанными в

    , и я основывался на этих стандартах при разработке данного протокола.

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


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

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

    Обнаружение ошибок в TCP протоколе достигается благодаря установке соединения с конечным узлом, сохранению состояния этого соединения, указанию номера отправленных байт в каждом заголовке пакета, и уведомлениях о получениях с помощью номера подтверждения «acknowledge number».

    Дополнительно, для повышения производительности (т.е. отправки более одного сегмента без получения подтверждения) TCP протокол использует так называемое окно передачи — число байт данных которые отправитель сегмента ожидает принять.

    Более подробно с TCP протоколом можно ознакомиться в rfc 793, с UDP в rfc 768, где они, собственно говоря, и определены.

    Из вышеописанного, понятно, что для создания надежного протокола доставки сообщений поверх UDP (в дальнейшем будем называть Reliable UDP), требуется реализовать схожие с TCP механизмы передачи данных. А именно:

    Дополнительно, требуется:

    1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 4,00 из 5)
    Загрузка...

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *

    Adblock
    detector