Передача указателей в объектах классов и структур по сети

Все исходники / Язык программирования C++ / OS Windows / Desktop / Сетевые приложения / Передача указателей в объектах классов и структур по сети

Передача различных данных по сети

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

Вместе с тем, расшифровая это обобщение, надо сказать, что для передачи по сети различных типов переменных используются разные алгоритмы. Классы имеющие только примитивные типы: int, char, bool, float, double и объекты классов, состоящие из них, занимают фиксированный объем памяти на любой машине. В передающем методе указывается адрес отправляемого объекта и размер в байтах измеренный sizeof(). При получении, аналогично, в метод Receive(...) передаётся адрес и указывается размер в байтах объекта-приёмника. Извлекающим методом чётко распределяются полученные байты в памяти занимаемым объектом класса. Но передать по сети таким способом классы и структуры содержащие переменные (строки, указатели с данными, массивы) распределяемые в динамической памяти и не получится.

Сложности передачи динамических переменных

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

Если в составе класса переменные фиксированного размера сетевой обмен строится просто. Функция sizeof() даёт точное количество байтов занимаемое в памяти объектом класса. Сложности возникают при передаче переменных с изменяемыми размерами. Указатели на объекты, на массивы, содержащие только адреса на реальные данные отправлять в сеть в составе класса не имеет смысла. Сам указатель это всего лишь переменная хранящая адрес и стандартное измерение её размера не учитывает связанные с ней данные. Если отправить в сеть в составе класса указатель на существующие данные, то эти данные неизбежно будут потеряны. Объекту отправляемого класса принадлежит только переменная-указатель, но не данные, на которые ссылается этот указатель.

Указатель содержит адрес на ячейку памяти с какими-либо объектами. Каждая машина имеет своё уникальное адресное пространство. Адрес в памяти имеет актуальность только на отправляющей машине, на принимающем устройстве, извлеченный указатель будет ссылаться на случайные данные. При приёме, да и при отправке, ненулевого указателя неизбежно возникнет исключение.

Способ передачи указателей по сети

Передача указателей вместе с "их данными" в составе класса заключается в передаче по очереди данных, с которыми они ассоциируются и на точке-приёма создание копии объекта класса на основе полученных байтов. Если в составе класса есть объект CString, перед отправкой необходимо получить указатель LPCTSTR на строку и размер в байтах применяя программный код:

int len = m_String.GetLength(); int sizeBytes = len * sizeof(TCHAR) + sizeof(TCHAR);

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

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

Листинг вспомогательного класса:
class CSendInfo
{
public:
    // Все размеры указываются в байтах.

    // Размер строки
    int m_SizeString = 0;

    // Размер массива символов
    int m_SizeArrayTCHAR = 0;

    // Размер массива целых чисел
    int m_SizeArrayInt = 0;

    // Общий размер всех данных
    int m_TotalDataSize = 0;

    // Вспомогательный метод сброса 
    // всех размеров в ноль.
    void Reset();
};

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

Исходник приложения отправки указателей по сети

Программа передачи указателей по сетиК статье прикреплён исходник примера передачи по сети указателей в составе класса. Исходник представляет собой сетевое приложение, которое может работать в двух режимах: в серверном или клиентском. Пара сервер-клиент обмениваются по локальной сети классом в составе которого указатели на объекты размещенные в динамической памяти.

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

Класс указателей CPointers

CPointers содержит несколько указателей для различных типов и вспомогательные переменные и методы для вычисления размера отправляемых данных. В качестве строк произвольной длины используются класс CString и массив символов TCHAR.

Небольшое отступление от темы статьи. TCHAR это не тип, а макрос определяющий тип символов в зависимости от настройки конфигурации приложения. TCHAR может определяться как двухбайтовый WCHAR при установке наборе символов Unicode или как однобайтовый char при настройке на многобайтовую кодировку. В нашем случае приложение настроено на использование символов Unicode.

Листинг класса CPointers:
// --- Объявление класса, файл CPointers.h ---

