Отправка большого файла по сети

Все исходники /  Язык программирования C++ /  OS Windows /  Desktop /  Сетевые приложения / Отправка большого файла по сети

Приложение отправки файла по сети

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

Работа приложения построена на асинхронных сокетах класса CAsynsSocket библиотеки С++ MFC.

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

Отправка большого файла

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

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

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

Листинг информационной структуры:
// Размер структуры 1028 байт.
struct BUFFERINFO
{
    BUFFERINFO()
    {
        //  Обнуляем члены массива символов,
        // иначе название файла может исказиться.
        SecureZeroMemory(FileName, 512);
    }
  
    // Длина файла    
    int FileLength = 0;
    // Название файла.
    TCHAR FileName[512];
};

Событие OnSend() CAsyncSocket

Работа асинхронного сокета класса CAsyncSocket построена на событиях. Это значит, что отправку и приём сообщений необходимо выполнять либо внутри метода события, либо косвенно ожидать вызов соответствующего события. В прилагаемом исходнике применено косвенное ожидание. Если мы будем отправлять небольшое сообщение методом Send(), вполне возможно что оно успешно достигнет принимающей стороны. В локальной сети таким образом можно успешно отправлять даже сообщения среднего размера. Но при отправке по сети между разными компьютерами вероятность успешной отправки значительно уменьшится. А при отправке больших файлов она приблизится к нулю.

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

Работая с асинхронными сокетами MFC CAsyncSocket, для гарантии надежной отправки сообщений по сети, необходимо учитывать вызовы события сокета OnSend(). Как это работает на практике: после отправки каждого пакета необходимо проверять успешность отправки, если возврат метода Send() равен константе SOCKET_ERROR (значение -1) исследуем последнюю ошибку с помощью функции GetLastError(). Если ошибка равна числу 10035 или константе WSAEWOULDBLOCK, приостанавливаем отправку и ждем вызова события OnSend().

WSAEWOULDBLOCK означает временную недоступность буфера отправки (переполнение буфера отправки), связанной с различными факторами (производительность компьютеров, загруженность сети, величина отправляемых байтов и др.) Возникновение события OnSend() класса CAsynsSocket означает освобождение буфера приёма и возможность отправки следующей партии сообщения методом Send(). Вновь анализируем возврат: есть отправленное количество байтов - отправляем следующий пакет, если ошибка WSAEWOULDBLOCK (число 10035) - ждем события OnSend(). Получается цикл с проверками результата работы метода отправки байтов. Во время ожидания вызова события OnSend() необходимо обеспечить непрерывную обработку цикла сообщений потока приложения, иначе программа зависнет, отправка окончательно заблокируется.

Вырезка из исходника. Цикл отправки пакетов в сеть и проверки результата:
// Отправка данных пакетами.
while (actualSendBytes < size)
{
    ...

    // Отправлять можно.
    if (m_IsCanSend == TRUE)
    {
        // Подсчитываем реально отправляемые данные.
        int sent = m_Socket.Send(pBuffer + actualSendBytes, sizeCompute);

        
        // Ошибка отправки.
        if (sent == SOCKET_ERROR)
        {
            // Остановка отправки.
            m_IsCanSend = FALSE;
      
	        // Получение последней ошибки после метода Send().
            DWORD error = GetLastError();

            // Анализ ошибки.
            if (error == WSAEWOULDBLOCK)
            {
                // Необходимо подождать пока сработает разрешение OnSend сокета.
                ShowServiceMessage(L"Буфер отправки временно недоступен!");
            }
            else
            {
                // Обработка других ошибок. Возможна отмена цикла.
            }
            
        }
        else
        {
            // Подсчёт общего количества отправленных байтов.
            actualSendBytes += sent;
        }
    }
    else
    {
        // Анализ ожидания события OnSend(). Возможна отмена цикла.
    }
    
    // Обработка сообщений потока приложения.
    ProccesMessages();
    ...
}

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

Буферы отправки и приёма

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

Если работа происходит по правилам протокола TCP, стороны могут обмениваться информацией производительности. Размер буфера отправки при настройках по умолчанию автоматически корректируется в зависимости от возможностей отправляющей и принимающей стороны. Если размер буфер приёма ограничить принудительно, событие OnReceive() вызывается повышенное количество раз.

Приём первых пакетов. Буфер информации

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

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

Буфер информации небольшой по размеру, около 1КБ, но вполне возможно что за один приём весь буфер не будет получен. В этом случае необходимо временное хранилище, пока не извлечётся необходимое количество байтов для буфера информации. В приложении роль такого хранилища выполняет вектор из стандартной библиотеки С++. Массив-вектор сохраняет полную информацию о каждом пакете: размер пакета и непосредственно принятые байты в экземпляре структуры BUFF.

