Передача файлов по UDP

120

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

Программы отправителя и получателя разделены на две логические части. В первой части отправитель посылает получателю (или получателям) информацию о файле (а именно расширение и размер файла) как сериализованный объект, а во второй части отправляется сам файл. В получателе первая часть принимает сериализованный объект с соответствующей информацией, а вторая часть создает файл на машине получателя. Чтобы сделать приложение более интересным, откроем сохраненный файл соответствующей программой (например, .doc-файл можно открыть программой Microsoft Word, а .html-файл — браузером).

Файловый сервер

Файловый сервер — это простое консольное приложение, реализованное в классе UdpFileServer. В этом классе есть вложенный класс FileDetails, содержащий информацию о файле — тип и размер файла. Начнем с импорта необходимых пространств имен и объявления полей класса. В классе есть пять закрытых полей: экземпляр класса FileDetails, объект UdpClient, а также информация о соединении с удаленным клиентом и объект FileStream для считывания файла, который отправляется клиенту:

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Xml.Serialization;
using System.Diagnostics;
using System.Threading;

public class UdpFileServer
{
    // Информация о файле (требуется для получателя)
    [Serializable]
    public class FileDetails
    {
        public string FILETYPE = "";
        public long FILESIZE = 0;
    }
    
    private static FileDetails fileDet = new FileDetails();

    // Поля, связанные с UdpClient
    private static IPAddress remoteIPAddress;
    private const int remotePort = 5002;
    private static UdpClient sender = new UdpClient();
    private static IPEndPoint endPoint;

    // Filestream object
    private static FileStream fs;

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

[STAThread]
static void Main(string[] args)
{
        try
        {
            // Получаем удаленный IP-адрес и создаем IPEndPoint
            Console.WriteLine("Введите удаленный IP-адрес");
            remoteIPAddress = IPAddress.Parse(Console.ReadLine().ToString());//"127.0.0.1");
            endPoint = new IPEndPoint(remoteIPAddress, remotePort);

            // Получаем путь файла и его размер (должен быть меньше 8kb)
            Console.WriteLine("Введите путь к файлу и его имя");
            fs = new FileStream(@Console.ReadLine().ToString(), FileMode.Open, FileAccess.Read);

            if (fs.Length > 8192)
            {
                Console.Write("Файл должен весить меньше 8кБ");
                sender.Close();
                fs.Close();
                return;
            }

            // Отправляем информацию о файле
            SendFileInfo();

            // Ждем 2 секунды
            Thread.Sleep(2000);

            // Отправляем сам файл
            SendFile();

            Console.ReadLine();

        }
        catch (Exception eR)
        {
            Console.WriteLine(eR.ToString());
        }
}

Метод SendFileInfo() заполняет поля объекта FileDetails, а затем сериализует объект в MemoryStream, используя объект XmlSerializer. Этот объект считывается в массив байтов и передается методу Send() класса UdpClient, который отправляет информацию о файле клиенту:

public static void SendFileInfo()
{

        // Получаем тип и расширение файла
        fileDet.FILETYPE = fs.Name.Substring((int)fs.Name.Length - 3, 3);

        // Получаем длину файла
        fileDet.FILESIZE = fs.Length;

        XmlSerializer fileSerializer = new XmlSerializer(typeof(FileDetails));
        MemoryStream stream = new MemoryStream();

        // Сериализуем объект
        fileSerializer.Serialize(stream, fileDet);

        // Считываем поток в байты
        stream.Position = 0;
        Byte[] bytes = new Byte[stream.Length];
        stream.Read(bytes, 0, Convert.ToInt32(stream.Length));

        Console.WriteLine("Отправка деталей файла...");

        // Отправляем информацию о файле
        sender.Send(bytes, bytes.Length, endPoint);
        stream.Close();

}

Метод SendFile() просто считывает содержимое файла из FileStream в массив байтов и отправляет его клиенту:

private static void SendFile()
{
        // Создаем файловый поток и переводим его в байты
        Byte[] bytes = new Byte[fs.Length];
        fs.Read(bytes, 0, bytes.Length);

        Console.WriteLine("Отправка файла размером " + fs.Length + " байт");
        try
        {
            // Отправляем файл
            sender.Send(bytes, bytes.Length, endPoint);	
        }
        catch (Exception eR)
        {
            Console.WriteLine(eR.ToString());
        }
        finally
        {
            // Закрываем соединение и очищаем поток
            fs.Close();
            sender.Close();
        }
        Console.WriteLine("Файл успешно отправлен.");
        Console.Read();
}

Файловый клиент

Файловый клиент или приемник файла — тоже консольное приложение, реализованное в классе UdpFileClient. Здесь также начинаем с импорта необходимых пространств имен и объявления полей класса:

using System;
using System.IO;
using System.Net;
using System.Diagnostics;
using System.Net.Sockets;
using System.Text;
using System.Xml.Serialization;

public class UdpFileClient
{
    // Детали файла
    [Serializable]
    public class FileDetails
    {
        public string FILETYPE = "";
        public long FILESIZE = 0;
    }
    
