Задача как-то унифицировать сериализируемые классы для общения по сети. Для этого в пакет пишется уникальный номер класса.
На данный момент этот номер хардкодится для каждого класса. Получается, создавая класс пакета, нужно придумать число, и присвоить его как статический номер класса. Для этого нужно учитывать что такой же номер пакета не был зарегистрирован в сериализаторе, иначе будет runtime ошибка.
Для того чтобы избавить разработчика сетевой логики при дизайне классов пакетов, от необходимости заботиться о номерах, хотелось бы сделать систему присвоения номера автоматической.
Но тут возникают проблемы, т.к. если использовать GetHashCode для самого Type класса, то на сервере и клиенте, они различаются, т.к. находятся в разных namespace'ах, или других причин. Тем более, GetHashCode, генерирует разные номера, в зависимости от битности системы (32/64). Также сам алгоритм меняется постоянно с новыми версиями .NET.
Можно взять имя класса, как уникальное данное, т.к. на сервере и клиенте они будут совпадать, это хороший подход. Но тогда нужно генерировать уникальный Hash (int) исходя из string'а.
Не могу придумать/найти хорошее решение для этой задачи, если есть идеи, буду рад выслушать, особенно с конкретными наводками на определённые алгоритмы, создания номера из string'а.
Можно из строки сделать MD5 хэш и запихнуть в 4 int-а или 2 long-а: http://en.csharp-online.net/Create_a_MD5_Hash_from_a_string
Если нужно именно int, то можно написать свою хэш-функцию и свернуть 128 бит в 32: http://www.concentric.net/~ttwang/tech/inthash.htm
А почему бы просто имя класса не отправлять? Решение легче некуда.
Тогда в заголовке пакета будет не 2 или 4 байта, а все те же 10+ (например: PacketAuthorizationRefused) и кодирование/декодирование в ASCII - это изврат, для того чтобы передать номер пакета. Не разумная трата трафика, будут нестабильности со многих сторон.
Пробую вариант с созданием хэша из имени класса используя CRC32.
> Не разумная трата трафика, будут нестабильности со многих сторон.
Стабильность то как раз будет нормальная. А насчёт перерасхода трафика мне сложно судить, надо смотреть на специфику программы.
В подходе с хешами есть один неприятный момент - можно словить коллизию на двух разных классах, а потом долго и нудно это дебажить. Если уж так хочется хеш, то стоит посмотреть в сторону gperf и аналогов, которые гарантируют отсутствие коллизий.
Насчёт длины пакетов, если название класса 24 значка, и содержание пакета 4 байта (uint), то для посылки одного числа, в заголовке будет ещё 24 байта, при этом ещё и сеть далее около 20-64 байт прикрепит. Ради одного числа..
А вот насчёт коллизий, тут да, но есть решение, т.к. мне хашь нужен как идентификатор, далее есть словарь, и классы идут в словарь по идентификатору, поэтому при включении приложения, он просто не позволит всунуть в словарь такой же key, от сюда можно вычеслить какой класс.
Сделал CRC32 хеш, пока полёт нормальный, около 80 разных пакетов, никаких проблем..
Шанс на совпадение очень минимальный (2^32-1).
а что мешает сделать проинициализировать все классы так, дай бог их будет сотня - это 5 минут работы...
void initNetIDs(){
int val = 0;
class1.net_id = ++val;
class2.net_id = ++val;
classOlolo.net_id = ++val;
classPыщьРыщь.net_id = ++val;
classГагагГагага.net_id = ++val;
}
MoKa
> Задача как-то унифицировать сериализируемые классы для общения по сети
Зачем? можно разжевать задачу? и ещё как именно сериализуете?
FDsagizi
Дело в том что идентификатор если использовать increment, то на клиенте если пакеты будут найдены рефлекцией в другом порядке, это уже одна мешанина, другое дело, если версия постарее, но поддерживается, колличество пакетов в фабрике пакетов, будет отличаться от сервера.
Задача иметь одинаковые идентификаторы на сервере и клиенте.
SergeyRu
> Зачем? можно разжевать задачу? и ещё как именно сериализуете?
Суть такова: для того чтобы посылать/читать пакеты, нужно составлять некий заголовок, который будет указывать на различие пакетов, таким образом мы будем знать что этот пакет с таким номером идентификатора - это запрос на список друзей, а вот тот например запрос на посылку сообщения в приват. Таким образом у нас будет различия, и ты будешь определять что читать из пакета, основываясь на его типе.
Сериализация реализована весьма просто, хотя это первый опыт подобного, вот пакет для посылки сообщения:
public class PacketUserRequest:Packet { public static uint PacketID; [SVariable(1)] uint id; public PacketUserRequest( uint id) : base( PacketID) { this.id = id; } public PacketUserRequest( Socket socket,byte[] buffer) : base( socket,buffer) { } public uint ID { get { return this.id; } } }
Он для сервера и для клиента. Это не финальная версия, т.к. нужно избавиться ещё от передачи сокета. Тут два конструктора, первый используется если нужно послать пакет, воторой если принимаем пакет (имея буффер).
PacketID - переменная которая держит уникальный идентификатор для этого класса пакета. Раньше это была const, и я её назначал ручками, но т.к. для этого нужно разработчику тогда заботиться о том какие номера уже есть, а каких нету, и т.п. Тут возникают проблемы и т.п. В общем при разработки, слишком много париться о том что тупо не нужно.
Поэтому было принято решение избавиться от ручного назначения, а использовать автоматический подход. Но т.к. на сервере и клиенте они должны совпадать, т.к. используются как идентификатор в пакете, чтобы при приёме узнать что за пакет идёт, поэтому нужно использовать алгоритм, что будет генерировать один и тот же номер из информации которая идентична на сервере и на клиенте. Поэтому был выбран подход генерировать число из имени класса, т.к. оно будет всегда совпадать у клиента и сервера. Для этого использовал CRC32 хэш функцию, работает как положено.
Есть также класс PacketFactory который занимается всеми опирациями для нахождения нужного класса и т.п. А класс Packet реализует всё чтение из буффера, запись в него и т.п.
Таким образом для создания нового пакета, нужно лишь объявить подобный класс как выше, и далее он готов к посылке:
client.Send(PacketUserRequest.PacketID,userID);
SVariable - это аттрибут, чтобы определить какие данные из пакета пихать в пакет, и читать из пакета. Он имеет индекс, и опционально если нужно записать строчку текста, кодировку (ASCII/Unicode).
Для получения пакета, не нужно иметь один из конструкторов. Также для отправки, не нужны Accessor'ы к данным и второй конструктор.
Всё это для организации удобства и стабильности в разработки логики для работы с сетью.
Кстати также, реализана Event система для пакетов, при приёме пакетов срабатывает событие, в котором данные из пакета уже готовы в удобном виде.
Таким образом, объявляем класс пакета, и событие с логикой при приёме, компилирум и всё.
Нафига такие сложности?
public abstract class NetworkPacket { public enum PacketID: uint { PacketNo1 = 1000, PacketNo2 = 1001 ... Packet100500 = 101500 } public abstract PacketID GetPacketID(); } public PacketNo1 : NetwortPacket { ... public override PacketID GetPacketID( ) { return PacketID.PacketNo1; } public void Serialize( ) { networkstream.AddData( ( uint)GetPacketID( )); } } ...
0iStalker
Значит для твоего пакета нужно за'ovveride'ить GetPacketID, и ещё ручками писать функцию Seialize. Также при добавлении пакета, писать его номер в enum.
Это пройденный этап. Удобство в твоём варианте весьма сомнительные, т.к. нужно о многих вещах заботиться. Ручками регестрировать пакеты. И с твоим вариантом, как ты будешь принимать пакет? Как найдёшь нужный класс. У меня всё автоматически:
Packet packet = PacketFactory.Make(this.buffer);
Прочтёт из буффера идентификатор класса, найдёт на сервере, проверит на валидность сериализуемые данные, и затем прочтёт всё создаст экземпляр нужного класса, например PacketAuthorizationApproved, который будет иметь внутри нужные данные. Никакой парки при создании, отправке и получении данных, всё автоматизировано.
В твоём же варианте, нада будет читать первые 32 бит (uint), и потом делать switch? Это изврат.
Сложность реализации, по сути не в самой реализации технически, а лишь логическая. При этом это даёт отличную простоту в использовании без каких либо ограничений при этом.
MoKa
> У меня всё автоматически:
Ты скорость-то своего "автоматически" измерь,... не такая уж и огромная работа, написать вручную сериализацию. Интроспекция даёт такие накладные рассходы, что мама не горюй. И в switch'е страшного ничего нет, на фоне всех остальных вещей, которые надо проверять в данных, приходящих от клиента. Ну пришлют тебе кривой пакет, с правильным ID, - что сделает твоя автоматическая десериализация?
Скорость отличная, всё что нужно хэшируется при старте приложения, далее лишь ссылки туда-сюда (статичные списки и хаш таблицы т.п.).
Кроме ID, есть также и другая информация, плюс, если прийдёт пакет с правильным ID но где-либо данные подменяны, или длина пакета ниже чем ожидается и многое другое, всё это без проблем держиться, и воспринимается как полагается.
Размер пакетов почти такой же как если работать напрямую с бинарником, ничего лишнего не пишется.
Скорость, миллионы пакетов в миллисекунды - этого достаточно. Больше жрёт сама конвертация данных из бинарных в другие, либо обработка данных пакета на уровне логики, нежели фабрика, которая лишь связывает списки и делает немного автоматической работы.
switch - не страшно, но видеть switch'и на 100 case'ов - уф какая какаша будет.
У меня же, пишешь модуль пользователей, выводишь в разные файлы, и пишешь лишь extensions и объявляешь callback'и с аттрибутами для авторегистрации в список событий для определённого пакета. Таким образом всё шустро, уложено где нужно и вполне устраивает.
Тем более, я непишу сервер для 10к+ игроков, а пишу сервер для весьма модулируемого и удобного в разработке логики приложения. А это другие задачи, потребности и т.п.
Тут скорее выглядит с твоей стороны: "зачем писать и городить, всё автаматизировать, если и вот такой простой (прямой) подход и так работает?".
Ответ: "Чтобы не париться при разработки логики приложения, о пакетах, а заниматься именно логикой.".
MoKa
> Ответ: "Чтобы не париться при разработки логики приложения, о пакетах, а
> заниматься именно логикой.".
Пока ты паришься с методом автоматического задания ID, - пацаны уже написали сериализацию/десериализацию, и давно занимаются логикой.
> Тут скорее выглядит с твоей стороны: "зачем писать и городить, всё
> автаматизировать, если и вот такой простой (прямой) подход и так работает?".
Как ты оценил его простоту? Простой, - это вот такой
public PacketNo1 { ... public uint GetPacketID() { return 1000; } public void Serialize( ) { networkstream.AddData( GetPacketID( )); } } public PacketNo2 { ... public uint GetPacketID( ) { return 1001; } public void Serialize( ) { networkstream.AddData( GetPacketID( )); } }
>и пишешь лишь extensions и объявляешь callback'и с аттрибутами для авторегистрации в список событий для определённого пакета.
Предполагается, что callback напишет за тебя компилятор? Ну и какой тогда смысл говорить про величину геморроя?
>но видеть switch'и на 100 case'ов - уф какая какаша будет.
Очень страшно,... можно поменять switch на массив
Чтобы не городить "рассуждения", опиши полный процесс по пунктам, что нужно чтобы объявить новый пакет который можно принять, и можно послать. Также опиши как ты принимаешь и работаешь с данными пакета.
И сравним где удобства в архитектуре проекта.
Тема в архиве.