По локальной сети, между приложениями, можно передавать любые программные данные. Передаваемая информация может состоять из целочисленных переменных, чисел с плавающей запятой, логических переменных, одиночных символов, строк фиксированной и произвольной длины, массивов различных типов, объектов классов и структур, состоящих из впереди перечисленного. Условно можно даже сказать, что по сети передаются и указатели вместе с информацией по адресам хранящихся у в этих указателях. Логически связанные данные целесообразно передавать по сети в составе класса или структуры.
Вместе с тем, расшифровая это обобщение, надо сказать, что для передачи по сети различных типов переменных используются разные алгоритмы. Классы имеющие только примитивные типы: 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 содержит несколько указателей для различных типов и вспомогательные переменные и методы для вычисления размера отправляемых данных. В качестве строк произвольной длины используются класс 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++.
Скачать исходник
Тема: «Передача указателей в объектах классов и структур по сети»
MFCPointerNetwork-vs16.zip
Размер:3853 КбайтЗагрузки:341