    private static FileDetails fileDet;

    // Поля, связанные с UdpClient
    private static int localPort = 5002;
    private static UdpClient receivingUdpClient = new UdpClient(localPort);
    private static IPEndPoint RemoteIpEndPoint = null;

    private static FileStream fs;
    private static Byte[] receiveBytes = new Byte[0];

Метод Main() этого приложения только вызывает два метода, чтобы получить, соответственно, информацию о файле и сам файл:

[STAThread]
static void Main(string[] args)
{
        // Получаем информацию о файле
        GetFileDetails();

        // Получаем файл
        ReceiveFile();
}

Метод GetFileDetails() вызывает метод Receive() объекта UdpClient. Тот в свою очередь получает от сервера сериализованный объект FileDetails, который сохраняется в объекте MemoryStream. Для десериализации этого потока в объект FileDetails используем объект XmlSerializer и отображаем полученную информацию на консоли:

private static void GetFileDetails()
{
        try
        {
            Console.WriteLine("-----------*******Ожидание информации о файле*******-----------");
            
            // Получаем информацию о файле
            receiveBytes = receivingUdpClient.Receive(ref RemoteIpEndPoint);
            Console.WriteLine("----Информация о файле получена!");

            XmlSerializer fileSerializer = new XmlSerializer(typeof(FileDetails));
            MemoryStream stream1 = new MemoryStream();

            // Считываем информацию о файле
            stream1.Write(receiveBytes, 0, receiveBytes.Length);
            stream1.Position = 0;

            // Вызываем метод Deserialize
            fileDet = (FileDetails)fileSerializer.Deserialize(stream1);
            Console.WriteLine("Получен файл типа ." + fileDet.FILETYPE + 
                " имеющий размер " + fileDet.FILESIZE.ToString() + " байт");
        }
        catch (Exception eR)
        {
            Console.WriteLine(eR.ToString());
        }
}

Метод ReceiveFile() получает файл от сервера и сохраняет его на диске под именем temp, добавляя расширение, извлеченное из объекта FileDetails. Затем вызываем статический метод Process.Start() и открываем документ связанной с расширением программой:

public static void ReceiveFile()
{
        try
        {
            Console.WriteLine("-----------*******Ожидайте получение файла*******-----------");
            
            // Получаем файл
            receiveBytes = receivingUdpClient.Receive(ref RemoteIpEndPoint);

            // Преобразуем и отображаем данные
            Console.WriteLine("----Файл получен...Сохраняем...");

            // Создаем временный файл с полученным расширением
            fs = new FileStream("temp." + fileDet.FILETYPE, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
            fs.Write(receiveBytes, 0, receiveBytes.Length);

            Console.WriteLine("----Файл сохранен...");

            Console.WriteLine("-------Открытие файла------");

            // Открываем файл связанной с ним программой
            Process.Start(fs.Name);			
        }
        catch (Exception eR)
        {
            Console.WriteLine(eR.ToString());
        }
        finally
        {
            fs.Close();
            receivingUdpClient.Close();
            Console.Read();
        }
}

Далее показан вывод на консоль на клиенте и сервере, полученный после запуска этой программы:

Клиент-серверное UDP-приложение по загрузке файла

Это приложение имеет некоторые ограничения. Во-первых, размер файла зависит от размера внутреннего буфера сообщений или предела сети. По умолчанию размер буфера равен 8192 байтам, поэтому невозможно отправлять файлы больше 8192 байтов. Это ограничение преодолимо — достаточно разбить файл на несколько частей, не превышающих размер буфера. Указывая при вызове метода Read() требуемый размер буфера, можно разделить файл на несколько частей.

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

Широковещательная передача

При выполнении этого приложения можно указать отдельный IP-адрес, по которому будет отправлен файл, но также можно задать широковещательный адрес, чтобы отправить файл всем машинам подсети или всей сети. Широковещательный адрес состоит из идентификатора подсети и единиц во всех остальных битах. Например, если хотим разослать сообщение всем хостам в диапазоне 192.168.0, то нужно использовать широковещательный адрес 192.168.0.255. Чтобы отправить сообщение всем машинам сети независимо от маски подсети, мы можем использовать адрес 255.255.255.255. При широковещательной передаче сообщение отправляется всем машинам сети, а клиенту предстоит решить, хочет ли он обработать эти данные.

Пройди тесты
Лучший чат для C# программистов