Мульти-клиент, асинхронные сокеты в C#, лучшие практики?
Я пытаюсь лучше понять сокеты tcp/ip в C#, так как я хочу испытать себя, чтобы посмотреть, смогу ли я создать работающую инфраструктуру MMO (игровой мир, карту, игроков и т. Д.) Исключительно для образовательных целей, поскольку у меня нет намерение стать еще одним из тех "OMGZ из меня сделает мою r0x0r MMORPG, которая будет лучше, чем WoW!!!", вы знаете, о чем я говорю.
Во всяком случае, мне было интересно, если кто-нибудь может пролить свет на то, как можно подходить к разработке такого рода системы и какие вещи требуются, и что я должен остерегаться?
Моя первоначальная идея состояла в том, чтобы разбить систему на отдельные клиент-серверные соединения, при этом каждое соединение (через свой порт) выполняет определенную задачу, такую как обновление позиций игрока / монстра, отправка и получение сообщений чата и т. Д., Которые я бы сделал обработка данных проще, потому что вам не всегда нужно помещать заголовок в данные, чтобы знать, какую информацию содержит пакет.
Это имеет смысл и полезно или я просто слишком усложняю вещи?
Ваши ответы очень ценятся.
4 ответа
Если вы занимаетесь программированием на уровне сокетов, то независимо от того, сколько портов вы открываете для каждого типа сообщений, вам все равно нужен какой-то заголовок. Даже если это всего лишь длина остальной части сообщения. Сказав, что к сообщению легко добавить простую структуру заголовка и хвоста. Я думаю, что проще иметь дело только с одним портом на стороне клиента.
Я считаю, что современные MMORPG (и, возможно, даже старые) имели два уровня серверов. Серверы входа, которые подтверждают, что вы платный клиент. После проверки они передают вас на игровой сервер, который содержит всю информацию об игровом мире. Тем не менее, это все еще требует, чтобы клиент имел один открытый сокет, но не запрещает иметь больше.
Кроме того, большинство MMORPGS также шифруют все свои данные. Если вы пишете это как упражнение для развлечения, тогда это не будет иметь большого значения.
Для проектирования / написания протоколов в целом вот что я беспокоюсь:
порядок байтов
Всегда ли клиент и сервер всегда имеют одинаковую последовательность. Если нет, то мне нужно обработать это в моем коде сериализации. Есть несколько способов справиться с порядком байтов.
- Проигнорируйте это - очевидно, плохой выбор
- Укажите порядковый номер протокола. Это то, что старые протоколы делали / делают, отсюда и термин сетевой порядок, который всегда был прямым порядком байтов. На самом деле не имеет значения, какой порядок байтов вы указываете, просто вы указываете один или другой. Некоторые старые программисты, работающие в сети, возьмутся в руки, если вы не будете использовать большие порядковые номера, но если ваши серверы и большинство клиентов имеют младший порядковый номер, вы действительно не купите себе ничего, кроме дополнительной работы, сделав протокол большим порядковым номером.
- Пометьте Endianess в каждом заголовке - вы можете добавить cookie, который сообщит вам об Endianess клиента / сервера и позволит каждому сообщению соответствующим образом преобразовываться по мере необходимости. Дополнительная работа!
- Сделайте свой протокол независимым - если вы отправляете все как строки ASCII, тогда endianess не имеет значения, но также намного более неэффективен.
Из 4 я обычно выбираю 2 и указываю, что порядок байтов равен порядку байтов большинства клиентов, которые в наши дни будут иметь порядок байтов.
Прямая и обратная совместимость
Должен ли протокол быть прямым и обратно совместимым. Ответ почти всегда должен быть да. В этом случае это определит, как я спроектирую общий протокол с точки зрения управления версиями, и как каждое отдельное сообщение создается для обработки незначительных изменений, которые на самом деле не должны быть частью управления версиями. Вы можете использовать это и использовать XML, но вы теряете большую эффективность.
Для общего управления версиями я обычно проектирую что-то простое. Клиент отправляет сообщение управления версиями, указывающее, что он говорит версию XY, при условии, что сервер может поддерживать эту версию, он отправляет обратно сообщение, подтверждающее версию клиента, и все продолжается. В противном случае он блокирует клиента и разрывает соединение.
Для каждого сообщения у вас есть что-то вроде следующего:
+-------------------------+-------------------+-----------------+------------------------+
| Length of Msg (4 bytes) | MsgType (2 bytes) | Flags (4 bytes) | Msg (length - 6 bytes) |
+-------------------------+-------------------+-----------------+------------------------+
Длина, очевидно, говорит вам, как долго это сообщение, не считая самой длины. MsgType является типом сообщения. Для этого всего два байта, поскольку 65356 - это множество типов сообщений для приложений. Флаги есть, чтобы вы знали, что сериализовано в сообщении. Это поле в сочетании с длиной дает вам прямую и обратную совместимость.
const uint32_t FLAG_0 = (1 << 0);
const uint32_t FLAG_1 = (1 << 1);
const uint32_t FLAG_2 = (1 << 2);
...
const uint32_t RESERVED_32 = (1 << 31);
Тогда ваш код десериализации может сделать что-то вроде следующего:
uint32 length = MessageBuffer.ReadUint32();
uint32 start = MessageBuffer.CurrentOffset();
uint16 msgType = MessageBuffer.ReadUint16();
uint32 flags = MessageBuffer.ReadUint32();
if (flags & FLAG_0)
{
// Read out whatever FLAG_0 represents.
// Single or multiple fields
}
// ...
// read out the other flags
// ...
MessageBuffer.AdvanceToOffset(start + length);
Это позволяет вам добавлять новые поля в конец сообщений без необходимости пересмотра всего протокола. Это также гарантирует, что старые серверы и клиенты будут игнорировать флаги, о которых они не знают. Если им нужно использовать новые флаги и поля, вы просто меняете общую версию протокола.
Использовать каркасную работу или нет
Существуют различные сетевые инфраструктуры, которые я хотел бы использовать в бизнес-приложениях. Если бы у меня не было особой необходимости царапаться, я бы использовал стандартную структуру. В вашем случае вы хотите изучить программирование на уровне сокетов, так что на этот вопрос вы уже ответили.
Если кто-то использует фреймворк, убедитесь, что он решает две проблемы, описанные выше, или, по крайней мере, не мешает вам, если вам нужно настроить его в этих областях.
Имею ли я дело с третьей стороной
Во многих случаях вы можете иметь дело со сторонним сервером / клиентом, с которым вам нужно общаться. Это подразумевает несколько сценариев:
- У них уже есть определенный протокол - просто используйте их протокол.
- У вас уже есть определенный протокол (и они готовы его использовать) - снова просто используйте определенный протокол
- Они используют стандартный фреймворк (на основе WSDL и т. Д.) - используют фреймворк.
- Ни одна из сторон не определилась с протоколом - попытайтесь определить наилучшее решение на основе всех имеющихся факторов (всех тех, что я упомянул здесь), а также уровня их компетенции (по крайней мере, насколько вы можете судить). Независимо от того, убедитесь, что обе стороны согласны и понимают протокол. По опыту это может быть болезненным или приятным. Это зависит от того, с кем вы работаете.
В любом случае вы не будете работать с третьей стороной, так что это просто добавлено для полноты.
Мне кажется, что я мог бы написать об этом гораздо больше, но это уже довольно долго. Я надеюсь, что это поможет, и если у вас есть какие-либо конкретные вопросы, просто задавайте вопросы по Stackru.
Правка для ответа на вопрос Нупкса:
Я думаю, вам нужно ползти, прежде чем идти и перед тем, как бежать. Сначала правильно разработайте структуру фреймворка и соединений, а затем подумайте о масштабируемости. Если ваша цель - научиться программировать на C# и tcp/ip, то не усложняйте это себе.
Продолжайте свои первоначальные мысли и держите потоки данных отдельно.
удачи и удачи
Я бы посоветовал против нескольких соединений для разной информации. Я бы разработал некоторый протокол, который содержит заголовок с информацией для обработки. Используйте как можно меньше ресурсов. Кроме того, вы можете не захотеть отправлять обновления для различных позиций и статистики с клиента на сервер. В противном случае вы можете оказаться в ситуации, когда пользователь может изменить свои данные, отправляемые обратно на сервер.
Скажем, пользователь фальсифицирует местоположение монстра, чтобы пройти что-то. Я бы только обновил вектор позиции пользователя и действия. Пусть остальная информация будет обработана и проверена сервером.
Да, я думаю, что вы правы насчет единственного соединения, и, конечно же, клиент не будет отправлять какие-либо фактические данные на сервер, больше похоже на простые команды, такие как "двигаться вперед", "повернуть налево" и т. Д., И сервер будет двигаться персонажа на карте и отправьте новые координаты обратно клиенту.