2 мая 2017 г.

Новый велосипед с UDP



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

Block

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




Packet Types


Используется, чтобы сообщить нашему приложению, что каждая датаграмма должна означать:
·         ACK - подтверждение. В основном для контрольных сообщений, отправленных обеими сторонами.
·         BYE - сообщение о конце передачи. Может быть отправлен отправителем или получателем в любое время.
·         REQF - запрос файла. Отправляется получателем отправителю. Необходимо отправить ACK  отправителем.
·         INFO - информация о передаваемом файле. Отправляется отправителем получателю после REQF. Требуется подтверждение ACK получателем.
·         REQB - запрос блока данных. Отправляется получателем отправителю после INFO.
·         SEND - ответ на запрос Block, содержащий данные блока. Отправляется отправителем получателю соответствующего REQB.

 Вот полезная диаграмма, которая объясняет, как работают обмены:






Packet Data (Payloads)


Вот некоторые данные, которые должны содержаться при обмене:
·         REQF - Payload должен содержать строку в кодировке UTF 8, которая является файлом.
·         Отправитель должен всегда посылать ACK. Если файл существует, то он должен содержать тот же Payload, который был в REQF. Если же нет, то должен отправить пустой Payload.
·         INFO - первые 16 байт Payload должны быть контрольной суммой MD5 исходного файла (несжатого). Следующие 4 байта указывают размер файл (в байтах) представленый в UInt32 . 4 байта после этого - максимальный размер блока (в байтах). Последние 4 байта - это общее количество блоков, которые будут переданы.
·         REQB - Payload число в UInt32 который представляет собой номер Block.Number, запрашиваемый Получателем.
·         SEND - Payload это блок, чей номер является номером одного запрошенного в предыдущем REQB.
·         BYE - Payload не требуется.

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



Common Files

 

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

Block


  1. using System;
  2. using System.Text;
  3. using System.Linq;
  4. namespace UdpFileTransfer
  5. {
  6.     // Всё это отправляется по сети
  7.     public class Block
  8.     {
  9.         public UInt32 Number { get; set; }
  10.         public byte[] Data { get; set; } = new byte[ 0];
  11.         #region Constructors
  12.         // Создать Блок с указаным номером (ID)
  13.         public Block(UInt32 number= 0)
  14.         {
  15.             Number = number;
  16.         }
  17.         // Создать Блок из массива байт
  18.         public Block (byte[] bytes)
  19.         {
  20.             // Первые 4 байта - это номер блока
  21.             Number = BitConverter.ToUInt32(bytes,  0);
  22.             // Данные идут с 4-го байта
  23.             Data = bytes.Skip(4).ToArray();
  24.         }
  25.         #endregion
  26.         public override string ToString()
  27.         {
  28.             // Используя несколько первых битов данных преобразовать это в строку
  29.             String dataStr;
  30.             if (Data.Length > 8)
  31.                 dataStr = Encoding.ASCII.GetString(Data,  0, 8) + "...";
  32.             else
  33.                 dataStr = Encoding.ASCII.GetString(Data,  0, Data.Length);
  34.             return string.Format(
  35.                 "[Block:\n" +
  36.                 "  Number={0},\n" +
  37.                 "  Size={1},\n" +
  38.                 "  Data=`{2}`]",
  39.                 Number, Data.Length, dataStr);
  40.         }
  41.         // Вернуть данные блока в виде массива байт
  42.         public byte[] GetBytes()
  43.         {
  44.             // Конвертировать номер в байты
  45.             byte[] numberBytes = BitConverter.GetBytes(Number);
  46.             // Объединение остальных данных в общий массив
  47.             byte[] bytes = new byte[numberBytes.Length + Data.Length];
  48.             numberBytes.CopyTo(bytes,  0);
  49.             Data.CopyTo(bytes, 4);
  50.             return bytes;
  51.         }
  52.     }
  53. }

Это контейнер данных, который используются для разделения файла на небольшие куски. Здесь присутствует 2 конструктор. Один для создания новых данных с заданным номер, и для восстановления из массива байт. Метод GetBytes  используется для получения блока как массива байт.