class CPointers
{
public:
    CPointers();
    ~CPointers();

public:
    int* m_pInt;
    double* m_pDouble;
    CString m_String;
    TCHAR* m_pTCHAR;
    int* m_pArrayInt;

private:
    int m_LenArrayTCHAR;
    int m_LenArrayInt;

public:
    LPCTSTR GetDataString(int &sizeBytes);
    int GetSizeArrayTCHAR();
    void SetArrayTCHAR(CString s);
    void SetArrayInt(int* array, int num);
    int GetSizeArrayInts();
};


//  --- Определение класса, файл CPointers.cpp ---
 ...
CPointers::~CPointers()
{
    // --- Освобождение динамической памяти ---

    // При уничтожении объекта класса, автоматически очистится память 
    // занимаемая данными, адреса которых записаны в указателях.
    // Объект m_String освобождает память самостоятельно.

    delete[] m_pTCHAR;
    m_pTCHAR = NULL;

    delete m_pInt;
    m_pInt = NULL;

    delete m_pDouble;
    m_pDouble = NULL;

    delete[] m_pArrayInt;
    m_pArrayInt = NULL;
} 
 ...

Отправка в сеть

Значения создающиеся в динамической памяти, размер которых заранее неизвестен, уверенно одним пакетом отправить не получится. В таком случае предпочтительней выбрать стратегию отправки данных порциями. Лучше создать общий буфер и в порядке очереди скопировать туда данные указателей. Благодаря стремительному языку С++, процесс копирования происходит мгновенно. Далее буфер отправляем пакетами, для рациональности, равными размеру буфера отправки.

Программный код отправки данных:
if (pSockSend != NULL)
{
    // Ставим флаг процесса отправки.
    // Пока партия данных не отправлена, следующую отправлять нельзя.
    m_FlagSent = FALSE;

    // Размер буфера отправки. 
    // Для наибольшего КПД, размер пакета должен быть равен размеру буфера отправки.
    int sizeBuffer = 0;
    int len = 4;
    if (pSockSend->GetSockOpt(SO_RCVBUF, &sizeBuffer, &len) == FALSE)
    {
        CUtil::BeepError();
    }

    // Объект, данные которого передаются по сети.
    // Данный объект может создаваться в любом месте 
    // программного кода приложения. 
    // Здесь он создаётся только для примера. 
    CPointers pointers;

    // Создаём в динамической памяти целое число и
    // сразу заполняем его значением из окна CEdit.
    pointers.m_pInt = new int;
    *pointers.m_pInt = m_valueInt;

    // Значение double в динамической памяти.
    pointers.m_pDouble = new double;
    *pointers.m_pDouble = m_valueDouble;


    // На самом деле размер массива может быть любым.
    // Но наглядность нам обеспечивают только 4 окна CEdit.
    // При желании можно добавить ещё окна и код будет работать
    // без проблем.
    int temp[4] = { m_valueArrayInt0, m_valueArrayInt1, m_valueArrayInt2, m_valueArrayInt3 };
    pointers.SetArrayInt(temp, 4);
    int sizeArrayInt = pointers.GetSizeArrayInts();


    // При получении указателя строки LPCTSTR, к размеру 
    // обязательно добавляем ещё и размер нулевого символа.
    // Это происходит в методе CPointers::GetDataString(sizeString)
    pointers.m_String = m_valueString;
    int sizeString = 0;
    LPCTSTR pString = pointers.GetDataString(sizeString);

    //
    pointers.SetArrayTCHAR(m_valueArrayChar);
    int tcharSize = pointers.GetSizeArrayTCHAR();

    // Информационный класс для сведений о размерах данных.
    CSendInfo cSendInfo;
    cSendInfo.m_SizeString = sizeString;
    cSendInfo.m_SizeArrayTCHAR = tcharSize;
    cSendInfo.m_SizeArrayInt = sizeArrayInt;

    // Итоговый размер всех данных.
    int totalDataSize = sizeof(CSendInfo) + sizeof(int) + 
        sizeof(double) + cSendInfo.m_SizeArrayInt + 
            cSendInfo.m_SizeString + cSendInfo.m_SizeArrayTCHAR;
    cSendInfo.m_TotalDataSize = totalDataSize;
		
		
    // Общий буфер для отправляемых данных
    BYTE* totalBuffer = new BYTE[totalDataSize];
    ZeroMemory(totalBuffer, totalDataSize);

    // Сдвиг адреса для добавления новых данных в общий буфер.
    int offset = 0;

    // Копирование байтов CSendInfo
    memcpy(totalBuffer + offset, &cSendInfo, sizeof(CSendInfo));

    // Копирование байтов pointers.m_pInt
    offset += sizeof(CSendInfo);
    memcpy(totalBuffer + offset, pointers.m_pInt, sizeof(int));

    // Копирование байтов pointers.m_pDouble
    offset += sizeof(int);
    memcpy(totalBuffer + offset, pointers.m_pDouble, sizeof(double));


    // Bytes of pointers.m_pArrayInt
    offset += sizeof(double);
    memcpy(totalBuffer + offset, pointers.m_pArrayInt, sizeArrayInt);

    // pString
    offset += sizeArrayInt;
    memcpy(totalBuffer + offset, pString, sizeString);

    // Массив символов 
    offset += sizeString;
    memcpy(totalBuffer + offset, pointers.m_pTCHAR, tcharSize);
		
    // Реально отправленное количество
    int totalActualSend = 0;
		
    // Отправка данных пакетами.
    while (totalActualSend < totalDataSize)
    {
        // Если размер буфера позволит, отправим всё сразу.
        int sizeCompute = (totalDataSize - totalActualSend);

        // Но если размер отправляемых данных больше буфера,
        // размер пакета устанавливаем равным буферу. 
        // Если меньше размер пакета остаётся равен отправляемому остатку.
        if ((totalDataSize - totalActualSend) > sizeBuffer)
        {
            sizeCompute = sizeBuffer;
        }
		
        // Подсчитываем реально отправляемые данные.
        int sent = pSockSend->Send(totalBuffer + totalActualSend, sizeCompute);
        totalActualSend += sent;
    }

    // Сброс данных для отправки следующей партии.
    if (totalActualSend == totalDataSize)
    {
        delete [] totalBuffer;
        totalBuffer = NULL;

        // Данные отправлены. Снимаем флаг. 
        // Разрешаем отправку следующей партии.
        m_FlagSent = TRUE;

        CUtil::BeepOk();
    }

    // Вывод статуса отправки.
    m_Status = _T(" Отправлено: ") + 
        CUtil::IntToStr(totalActualSend) + _T("(")  + 
             CUtil::IntToStr(totalDataSize) + _T(") байт");

    UpdateData(FALSE);
}

