Бинарная сериализация/десериализация вручную

Недавно пытался решить проблему с передачей данных по сети и попутно прочитал пару статей (и несколько видосов) про то, как работают сокеты. Заодно решил возродить свой очень древний проект клиент-сервера (который ещё с Delphi остался). Тогда я очень плохо понимал, что я делаю и почему оно не работает. Сейчас кажется, что стало понятно всё (по сравнению с тем, что было раньше).
А вопрос в следующем.
Вот допустим, надо передать через сокет структуру данных. Допустим, что структура небольшая. На C++ я делаю так:

enum PacketType
{
	ChatMessage
};

struct Packet
{
	PacketType type;
	int len;
};

int SendPacket(SOCKET* s, Packet packet)
{
	int packetSize = sizeof(Packet);
	char* buffer = (char*)_malloca(packetSize);
	if (!buffer)
	{
		return 0;
	}
	memcpy(buffer, &packet, packetSize);
	int ret = send(*s, buffer, packetSize, 0);
	_freea(buffer);
	return ret;
}

//отправка
		std::string msg = "Chat you!";
		int len = msg.length();
		Packet packet{ ChatMessage, len };
		if (SendPacket(&clientSocket, packet) == SOCKET_ERROR)
		{
			std::cerr << "Packet sending failed!" << std::endl;
			break;
		}
		if (send(clientSocket, msg.c_str(), len, 0) == SOCKET_ERROR)
		{
			std::cerr << "Message sending failed!" << std::endl;
			break;
		}

а на сервере принимаем похожим способом:

int ReceivePacket(SOCKET* s, Packet& packet)
{
	int packetSize = sizeof(Packet);
	char* buf = (char*)_malloca(packetSize);
	if (!buf)
	{
		return 0;
	}
	int ret = recv(*s, buf, packetSize, 0);
	if (ret != 0 && ret != SOCKET_ERROR)
	{
		memcpy(&packet, buf, packetSize);
	}
	_freea(buf);
	return ret;
}

		const int BUFFER_SIZE = 1024;
		char* buffer = (char*)_malloca(sizeof(char) * BUFFER_SIZE);
		if (!buffer)
		{
			std::cerr << "Can't allocate a buffer!" << std::endl;
			active = false;
			break;
		}
		while (true)
		{
			Packet packet;
			int bytesReceived = ReceivePacket(&clientSocket, packet);
			if (bytesReceived == SOCKET_ERROR || bytesReceived == 0)
			{
				std::cerr << "Packet receiving failed!" << std::endl;
				break;
			}

			memset(buffer, 0, BUFFER_SIZE);
			bytesReceived = recv(clientSocket, buffer, packet.len, 0);
			if (bytesReceived == SOCKET_ERROR || bytesReceived == 0)
			{
				std::cerr << "recv() failed!" << std::endl;
				break;
			}

			if (packet.type == ChatMessage)
			{
				if (bytesReceived != packet.len)
				{
					std::cerr << "Message receiving failed! Disconnecting the client..." << std::endl;
					closesocket(clientSocket);
					continue;
				}
				std::cout << "Received: " << buffer << std::endl;
			}
		}
		closesocket(clientSocket);

		_freea(buffer);

Тут есть что допиливать, но общий принцип рабочий.
Теперь к самой сути. На C++ можно делать так:

int SendPacket(SOCKET* s, Packet packet)
{
	int packetSize = sizeof(Packet);
	char* buffer = (char*)_malloca(packetSize);
	if (!buffer)
	{
		return 0;
	}
	memcpy(buffer, &packet, packetSize);
	int ret = send(*s, buffer, packetSize, 0);
	_freea(buffer);
	return ret;
}

То есть, просто взять указатель на структуру и через memcpy() скопировать все данные в массив байт (char). Всё просто и понятно. Потом этот массив можно либо записать в файл, либо передать через сокет. Потом вернуть эти байты обратно в структуру.
А на C# это предполагается делать через BinaryFormatter и MemoryStream:

namespace ConsoleApp2
{
    internal class Program
    {
        static void Main(string[] args)
        {
            MyClass myClass = new MyClass();
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            MemoryStream memoryStream = new MemoryStream();
            binaryFormatter.Serialize(memoryStream, myClass);

            byte[] buffer = memoryStream.ToArray();  //вот тут MyClass

            memoryStream.Dispose();
        }
    }

    public enum MyEnum { Something }

    [Serializable]
    public class MyClass
    { 
        public MyEnum MyEnum = MyEnum.Something;
        public string MyString = "something";
        public int MyInt = 666;
    }
}

А как это сделать вручную? Как узнать, сколько байт будет занимать класс и как преобразовать каждую переменную/поле в byte[]? Нельзя же просто (byte[])myInt :thinking:
Просто в целях повышения образованности

Вообще это вроде плохой способ, не гарантируется, что сработает. Если разные компиляторы и т.п.

Просто писать все примитивные поля в нужном формате.

Скорее всего еще и меньше места займет, в BinaryFormatter вроде куча лишней инфы, что за класс и т.п.

BitConverter Class (System) | Microsoft Learn

Ну как-бы да. Но в пределах одного компилятора должно быть норм :man_shrugging:

В C++, если брать ссылку на структуру (как в первом посте), тоже куча какой-то инфы пишется. Зато, так наверное быстрее, чем писать/читать каждое поле по-отдельности :man_shrugging:

Это только для чисел и bool. А как, например, массивы или строки? Получается, каждый элемент вручную? :thinking: То есть, нельзя, как в C++ взять какой-нибудь указатель? Для этого и нужен BInaryFormatter, получается?

Да, надо продумать свой формат, который потом получится прочитать обратно )

BinaryFormatter тоже проходит и пишет все элементы в каком-то своем формате.
[MS-NRBF]: .NET Remoting: Binary Format Data Structure | Microsoft Learn
https://referencesource.microsoft.com/#mscorlib/system/runtime/serialization/formatters/binary/binaryobjectwriter.cs,33

Там, кстати, проблема с безопасностью потому что он сериализует имена типов и при десереализации можно подсунуть что-то плохое. В будущем его вообще уберут.
c# - BinaryFormatter deserialise malicious code? - Stack Overflow

Ну это ясен хрен :grin: Просто я не люблю, когда нельзя котроллировать, что и как пишется. Если придётся вычитывать чем-то другим - ты просто не знаешь, как оно записано :man_shrugging:

А чем заменят? :thinking:

BinaryReader and BinaryWriter for XML and JSON.

Почему Binary, если XML и JSON текстовые? Как в них, допустим, игровую карту сохранять?

BinaryWriter вроде просто помогает выводить в виде байтов, а не string. И вывод будет 4 байта длина + байты строки.

А в чем проблема в текстовом формате карту сохранять? Она как представлена? Если массив, то библиотеки для работы с JSON и XML обычно умеют это. Может только больше места занимать, но можно придумать как оптимизировать если надо.

Ну это как-то странно :man_shrugging:
Как минимум, потому что придется переводить байты в HEX-символы и обратно. Это во-первых долго, во-вторых куча лишнего места.

В виде массива. Карта tile-based. Каждый байт это ID какого-то игрового элемента.
Однако, проект временно приостановлен. Сейчас пока кроме огрызка прототипа на C++ ничего не существует.
Сейчас пока другим занимаюсь.
Если писать каждый байт в виде FF это ведь уже два байта получится :thinking: Плюс ещё, наверное, пробелы какие-нибудь будут. Это ещё байты.

Например?

base64 например.