Листинг структуры хранения данных первых пакетов:
struct BUFF
{
    BUFF()
    {
        size = 0;
        pArray = NULL;
    }
    // Размер пакета
    DWORD size;
    // Байты пакета
    BYTE* pArray;
};

Метод приёма первых байтов гарантирует надёжное извлечение информации об отправленном файле даже если установить размер буфера приёма в 1 байт. Наследник скорости ассемблера - язык С++ и небольшой размер структуры информации обеспечивает высокую скорость получения первых байтов даже применяя высокоуровневый контейнер std::vector.

Поскольку вызов события приёма OnReceive() происходит после каждого прихода пакета экземпляр вектора сделан статическим. Такое построение даёт возможность поэтапного накопления байтов и локализировать вспомогательный массив в пределах тела одного метода. После выполнения своей задачи в этом же методе вектор очищается и готов к приему следующего файла.

Метод приёма буфера информации:
int CMFCSendFilesNetworkDlg::ReceiveBufferInfo()
{
    // Получаем размер информационного буфера
    int sizeBufferInfo = sizeof(BUFFERINFO);

    DWORD canReadBytes = 0;
    if (m_Socket.IOCtl(FIONREAD, &canReadBytes) == FALSE)
    {
        // Код определения ошибки
    }
    else
    {
        // Вывод информации в окно списка
        ShowServiceMessage(L"Размер данных в буфере приёма - " + IntToStr(canReadBytes));

        // Буфер только для одного пакета.
        BYTE* pBuffer = new BYTE[canReadBytes];
        ZeroMemory(pBuffer, canReadBytes);

        // Извлекаем все байты пришедших в буфер приёма на данный момент.
        UINT actualBytes = m_Socket.Receive(pBuffer, canReadBytes);

        // Вектор-массив для накопления первых байтов.
        static std::vector vec;

        // Добавление в вектор контента и размера текущего пакета.
        BUFF buff;
        buff.size = actualBytes;
        buff.pArray = new BYTE[actualBytes];
        ZeroMemory(buff.pArray, actualBytes);
        memcpy_s(buff.pArray, actualBytes, pBuffer, actualBytes);
        vec.push_back(buff);

        // Теперь временный буфер можно удалить.
        delete[] pBuffer;
        pBuffer = NULL;

        // Подсчёт количества уже принятых байтов.
        DWORD check = 0;
        for (int i = 0; i < vec.size(); i++)
        {
            check += vec[i].size;
        }

        // Как только количество первых байтов будет равно или больше
        // размера буфера информации, можно приступать к расшифровке 
        // информации об извлекаемом файле.
        if (check >= sizeBufferInfo)
        {
            // Подсчёт общего числа принятых байтов. Необходимо для получения остальных байтов.
            m_CounterRecv = check;

            // Байты хранимые в векторе копируем в массив байтов.
            BYTE* pVecBuffer = new BYTE[check];
            int step = 0;
            for (int i = 0; i < vec.size(); i++)
            {
                memcpy_s(pVecBuffer + step, vec[i].size, vec[i].pArray, vec[i].size);

                step += vec[i].size;
            }

            // Расшифровываем информацию.
            memcpy_s(&m_RcvBufferInfo, sizeof(BUFFERINFO), pVecBuffer, sizeof(BUFFERINFO));

            // Вывод названия и размера файла в окно.
            m_wndRcvFileName.SetWindowText(m_RcvBufferInfo.FileName);
            m_wndRcvFileSize.SetWindowText(IntToStr(m_RcvBufferInfo.FileLength));

            // Теперь стал известен общее количество извлекаемых байтов.
            int totalSize = sizeof(BUFFERINFO) + m_RcvBufferInfo.FileLength;
    
            // Подготовка массива байтов для всего сообщения.
            m_pReceiveBuffer = new BYTE[totalSize];
            ZeroMemory(m_pReceiveBuffer, totalSize);

            // Копирование уже полученных байтов.
            memcpy_s(m_pReceiveBuffer, check, pVecBuffer, check);

            // Теперь удаляем ненужный массив.
            delete[] pVecBuffer;
            pVecBuffer = NULL;

            // Освобождаем память от данных вектора.
            for (int i = 0; i < vec.size(); i++)
            {
                delete[] vec[i].pArray;
                vec[i].pArray = NULL;
            }
            // Очищаем вектор для следующей сессии сетевого обмена.
            vec.clear();

            return 0;
        }

    }

    return -1;
}

Извлечение байтов файла

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