Извлечение данных из сетевого буфера

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

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

Программный код метода извлечения байтов из сетевого буфера:
ParseReceiveData(CMySocket* recvSocket)
{
    DWORD recvSize = 0;
    if (recvSocket->IOCtl(FIONREAD, &recvSize) == TRUE)
    {
        // Данный объект, после создания полной копии
        // отправленного объекта, можно передать далее
        // какой-либо метод. Здесь же он просто в конце
        // этого метода разрушается.
        CPointers pointersReceive;

        if (m_InfoOk == FALSE)
        {
            // Размер CSendInfo не более 20 байт. Первый пакет, с высокой вероятностью, 
            // размером будет больше чем объект CSendInfo.
            // Но для коммерческих версий здесь необходим 
            // код обработки ситуации когда recvSize < sizeof(CSendInfo).
            // В последующих статьях я добавлю этот код.
            //if (recvSize >= sizeof(CSendInfo))
            //{
                   BYTE* buffer = new BYTE[recvSize];
                   int check = recvSocket->Receive(buffer, recvSize);

                   m_CounterRecv += check;
				
                   // Копируем только размер объекта CSendInfo.
                   // Остальные байты будут получать другие переменные.
                   memcpy_s(&m_sendInfo, sizeof(CSendInfo), buffer, sizeof(CSendInfo));

                   // Основной буфер
                   m_DataBuffer = new BYTE[m_sendInfo.m_TotalDataSize];
                   memcpy_s(m_DataBuffer, recvSize, buffer, recvSize);

                   m_InfoOk = TRUE;
                   delete[] buffer;
                   buffer = NULL;

           //}
        }
        else
        {
            int check = recvSocket->Receive(m_DataBuffer + m_CounterRecv, recvSize);

            m_CounterRecv += check;
        }

        // Проверка окончания приёма данных.
        if (m_sendInfo.m_TotalDataSize > 0 && m_CounterRecv >= m_sendInfo.m_TotalDataSize)
        {
            // Возможная ошибка.
            if (m_CounterRecv > m_sendInfo.m_TotalDataSize)
            {
                CUtil::BeepError();
            }
			
            // Смещение точки отсчёта для копирования следующих данных.
            int offset = sizeof(CSendInfo);

            // --- Получаем значение m_pInt класса CPointers ---

            int* i = new int;
            memcpy_s(i, sizeof(int), m_DataBuffer + offset, sizeof(int));

            //  Восстановление данных указателя
            pointersReceive.m_pInt = i;
            // Показ значения в окне интерфейса
            m_valueInt = *pointersReceive.m_pInt;

			
            // --- Получаем значение m_pDouble класса CPointers ---

            offset += sizeof(int);
            double* d = new double;
            memcpy_s(d, sizeof(double), m_DataBuffer + offset, sizeof(double));

            pointersReceive.m_pDouble = d;
            m_valueDouble = *pointersReceive.m_pDouble;

            // --- Получаем значение массива целочисленных значений CPointers::m_pArrayInt --- 

            offset += sizeof(double);
            int* pArrayInt = new int[m_sendInfo.m_SizeArrayInt];
            memcpy_s(pArrayInt, m_sendInfo.m_SizeArrayInt, 
                m_DataBuffer + offset, m_sendInfo.m_SizeArrayInt);

            // Восстановление указателя на массив
            pointersReceive.m_pArrayInt = pArrayInt;

            // Показ значений в окнах интерфейса
            m_valueArrayInt0 = pointersReceive.m_pArrayInt[0];
            m_valueArrayInt1 = pointersReceive.m_pArrayInt[1];
            m_valueArrayInt2 = pointersReceive.m_pArrayInt[2];
            m_valueArrayInt3 = pointersReceive.m_pArrayInt[3];


            // --- Получаем данные строки CString m_String класса CPointers --- 

            offset += m_sendInfo.m_SizeArrayInt;
            BYTE* pString = new BYTE[m_sendInfo.m_SizeString];
            memcpy_s(pString, m_sendInfo.m_SizeString, 
                m_DataBuffer + offset, m_sendInfo.m_SizeString);

            pointersReceive.m_String = (LPCTSTR)pString; //(TCHAR*)pString;
            m_valueString = pointersReceive.m_String; 

            // Байты строки pString полностью копируются в объект pointersReceive.m_String,
            // поэтому память занятую pString необходимо освобождать.
            delete[] pString;
            pString = NULL;

            // --- Поучаем байты массива символов CPointers::m_pTCHAR ---

            offset += m_sendInfo.m_SizeString;
            TCHAR* pArrayChar = new TCHAR[m_sendInfo.m_SizeArrayTCHAR];
            memcpy_s(pArrayChar, m_sendInfo.m_SizeArrayTCHAR, 
                m_DataBuffer + offset, m_sendInfo.m_SizeArrayTCHAR);
			 
            pointersReceive.m_pTCHAR = pArrayChar;
            m_valueArrayChar = pointersReceive.m_pTCHAR;

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

            // Получена точная копия объекта класса CPointers, данные которого 
            // были отправлены в сеть.
            // pointersReceive

            // Индикация статуса приёма данных.			
            m_Status = L" Получено:" + CUtil::IntToStr(m_CounterRecv) + L" байт";

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

		
    }
    else
    {
        CUtil::BeepError();
    }

    UpdateData(FALSE);

    return FALSE;
}

Исходник примера передачи указателей по сети

Что было описано выше реализовано в исходнике приложения прикрепленного к данной статье. Исходник создан в интегрированной среде программирования MS Visual Studio 2019. Для наглядного графического интерфейса исходника С++ приложение построено на диалоговых окнах MFC (Microsoft Foundation Classes). Библиотека MFC в разы повышает скорость разработки на языке программирования C++.