Packet 



  1. using System;
  2. using System.Text;
  3. using System.Linq;
  4. namespace UdpFileTransfer
  5. {
  6.     public class Packet
  7.     {
  8.         #region Message Types
  9.         public static UInt32 Ack = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("ACK "),  0);
  10.         public static UInt32 Bye = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("BYE "),  0);
  11.         public static UInt32 RequestFile = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("REQF"),  0);
  12.         public static UInt32 RequestBlock = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("REQB"),  0);
  13.         public static UInt32 Info = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("INFO"),  0);
  14.         public static UInt32 Send = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("SEND"),  0);
  15.         #endregion
  16.         // Поля для пакета
  17.         public UInt32 PacketType { get; set; }
  18.         public byte[] Payload { get; set; } = new byte[  0];
  19.         #region Message Properties
  20.         public bool IsAck { get { return PacketType == Ack; } }
  21.         public bool IsBye { get { return PacketType == Bye; } }
  22.         public bool IsRequestFile { get { return PacketType == RequestFile; } }
  23.         public bool IsRequestBlock { get { return PacketType == RequestBlock; } }
  24.         public bool IsInfo { get { return PacketType == Info; } }
  25.         public bool IsSend { get { return PacketType == Send; } }
  26.         public bool IsUnknown { get { return !(IsAck || IsBye || IsRequestFile || IsRequestBlock || IsInfo || IsSend); } }
  27.         public string MessageTypeString { get { return Encoding.UTF8.GetString(BitConverter.GetBytes(PacketType)); } }
  28.         #endregion
  29.         #region Constructors
  30.         public Packet(UInt32 packetType)
  31.         {
  32.             // Установить тип пакета
  33.             PacketType = packetType;
  34.         }
  35.         // Создать пакет из массива байт
  36.         public Packet(byte[] bytes)
  37.         {
  38.             // Используя первые 4 байта которые определяют тип
  39.             PacketType = BitConverter.ToUInt32(bytes,  0);
  40.             // Payload начинается с 4-го байта
  41.             Payload = new byte[bytes.Length - 4];
  42.             bytes.Skip(4).ToArray().CopyTo(Payload,  0);
  43.         }
  44.         #endregion
  45.         public override string ToString()
  46.         {
  47.             // Используя несколько первых битов данных преобразовать это в строку
  48.             String payloadStr;
  49.             int payloadSize = Payload.Length;
  50.             if (payloadSize > 8)
  51.                 payloadStr = Encoding.ASCII.GetString(Payload,  0, 8) + "...";
  52.             else
  53.                 payloadStr = Encoding.ASCII.GetString(Payload,  0, payloadSize);
  54.             // тип в строке
  55.             String typeStr = "UKNOWN";
  56.             if (!IsUnknown)
  57.                 typeStr = MessageTypeString;
  58.             return string.Format(
  59.                 "[Packet:\n" +
  60.                 "  Type={0},\n" +
  61.                 "  PayloadSize={1},\n" +
  62.                 "  Payload=`{2}`]",
  63.                 typeStr, payloadSize, payloadStr);
  64.         }
  65.         // Вернуть пакет в виде массива байт
  66.         public byte[] GetBytes()
  67.         {
  68.             // объединение массиа байт
  69.             byte[] bytes = new byte[4 + Payload.Length];
  70.             BitConverter.GetBytes(PacketType).CopyTo(bytes,  0);
  71.             Payload.CopyTo(bytes, 4);
  72.             return bytes;
  73.         }
  74.     }
  75.     #region Definite Packets
  76.     // ACK
  77.     public class AckPacket : Packet
  78.     {
  79.         public string Message
  80.         {
  81.             get { return Encoding.UTF8.GetString(Payload); }
  82.             set { Payload = Encoding.UTF8.GetBytes(value); }
  83.         }
  84.         public AckPacket(Packet p=null) :
  85.             base(Ack)
  86.         {
  87.             if (p != null)
  88.                 Payload = p.Payload;
  89.         }
  90.     }
  91.     // REQF
  92.     public class RequestFilePacket : Packet
  93.     {
  94.         public string Filename
  95.         {
  96.             get { return Encoding.UTF8.GetString(Payload); }
  97.             set { Payload = Encoding.UTF8.GetBytes(value); }
  98.         }
  99.         public RequestFilePacket(Packet p=null) :
  100.             base(RequestFile)
  101.         {
  102.             if (p != null)
  103.                 Payload = p.Payload;
  104.         }
  105.     }
  106.     // REQB
  107.     public class RequestBlockPacket : Packet
  108.     {
  109.         public UInt32 Number
  110.         {
  111.             get { return BitConverter.ToUInt32(Payload,  0); }
  112.             set { Payload = BitConverter.GetBytes(value); }
  113.         }
  114.         public RequestBlockPacket(Packet p = null)
  115.             : base(RequestBlock)
  116.         {
  117.             if (p != null)
  118.                 Payload = p.Payload;
  119.         }
  120.     }
  121.     // INFO
  122.     public class InfoPacket : Packet
  123.     {
  124.         // Должна быть контрольная сумма MD5
  125.         public byte[] Checksum
  126.         {
  127.             get { return Payload.Take(16).ToArray(); }
  128.             set { value.CopyTo(Payload,  0); }
  129.         }
  130.         public UInt32 FileSize
  131.         {
  132.             get { return BitConverter.ToUInt32(Payload.Skip(16).Take(4).ToArray(),  0); }
  133.             set { BitConverter.GetBytes(value).CopyTo(Payload, 16); }
  134.         }
  135.         public UInt32 MaxBlockSize
  136.         {
  137.             get { return BitConverter.ToUInt32(Payload.Skip(16 + 4).Take(4).ToArray(),  0); }
  138.             set { BitConverter.GetBytes(value).CopyTo(Payload, 16 + 4); }
  139.         }
  140.         public UInt32 BlockCount
  141.         {
  142.             get { return BitConverter.ToUInt32(Payload.Skip(16 + 4 + 4).Take(4).ToArray(),  0); }
  143.             set { BitConverter.GetBytes(value).CopyTo(Payload, 16 + 4 + 4); }
  144.         }
  145.         public InfoPacket(Packet p = null)
  146.             : base(Info)
  147.         {
  148.             if (p != null)
  149.                 Payload = p.Payload;
  150.             else
  151.                 Payload = new byte[16 + 4 + 4 + 4];
  152.         }
  153.     }
  154.     // SEND
  155.     public class SendPacket : Packet
  156.     {
  157.         public Block Block {
  158.             get { return new Block(Payload); }
  159.             set { Payload = value.GetBytes(); }
  160.         }
  161.         public SendPacket(Packet p=null)
  162.             : base(Send)
  163.         {
  164.             if (p != null)
  165.                 Payload = p.Payload;
  166.         }
  167.     }
  168.     #endregion
  169. }


Packet - это то, чем мы орудуем в сетях. Он имеет только два поля данных, PacketType и Payload. Все пакеты должны иметь один из перечисленых типов в области "Message Types". Также существует второй конструктор для создания пакета из массива байт и метод GetBytes, чтобы получить пакет в виде массива.