Упрощенный листинг метода полного извлечения байтов сообщения из сети:
// Общий метод извлечения данных. 
BOOL CMFCSendFilesNetworkDlg::ReceiveData()
{
    // Модуль приёма первых байтов.
    if (m_BufferInfoOk == false)
    {
        if (ReceiveBufferInfo() == 0) m_BufferInfoOk = TRUE;
    }
    else
    {
        // Общий размер принимаемых данных
        UINT totalSize = sizeof(BUFFERINFO) + m_RcvBufferInfo.FileLength;

        // Количество байтов в буфере приёма на текущий вызов события OnReceive()
        DWORD canReadBytes = 0;
        m_Socket.IOCtl(FIONREAD, &canReadBytes);

        int sizeBuffer = 0;

        // Размер переменной sizeBuffer.
        int len = 4;

        if (m_Socket.GetSockOpt(SO_RCVBUF, &sizeBuffer, &len) == TRUE)
        {
            // Если остаток принимаемых данных меньше возможностей приёма,
            // размер для принимаемых данных установим равным размеру остатка.
            if (totalSize - m_CounterRecv < canReadBytes)
            {
                canReadBytes = totalSize - m_CounterRecv;
            }

            // Подсчёт реально принятых данных.
            int check = m_Socket.Receive(m_pReceiveBuffer + m_CounterRecv, canReadBytes);

            if (check != SOCKET_ERROR)
            {
                m_CounterRecv += check;

                m_wndRcvResultBytes.SetWindowText(IntToStr(m_CounterRecv));
            }
            else
            {
                DWORD error = GetLastError();

                if (error == WSAEWOULDBLOCK)
                {
                    ShowServiceMessage(L"Блокировка приёма!");
                }
                else
                {

                    // В случае ошибки работы сокета необходимо освободить используемые ресурсы.
                    // Освобождаем память занятую главным буфером.
                    delete[] m_pReceiveBuffer;
                    m_pReceiveBuffer = NULL;

                    // Сброс глобальных переменных-членов для следующего сеанса по сети.
                    m_CounterRecv = 0;
                    m_BufferInfoOk = FALSE;

                    return FALSE;
                }
            }
        }

        // Проверка окончания приёма байтов. 
        if (totalSize > 0 && m_CounterRecv == totalSize)
        {
            // Вывод информации в окно.
            m_wndRcvResultBytes.SetWindowText(IntToStr(m_CounterRecv - sizeof(BUFFERINFO)));

            // Создание и сохранение копии отправленного файла.
            SaveFile(m_RcvBufferInfo.FileName, m_pReceiveBuffer + sizeof(BUFFERINFO), 
                            m_RcvBufferInfo.FileLength);

            // Освобождаем память занятую главным буфером.
            delete[] m_pReceiveBuffer;
            m_pReceiveBuffer = NULL;


            // Сброс глобальных переменных-членов для следующего сеанса по сети.
            m_CounterRecv = 0;
            m_BufferInfoOk = FALSE;

            return TRUE;
        }
    }

    return FALSE;
}

Ситуация когда возникает ошибка WSAEWOULDBLOCK хотя и описана в документации к OnReceive() CAsyncSocket, но при различных условиях тестирования она не возвращалась методом извлечения сетевых данных. Даже при малом размере приёмного буфера в 100 байт не происходит блокировка. Думается, что алгоритм сетевого обмена вероятнее всего придерживает (уменьшает размер пакетов или блокирует ) буфер отправки, чтобы события OnReceive() шли чётко друг за другом.

Буфер приема и возможности приема

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

Узнать размер буфера приёма можно вызвав метод класса CAsyncSocket GetSockOpt() с параметром SO_RCVBUF. Не всегда в буфере приёма количество пришедших данных совпадает с его размером. Часто возможное количество считываемых байтов, из-за загруженности сети и компьютеров, меньше буфера извлечения. Чтобы получить количество байтов которые находятся в буфере приёма при текущем вызове события извлечения необходимо применить метод CAsyncSocket::IOCtl с параметром FIONREAD. Именно это максимальное количество которое можно считать за текущий вызов OnReceive(), события извлечения данных.

// --- Получение размера буфера приёма ---
// Переменная в которую считывается размер.
int sizeBuffer = 0;
// Выделение памяти для переменной sizeBuffer.
int len = 4;

if (m_Socket.GetSockOpt(SO_RCVBUF, &sizeBuffer, &len) == TRUE)
{
    // Успешное выполнение
}
else
{
    // Обработка ошибок
}

...

// --- Получение количества данных пришедших в буфер приёма ---
DWORD canReadBytes = 0;
if(m_Socket.IOCtl(FIONREAD, &canReadBytes) == TRUE)
{
    // Успешное получение
}
else
{
    // Обработка ошибок
}

Исходник сетевого приложения

Исходный код создан в среде программирования MS Visual Studio 2019. Язык программирования С++. В комплекте с исходником файл приложения для быстрого ознакомления с работой сетевого обмена файлами.

Скачать исходник