Дальше опеределены «специальные пакеты», которые наследуются от Packet и позволяют упростить работу с данными Payload с помощью некоторых типов сообщений. Однако BYE не определен, поскольку не нуждается Payload.

NetworkMessage

 


  1. using System.Net;
  2. namespace UdpFileTransfer
  3. {
  4.     // Структура данных, используется в очереди пакетов 
  5.     public class NetworkMessage
  6.     {
  7.         public IPEndPoint Sender { get; set; }
  8.         public Packet Packet { get; set; }
  9.     }
  10. }

NetworkMessage - это структура данных, которая используется для соединения пакета с отправителем. Используются только при чтении полученных данных.

Отправитель


Отправитель работает как сервер.  Он будет ждать запрос на файл и отвечать на него.


  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.IO.Compression;
  5. using System.Text;
  6. using System.Linq;
  7. using System.Net;
  8. using System.Net.Sockets;
  9. using System.Threading;
  10. using System.Collections.Generic;
  11. using System.Security.Cryptography;
  12. namespace UdpFileTransfer
  13. {
  14.     public class UdpFileSender
  15.     {
  16.         #region Statics
  17.         public static readonly UInt32 MaxBlockSize = 8 * 1024; // 8KB
  18.         #endregion
  19.         enum SenderState
  20.         {
  21.             NotRunning,
  22.             WaitingForFileRequest,
  23.             PreparingFileForTransfer,
  24.             SendingFileInfo,
  25.             WaitingForInfoACK,
  26.             Transfering
  27.         }
  28.         // Данные подключения
  29.         private UdpClient _client;
  30.         public readonly int Port;
  31.         public bool Running { get; private set; } = false;
  32.         // Данные передачи
  33.         public readonly string FilesDirectory;
  34.         private HashSet<string> _transferableFiles;
  35.         private Dictionary<UInt32, Block> _blocks = new Dictionary<UInt32, Block>();
  36.         private Queue<NetworkMessage> _packetQueue = new Queue<NetworkMessage>();
  37.         // MD5 сумма
  38.         private MD5 _hasher;
  39.         // Создание UdpClient на <порту>
  40.         public UdpFileSender(string filesDirectory, int port)
  41.         {
  42.             FilesDirectory = filesDirectory;
  43.             Port = port;
  44.             _client = new UdpClient(Port, AddressFamily.InterNetwork); // Bind in IPv4
  45.             _hasher = MD5.Create();
  46.         }
  47.         // Подготовка отправителя для отправки файла
  48.         public void Init()
  49.         {
  50.             // Сканирование файлов
  51.             List<string> files = new List<string>(Directory.EnumerateFiles(FilesDirectory));
  52.             _transferableFiles = new HashSet<string>(files.Select(s => s.Substring(FilesDirectory.Length + 1)));
  53.             // Убедить что есть хотя бы один файл для отправки
  54.             if (_transferableFiles.Count !=  0)
  55.             {
  56.                 // Изменить флаг состояния
  57.                 Running = true;
  58.                 // Вывести INFO
  59.                 Console.WriteLine("I'll transfer these files:");
  60.                 foreach (string s in _transferableFiles)
  61.                     Console.WriteLine("  {0}", s);
  62.             }
  63.             else
  64.                 Console.WriteLine("I don't have any files to transfer.");
  65.         }
  66.         // Сигнал о завершении
  67.         public void Shutdown()
  68.         {
  69.             Running = false;
  70.         }
  71.         // Главный цикл для отправки
  72.         public void Run()
  73.         {
  74.             // Изменить состояние 
  75.             SenderState state = SenderState.WaitingForFileRequest;
  76.             string requestedFile = "";
  77.             IPEndPoint receiver = null;
  78.             // Это функция для сброса состояние передачи
  79.             Action ResetTransferState = new Action(() =>
  80.                 {
  81.                     state = SenderState.WaitingForFileRequest;
  82.                     requestedFile = "";
  83.                     receiver = null;
  84.                     _blocks.Clear();
  85.                 });
  86.             while (Running)
  87.             {
  88.                 // Проверка наличия новых сообщений
  89.                 _checkForNetworkMessages();
  90.                 NetworkMessage nm = (_packetQueue.Count >  0) ? _packetQueue.Dequeue() : null;
  91.                 // Проверка на BYE
  92.                 bool isBye = (nm == null) ? false : nm.Packet.IsBye;
  93.                 if (isBye)
  94.                 {
  95.                     // Возврат к исходному состоянию
  96.                     ResetTransferState();
  97.                     Console.WriteLine("Received a BYE message, waiting for next client.");
  98.                 }
  99.                 // Выполнить действия в зависимости от текущего состояния
  100.                 switch (state)
  101.                 {
  102.                     case SenderState.WaitingForFileRequest:
  103.                         // проверка на запрос файла
  104.                         // Если полученный пакет это файл запроса, отправить ACK и изменить состояние
  105.                         bool isRequestFile = (nm == null) ? false : nm.Packet.IsRequestFile;
  106.                         if (isRequestFile)
  107.                         {
  108.                             // подготовка ACK
  109.                             RequestFilePacket REQF = new RequestFilePacket(nm.Packet);
  110.                             AckPacket ACK = new AckPacket();
  111.                             requestedFile = REQF.Filename;
  112.                             // печать INFO
  113.                             Console.WriteLine("{0} has requested file file \"{1}\".", nm.Sender, requestedFile);
  114.                             // проверка что файл есть
  115.                             if (_transferableFiles.Contains(requestedFile))
  116.                             {
  117.                                 // Пометить что файл есть, назначить текущего получателя
  118.                                 receiver = nm.Sender;
  119.                                 ACK.Message = requestedFile;
  120.                                 state = SenderState.PreparingFileForTransfer;
  121.                                 Console.WriteLine("  We have it.");
  122.                             }
  123.                             else
  124.                                 ResetTransferState();
  125.                             // Отправить сообщение
  126.                             byte[] buffer = ACK.GetBytes();
  127.                             _client.Send(buffer, buffer.Length, nm.Sender);
  128.                         }
  129.                         break;
  130.                     case SenderState.PreparingFileForTransfer:
  131.                         // Используя файл подготовить его в память
  132.                         byte[] checksum;
  133.                         UInt32 fileSize;
  134.                         if (_prepareFile(requestedFile, out checksum, out fileSize))
  135.                         {
  136.                             // если всё хорошо отправить пакет с INFO
  137.                             InfoPacket INFO = new InfoPacket();
  138.                             INFO.Checksum = checksum;
  139.                             INFO.FileSize = fileSize;
  140.                             INFO.MaxBlockSize = MaxBlockSize;
  141.                             INFO.BlockCount = Convert.ToUInt32(_blocks.Count);
  142.                             // Отправляем пакет
  143.                             byte[] buffer = INFO.GetBytes();
  144.                             _client.Send(buffer, buffer.Length, receiver);
  145.                             // сменить состояние
  146.                             Console.WriteLine("Sending INFO, waiting for ACK...");
  147.                             state = SenderState.WaitingForInfoACK;
  148.                         }
  149.                         else
  150.                             ResetTransferState(); // File not good, reset the state
  151.                         break;
  152.                     case SenderState.WaitingForInfoACK:
  153.                         // Если получили ACK и Payload - это имя файла
  154.                         bool isAck = (nm == null) ? false : (nm.Packet.IsAck);
  155.                         if (isAck)
  156.                         {
  157.                             AckPacket ACK = new AckPacket(nm.Packet);
  158.                             if (ACK.Message == "INFO")
  159.                             {
  160.                                 Console.WriteLine("Starting Transfer...");
  161.                                 state = SenderState.Transfering;
  162.                             }
  163.                         }
  164.                         break;
  165.                     case SenderState.Transfering:
  166.                         // Если запрос блока то посылаем его
  167.                         bool isRequestBlock = (nm == null) ? false : nm.Packet.IsRequestBlock;
  168.                         if (isRequestBlock)
  169.                         {
  170.                             // Извлечение данных
  171.                             RequestBlockPacket REQB = new RequestBlockPacket(nm.Packet);
  172.                             Console.WriteLine("Got request for Block #{0}", REQB.Number);
  173.                             // Создать ответный пакет
  174.                             Block block = _blocks[REQB.Number];
  175.                             SendPacket SEND = new SendPacket();
  176.                             SEND.Block = block;
  177.                             // посылаем
  178.                             byte[] buffer = SEND.GetBytes();
  179.                             _client.Send(buffer, buffer.Length, nm.Sender);
  180.                             Console.WriteLine("Sent Block #{0} [{1} bytes]", block.Number, block.Data.Length);
  181.                         }
  182.                         break;
  183.                 }
  184.                 Thread.Sleep(1); //иногда полезно на мгновенье отвлечься от забот
  185.             }
  186.             // Если получатель был назначет, то уведомляем его о завершении (BYE)
  187.             if (receiver != null)
  188.             {
  189.                 Packet BYE = new Packet(Packet.Bye);
  190.                 byte[] buffer = BYE.GetBytes();
  191.                 _client.Send(buffer, buffer.Length, receiver);
  192.             }
  193.             state = SenderState.NotRunning;
  194.         }
  195.         // Закрытие основного клиента UDP
  196.         public void Close()
  197.         {
  198.             _client.Close();
  199.         }
  200.         // Попытка заполнить очередь пакетов
  201.         private void _checkForNetworkMessages()
  202.         {
  203.             if (!Running)
  204.                 return;
  205.             // Проверка какие-то входяшие есть, как минимум 4 байта для типа
  206.             int bytesAvailable = _client.Available;
  207.             if (bytesAvailable >= 4)
  208.             {
  209.                 // Это будет читать ОДНУ датаграмму, даже если несколько были получены
  210.                 IPEndPoint ep = new IPEndPoint(IPAddress.Any,  0);
  211.                 byte[] buffer = _client.Receive(ref ep);
  212.                 // создать сообщение и разместить в очереди для обработки
  213.                 NetworkMessage nm = new NetworkMessage();
  214.                 nm.Sender = ep;
  215.                 nm.Packet = new Packet(buffer);
  216.                 _packetQueue.Enqueue(nm);
  217.             }
  218.         }
  219.         // Загружает файл в блоки, возвращает true если файл готов
  220.         private bool _prepareFile(string requestedFile, out byte[] checksum, out UInt32 fileSize)
  221.         {
  222.             Console.WriteLine("Preparing the file to send...");
  223.             bool good = false;
  224.             fileSize =  0;
  225.             try
  226.             {
  227.                 // Прочитать и вычислить контрольную сумму исходного файла
  228.                 byte[] fileBytes = File.ReadAllBytes(Path.Combine(FilesDirectory, requestedFile));
  229.                 checksum = _hasher.ComputeHash(fileBytes);
  230.                 fileSize = Convert.ToUInt32(fileBytes.Length);
  231.                 Console.WriteLine("{0} is {1} bytes large.", requestedFile, fileSize);
  232.                 // Сжать файл
  233.                 Stopwatch timer = new Stopwatch();
  234.                 using (MemoryStream compressedStream = new MemoryStream())
  235.                 {
  236.                     // Выполните фактическое сжатие
  237.                     DeflateStream deflateStream = new DeflateStream(compressedStream, CompressionMode.Compress, true);
  238.                     timer.Start();
  239.                     deflateStream.Write(fileBytes,  0, fileBytes.Length);
  240.                     deflateStream.Close();
  241.                     timer.Stop();
  242.                     // Поместить в блоки
  243.                     compressedStream.Position =  0;
  244.                     long compressedSize = compressedStream.Length;
  245.                     UInt32 id = 1;
  246.                     while (compressedStream.Position < compressedSize)
  247.                     {
  248.                         // Взять кусок
  249.                         long numBytesLeft = compressedSize - compressedStream.Position;
  250.                         long allocationSize = (numBytesLeft > MaxBlockSize) ? MaxBlockSize : numBytesLeft;
  251.                         byte[] data = new byte[allocationSize];
  252.                         compressedStream.Read(data,  0, data.Length);
  253.                         // Создать новый блок
  254.                         Block b = new Block(id++);
  255.                         b.Data = data;
  256.                         _blocks.Add(b.Number, b);
  257.                     }
  258.                     // Вывести инфу и установить результат на True
  259.                     Console.WriteLine("{0} compressed is {1} bytes large in {2:0.000}s.", requestedFile, compressedSize, timer.Elapsed.TotalSeconds);
  260.                     Console.WriteLine("Sending the file in {0} blocks, using a max block size of {1} bytes.", _blocks.Count, MaxBlockSize);
  261.                     good = true;
  262.                 }
  263.             }
  264.             catch (Exception e)
  265.             {
  266.                 // Ошибка
  267.                 Console.WriteLine("Could not prepare the file for transfer, reason:");
  268.                 Console.WriteLine(e.Message);
  269.                 // перезапуск
  270.                 _blocks.Clear();
  271.                 checksum = null;
  272.             }
  273.             return good;
  274.         }
  275.         #region Program Execution
  276.         public static UdpFileSender fileSender;
  277.         public static void InterruptHandler(object sender, ConsoleCancelEventArgs args)
  278.         {
  279.             args.Cancel = true;
  280.             fileSender?.Shutdown();
  281.         }
  282.         public static void Main(string[] args)
  283.         {
  284.             // Настройка отправителя
  285.             string filesDirectory = "Files";//args[0].Trim();
  286.             int port = 6000;//int.Parse(args[1].Trim());
  287.             fileSender = new UdpFileSender(filesDirectory, port);
  288.             // Добавляем выход на Ctrl+C
  289.             Console.CancelKeyPress += InterruptHandler;
  290.             // запускаем всё
  291.             fileSender.Init();
  292.             fileSender.Run();
  293.             fileSender.Close();
  294.         }
  295.         #endregion
  296.     }
  297. }

Так, здесь довольно много кода, лучше его разобрать.
В конструкторе для UdpFileSender, мы говорим ему, какой каталог мы хотим использовать для файлов, которые могут быть переданы, и мы создаем UdpClient. Прослушивать входящие датаграммы на конкретном порту.

Init() сканирует файл FilesDirectory (нерекурсивно) для любых файлов и помечает их как переносимые. Он также устанавливает Running на true, чтобы мы могли начать прослушивание получателей. Shutdown() используется для простого отключения «сервера». 

Run() - основной метод UdpFileSender. Сначала описаны переменные для состояния передачи, и функция для сброса состояния в по необходимости. В начале цикла смотрим, есть ли у нас какие-либо новые пакеты, которые были получены. Если было сообщение BYE, это означает, что клиент преждевременно отключился, и необходимо сбросить состояние передачи, чтобы ждать нового клиента. Оператор switch используется для запуска процесса передачи. Каждое состояние будет ждать определенного типа пакета, затем выполнить обработку (например, создать ответ или подготовить файл для передачи), затем перейти к следующему в очереди. И в конце функции мы посылаем клиенту сообщение BYE, если у нас есть один подключенный получатель.

Метод Close() очистит UdpClient.

 _checkForNetworkMessages() смотрит, есть ли у нас новые датаграммы, которые были получены. Поскольку все наши пакеты имеют длину не менее 4 байт, мы хотим убедиться, что многие байты готовы. Мы создаем фиктивный объект IPEndPoint. Используя UdpClient.Receive, он будет брать ровно одну датаграмму, которая была получена по сети. В отличие от TcpClient, нам не нужно возиться с NetworkStreams. После этого мы отправляем его отправителя и пакет в очередь NetworkMessage для последующей обработки.

_prepareFile() будет очевидно готовить файл к передаче. Он принимает в путь к файлу, вычисляет контрольную сумму исходных байтов, сжимает ее и разбивает на блоки. Если все пройдет хорошо, оно вернет true, и его параметры out будут заполнены. 

И всё. Это в принципе всё что делает отправитель.

Получатель


Он запрашивает файл у отправителя и загружает его на компьютер


  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.IO.Compression;
  5. using System.Text;
  6. using System.Linq;
  7. using System.Net;
  8. using System.Net.Sockets;
  9. using System.Threading;
  10. using System.Collections.Generic;
  11. using System.Security.Cryptography;
  12. namespace UdpFileTransfer
  13. {
  14.     class UdpFileReceiver
  15.     {
  16.         #region Statics
  17.         public static readonly int MD5ChecksumByteSize = 16;
  18.         #endregion /
  19.         enum ReceiverState {
  20.             NotRunning,
  21.             RequestingFile,
  22.             WaitingForRequestFileACK,
  23.             WaitingForInfo,
  24.             PreparingForTransfer,
  25.             Transfering,
  26.             TransferSuccessful,
  27.         }
  28.         // Данные подключения
  29.         private UdpClient _client;
  30.         public readonly int Port;
  31.         public readonly string Hostname;
  32.         private bool _shutdownRequested = false;
  33.         private bool _running = false;
  34.         // Данные приемника
  35.         private Dictionary<UInt32, Block> _blocksReceived = new Dictionary<UInt32, Block>();
  36.         private Queue<UInt32> _blockRequestQueue = new Queue<UInt32>();
  37.         private Queue<NetworkMessage> _packetQueue = new Queue<NetworkMessage>();
  38.         // MD5 сумма
  39.         private MD5 _hasher;
  40.         // Конструктор устанавливаем соединение с <хост> по <порт>
  41.         public UdpFileReceiver(string hostname, int port)
  42.         {
  43.             Port = port;
  44.             Hostname = hostname;
  45.             // Устанавливает клиент по умолчанию для отправки/получения пакетов 
  46.             _client = new UdpClient(Hostname, Port);
  47.             _hasher = MD5.Create();
  48.         }
  49.         // Выполнить корректное завершение работы
  50.         public void Shutdown()
  51.         {
  52.             _shutdownRequested = true;
  53.         }
  54.         // Взять файл и загрузить на компьютер
  55.         public void GetFile(string filename)
  56.         {
  57.             // Инициализация состояние получения файла
  58.             Console.WriteLine("Requesting file: {0}", filename);
  59.             ReceiverState state = ReceiverState.RequestingFile;
  60.             byte[] checksum = null;
  61.             UInt32 fileSize =  0;
  62.             UInt32 numBlocks =  0;
  63.             UInt32 totalRequestedBlocks =  0;
  64.             Stopwatch transferTimer = new Stopwatch();
  65.             // Функция для сброса состояния передачи
  66.             Action ResetTransferState = new Action(() =>
  67.                 {
  68.                     state = ReceiverState.RequestingFile;
  69.                     checksum = null;
  70.                     fileSize =  0;
  71.                     numBlocks =  0;
  72.                     totalRequestedBlocks =  0;
  73.                     _blockRequestQueue.Clear();
  74.                     _blocksReceived.Clear();
  75.                     transferTimer.Reset();
  76.                 });
  77.             // главный цикл
  78.             _running = true;
  79.             bool senderQuit = false;
  80.             bool wasRunning = _running;
  81.             while (_running)
  82.             {
  83.                 // Проверка наличие новых пакетов
  84.                 _checkForNetworkMessages();
  85.                 NetworkMessage nm = (_packetQueue.Count >  0) ? _packetQueue.Dequeue() : null;
  86.                 // В случае, если отправитель завершает работу, мы тоже завершите работу
  87.                 bool isBye = (nm == null) ? false : nm.Packet.IsBye;
  88.                 if (isBye)
  89.                     senderQuit = true;
  90.                 // Состояние
  91.                 switch (state)
  92.                 {
  93.                     case ReceiverState.RequestingFile:
  94.                         // Создать REQF
  95.                         RequestFilePacket REQF = new RequestFilePacket();
  96.                         REQF.Filename = filename;
  97.                         // Послать это
  98.                         byte[] buffer = REQF.GetBytes();
  99.                         _client.Send(buffer, buffer.Length);
  100.                         // Сменить состояние и ждать ACK
  101.                         state = ReceiverState.WaitingForRequestFileACK;
  102.                         break;
  103.                     case ReceiverState.WaitingForRequestFileACK:
  104.                         // Если это ACK, а Payload - это имя файла
  105.                         bool isAck = (nm == null) ? false : (nm.Packet.IsAck);
  106.                         if (isAck)
  107.                         {
  108.                             AckPacket ACK = new AckPacket(nm.Packet);
  109.                             // Убедиться что ответ с именем файла
  110.                             if (ACK.Message == filename)
  111.                             {
  112.                                 // всё в порядке сменяем состояние
  113.                                 state = ReceiverState.WaitingForInfo;
  114.                                 Console.WriteLine("They have the file, waiting for INFO...");
  115.                             }
  116.                             else
  117.                                 ResetTransferState(); // Не то что мы хотели, перезапуск
  118.                         }
  119.                         break;
  120.                     case ReceiverState.WaitingForInfo:
  121.                         // Проверяем информацию о файле
  122.                         bool isInfo = (nm == null) ? false : (nm.Packet.IsInfo);
  123.                         if (isInfo)
  124.                         {
  125.                             // Взять данные
  126.                             InfoPacket INFO = new InfoPacket(nm.Packet);
  127.                             fileSize = INFO.FileSize;
  128.                             checksum = INFO.Checksum;
  129.                             numBlocks = INFO.BlockCount;
  130.                             // вывод о INFO
  131.                             Console.WriteLine("Received an INFO packet:");
  132.                             Console.WriteLine("  Max block size: {0}", INFO.MaxBlockSize);
  133.                             Console.WriteLine("  Num blocks: {0}", INFO.BlockCount);
  134.                             // Отправить ACK для INFO
  135.                             AckPacket ACK = new AckPacket();
  136.                             ACK.Message = "INFO";
  137.                             buffer = ACK.GetBytes();
  138.                             _client.Send(buffer, buffer.Length);
  139.                             // сменить позицию на подготовку передачи
  140.                             state = ReceiverState.PreparingForTransfer;
  141.                         }
  142.                         break;
  143.                     case ReceiverState.PreparingForTransfer:
  144.                         // Подготовить очередь запросов
  145.                         for (UInt32 id = 1; id <= numBlocks; id++)
  146.                             _blockRequestQueue.Enqueue(id);
  147.                         totalRequestedBlocks += numBlocks;
  148.                         // сменить позицию
  149.                         Console.WriteLine("Starting Transfer...");
  150.                         transferTimer.Start();
  151.                         state = ReceiverState.Transfering;
  152.                         break;
  153.                     case ReceiverState.Transfering:
  154.                         // Отправить запрос блока
  155.                         if (_blockRequestQueue.Count >  0)
  156.                         {
  157.                             // Установить запрос для блока
  158.                             UInt32 id = _blockRequestQueue.Dequeue();
  159.                             RequestBlockPacket REQB = new RequestBlockPacket();
  160.                             REQB.Number = id;
  161.                             // Послать пакет
  162.                             buffer = REQB.GetBytes();
  163.                             _client.Send(buffer, buffer.Length);
  164.                             // Некоторая информация о блоке
  165.                             Console.WriteLine("Sent request for Block #{0}", id);
  166.                         }
  167.                         // Проверка есть ли блоки в очереди
  168.                         bool isSend = (nm == null) ? false : (nm.Packet.IsSend);
  169.                         if (isSend)
  170.                         {
  171.                             // Получить данные и сохранить их
  172.                             SendPacket SEND = new SendPacket(nm.Packet);
  173.                             Block block = SEND.Block;
  174.                             _blocksReceived.Add(block.Number, block);
  175.                             // вывести инфу
  176.                             Console.WriteLine("Received Block #{0} [{1} bytes]", block.Number, block.Data.Length);
  177.                         }
  178.                         // Передать любые запросы, которые мы не получили
  179.                         if ((_blockRequestQueue.Count ==  0) && (_blocksReceived.Count != numBlocks))
  180.                         {
  181.                             for (UInt32 id = 1; id <= numBlocks; id++)
  182.                             {
  183.                                 if (!_blocksReceived.ContainsKey(id) && !_blockRequestQueue.Contains(id))
  184.                                 {
  185.                                     _blockRequestQueue.Enqueue(id);
  186.                                     totalRequestedBlocks++;
  187.                                 }
  188.                             }
  189.                         }
  190.                         // Проверить получили ли мы все блоки. Перейдите в успешное состояние переноса.
  191.                         if (_blocksReceived.Count == numBlocks)
  192.                             state = ReceiverState.TransferSuccessful;
  193.                         break;
  194.                     case ReceiverState.TransferSuccessful:
  195.                         transferTimer.Stop();
  196.                         // Все хорошо, отправить сообщение BYE
  197.                         Packet BYE = new Packet(Packet.Bye);
  198.                         buffer = BYE.GetBytes();
  199.                         _client.Send(buffer, buffer.Length);
  200.                         Console.WriteLine("Transfer successful; it took {0:0.000}s with a success ratio of {1:0.000}.",
  201.                             transferTimer.Elapsed.TotalSeconds, (double)numBlocks / (double)totalRequestedBlocks);
  202.                         Console.WriteLine("Decompressing the Blocks...");
  203.                         // Восстановите данные
  204.                         if (_saveBlocksToFile(filename, checksum, fileSize))
  205.                             Console.WriteLine("Saved file as {0}.", filename);
  206.                         else
  207.                             Console.WriteLine("There was some trouble in saving the Blocks to {0}.", filename);
  208.                         // конец передачи
  209.                         _running = false;
  210.                         break;
  211.                 }
  212.                 // поспать
  213.                 Thread.Sleep(1);
  214.                 // Проверка на выключение
  215.                 _running &= !_shutdownRequested;
  216.                 _running &= !senderQuit;
  217.             }
  218.             // Отправить сообщение BYE, если пользователь хочет отменить передачу
  219.             if (_shutdownRequested && wasRunning)
  220.             {
  221.                 Console.WriteLine("User canceled transfer.");
  222.                 Packet BYE = new Packet(Packet.Bye);
  223.                 byte[] buffer = BYE.GetBytes();
  224.                 _client.Send(buffer, buffer.Length);
  225.             }
  226.             // Если сервер сообщил нам о завершении работы
  227.             if (senderQuit && wasRunning)
  228.                 Console.WriteLine("The sender quit on us, canceling the transfer.");
  229.             ResetTransferState(); // Очистка состояния
  230.             _shutdownRequested = false;
  231.         }
  232.         public void Close()
  233.         {
  234.             _client.Close();
  235.         }
  236.         // Попытка заполнения очереди пакетов
  237.         private void _checkForNetworkMessages()
  238.         {
  239.             if (!_running)
  240.                 return;
  241.             // Проверка что есть данные и как минимум четыре байта для типа
  242.             int bytesAvailable = _client.Available;
  243.             if (bytesAvailable >= 4)
  244.             {
  245.                 // Это будет читать ОДНУ датаграмму (даже если несколько были получены)
  246.                 IPEndPoint ep = new IPEndPoint(IPAddress.Any,  0);
  247.                 byte[] buffer = _client.Receive(ref ep);
  248.                 Packet p = new Packet(buffer);
  249.                 // Создайте сообщение и разместите ее в очередь для обработки
  250.                 NetworkMessage nm = new NetworkMessage();
  251.                 nm.Sender = ep;
  252.                 nm.Packet = p;
  253.                 _packetQueue.Enqueue(nm);
  254.             }
  255.         }
  256.         // Разархивировать блоки и сохранить их в файл
  257.         private bool _saveBlocksToFile(string filename, byte[] networkChecksum, UInt32 fileSize)
  258.         {
  259.             bool good = false;
  260.             try
  261.             {
  262.                 // Выделить память
  263.                 int compressedByteSize =  0;
  264.                 foreach (Block block in _blocksReceived.Values)
  265.                     compressedByteSize += block.Data.Length;
  266.                 byte[] compressedBytes = new byte[compressedByteSize];
  267.                 // Реконструция в один большой блок
  268.                 int cursor =  0;
  269.                 for (UInt32 id = 1; id <= _blocksReceived.Keys.Count; id++)
  270.                 {
  271.                     Block block = _blocksReceived[id];
  272.                     block.Data.CopyTo(compressedBytes, cursor);
  273.                     cursor += Convert.ToInt32(block.Data.Length);
  274.                 }
  275.                 // Сохранить
  276.                 using (MemoryStream uncompressedStream = new MemoryStream())
  277.                 using (MemoryStream compressedStream = new MemoryStream(compressedBytes))
  278.                 using (DeflateStream deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
  279.                 {
  280.                     deflateStream.CopyTo(uncompressedStream);
  281.                     // Сверка контрольной суммы
  282.                     uncompressedStream.Position =  0;
  283.                     byte[] checksum = _hasher.ComputeHash(uncompressedStream);
  284.                     if (!Enumerable.SequenceEqual(networkChecksum, checksum))
  285.                         throw new Exception("Checksum of uncompressed blocks doesn't match that of INFO packet.");
  286.                     // записать в файл
  287.                     uncompressedStream.Position =  0;
  288.                     using (FileStream fileStream = new FileStream(filename, FileMode.Create))
  289.                         uncompressedStream.CopyTo(fileStream);
  290.                 }
  291.                 good = true;
  292.             }
  293.             catch (Exception e)
  294.             {
  295.                 // Ошибка
  296.                 Console.WriteLine("Could not save the blocks to \"{0}\", reason:", filename);
  297.                 Console.WriteLine(e.Message);
  298.             }
  299.             return good;
  300.         }
  301.         #region Program Execution
  302.         public static UdpFileReceiver fileReceiver;
  303.         public static void InterruptHandler(object sender, ConsoleCancelEventArgs args)
  304.         {
  305.             args.Cancel = true;
  306.             fileReceiver?.Shutdown();
  307.         }
  308.         public static void Main(string[] args)
  309.         {
  310.             // Настройка получателя
  311.             string hostname = "localhost";//args[0].Trim();
  312.             int port = 6000;//int.Parse(args[1].Trim());
  313.             string filename = "short_message.txt";//args[2].Trim();
  314.             fileReceiver = new UdpFileReceiver(hostname, port);
  315.             // Добавить событие Ctrl+C
  316.             Console.CancelKeyPress += InterruptHandler;
  317.             // Получиться файл
  318.             fileReceiver.GetFile(filename);
  319.             fileReceiver.Close();
  320.         }
  321.         #endregion
  322.     }
  323. }

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

Shutdown() в этом классе просто запросит отмену существующей передачи файла, это также можно увидеть в методе GetFile(), как это работает. 

Функция GetFile() является самой важной функцией класса и очень похожа на UdpFileSender.Run(). Здесь и происходит общение с отправителем. В начале тела функции находятся переменные состояния передачи файлов и вспомогательная функция для их сброса, если это необходимо. В начале цикла while мы проверяем наличие новых сообщений NetworkMessages. Сначала проверяем, является ли последний полученный пакет BYE, затем он передает его в оператор switch. Здесь внутри находится управление состоянием передачи файла (REQF, ожидание ACK, ожидание INFO, ACK и т. д.). Внутри  ReceiverState.Transfering очистит очередь запросов Block, проверит наличие любых сообщений SEND (блок данных). Если очередь запросов пуста, но у нас нет всех блоков,  то пополнится очередь запросов недостающими значениями Block.Number. Но если у нас есть все блоки, мы говорим BYE отправителю (чтобы он мог принимать новые запросы), а затем мы восстанавливаем блоки в массив байт, распаковываем их и сохраняем файл.

Close() очистит основные сетевые ресурсы, которые относятся к UdpClient.

_checkForNetworkMessages() идентична той, что есть в Sender. Он видит, есть ли достаточное количество байт для сообщения пакета, а затем ставит его в очередь для обработки. 

_saveBlocksToFile() - здесь мы берем Блоки, собираем их, распаковываем данные, проверяем их с помощью контрольной суммы и затем сохраняем на диск. 


ИТОГ


Здесь всё еще есть недостатки, которые и их можно исправить. Вот некоторые из них:

  • Эта программа может занять много памяти. Когда я тестировал его с файлом большего размера, у меня было несколько ошибок OutOfMemoryExceptions. Куда правильнее было бы сохраненять блоки на диске (вместо того, чтобы хранить их в памяти), когда мы их отправляем и получаем.
  • Отправитель необходимо улучшить, добавив управление клиентами. Скажем, если бы второй  клиент подключился и отправил сообщение BYE в середине передачи, он прервал бы передачу первого клиента.
  • Тайм-ауты и повторы. Это крайне важно. Здесь полностью отсутствуют какие-либо тайм-ауты и повторы (за исключением запроса на блокировку). Когда одно из приложений ожидает ACK, оно должно дождаться установленного количества времени и затем повторно отправить свой пакет в случае, если ответа не было. Следует добавить  свои собственные повторы и тайм-ауты для контрольных пакетов. 
    • По факту метод перебора блоков сделан плохо. Он должен дождаться установленного количества времени прежде, чем сделать еще один запрос для блока. Когда я впервые протестировал это через интернет, мой получатель создал много REQB, прежде чем отправитель смог даже ответить на первый, который он получил.
Имейте в виду, что это примерное приложение, демонстрирующее UDP. Для реальной передачи файлов в вашем приложении следует внести изменения в программу для более надежной работы при использовании UDP.
 
  , ,

0 коммент. :

Отправить комментарий