Многоклиентские, асинхронные сокеты в С#, лучшие практики?

Я пытаюсь лучше понять сокеты tcp/ip в С#, так как хочу бросить вызов себе, чтобы увидеть, могу ли я создать рабочую инфраструктуру MMO (игровой мир, карту, игроков и т.д.) исключительно для образовательных целей, я не собираюсь быть еще одним из тех, "OMGZ собирается сделать мою MMORPG r0x0r, которая будет лучше, чем WoW!!!", вы знаете, о чем они говорят.

Во всяком случае, мне было интересно, может ли кто-нибудь пролить свет на то, как можно подойти к разработке такого типа системы и какие вещи необходимы, и о чем я должен следить?

Моя первоначальная идея заключалась в том, чтобы разбить систему на отдельные клиент-серверные соединения с каждым соединением (на своем собственном порту), выполняющим определенную задачу, например, обновление позиций игрока/монстра, отправку и получение сообщений чата и т.д., которые я бы облегчил обработку данных, потому что вам не всегда нужно было размещать заголовок данных, чтобы знать, какую информацию содержит пакет.

Это имеет смысл и полезно, или я просто усложняю вещи?

ваши отзывы очень ценятся.

Ответ 1

Если вы выполняете программирование на уровне сокетов, то независимо от того, сколько портов вы открываете для каждого типа сообщения, вам все равно нужно иметь какой-то заголовок. Даже если это всего лишь длина остальной части сообщения. Сказав, что легко добавить простую структуру заголовка и хвоста в сообщение. Я бы подумал, что проще иметь дело только с одним портом на стороне клиента.

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

Кроме того, большинство MMORPGS также шифруют все свои данные. Если вы пишете это как упражнение для удовольствия, тогда это не будет иметь большого значения.

Для разработки/написания протоколов в целом вот о чем я беспокоюсь:

порядок байтов

Всегда ли клиент и сервер гарантированно имеют одинаковую контенту. Если нет, мне нужно обработать это в моем коде сериализации. Существует несколько способов обработки endianess.

  • Игнорировать его - Очевидно, что плохой выбор
  • Укажите соответствие протокола. Это то, что старые протоколы делали/делали, следовательно, термин "порядок сети", который всегда был большим аргументом. На самом деле не имеет значения, какую конкретизацию вы укажете только, что вы указываете тот или иной. Некоторые хрустящие старые сетевые программисты встанут на руки, если вы не пользуетесь большой энтузиазмом, но если ваши серверы и большинство клиентов мало ориентированы, вы действительно не покупаете себе ничего, кроме дополнительной работы, сделав протокол большим эндиантом.
  • Отметьте Endianess в каждом заголовке. Вы можете добавить куки файл, который будет сообщать вам конечную цель клиента/сервера и дать каждому сообщению преобразование соответствующим образом по мере необходимости. Дополнительная работа!
  • Сделайте свой протокол неактивным - если вы отправляете все как строки ASCII, то endianess не имеет значения, но также намного более неэффективно.

Из 4 я обычно выбирал бы 2 и указывал бы, что endianess будет таковой у большинства клиентов, которые теперь дни будут немногочисленными.

Обратная и обратная совместимость

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

Для общего управления версиями я обычно проектирую что-то простое. Клиент отправляет сообщение с версией, указывающее, что он говорит версию X.Y, если сервер может поддерживать эту версию, он отправляет обратно сообщение, подтверждающее версию клиента, и все идет вперед. В противном случае он удаляет клиента и завершает соединение.

Для каждого сообщения у вас есть что-то вроде следующего:

+-------------------------+-------------------+-----------------+------------------------+
| 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 и т.д.). Используйте фреймворк.
  • Ни одна из сторон не имеет определенного протокола. Попробуйте определить оптимальное решение, основанное на всех факторах (все те, которые я упомянул здесь), а также уровень их компетенции (по крайней мере, насколько это возможно). Независимо убедитесь, что обе стороны согласны и понимают протокол. Из опыта это может быть болезненным или приятным. Это зависит от того, с кем вы работаете.

В любом случае вы не будете работать с третьей стороной, так что это действительно просто добавлено для полноты.

Мне кажется, что я мог бы написать гораздо больше об этом, но он довольно длинный. Я надеюсь, что это поможет, и если у вас есть какие-то конкретные вопросы, просто спросите об Stackoverflow.

Редактирование ответа на вопрос knoopx:

Ответ 2

Я думаю, вам нужно ползти перед тем, как идти и прежде чем бежать. Сначала создайте структуру фреймворков и подключений, а затем о масштабируемости. Если ваша цель - научиться программированию на С# и tcp/ip, то не делайте этого сложнее на себе.

Продолжайте свои первоначальные мысли и продолжайте разделять потоки данных.

получайте удовольствие и удачи

Ответ 3

Я бы посоветовал не использовать несколько соединений для различной информации. Я бы разработал некоторый протокол, который содержит заголовок с информацией, подлежащей обработке. Используйте как можно меньше ресурсов. Кроме того, вы можете не захотеть отправлять обновления для разных позиций и статистики с клиента на сервер. В противном случае вы можете оказаться в ситуации, когда пользователь может изменить свои данные, отправляемые обратно на сервер.

Скажите, что пользователь фальсифицирует местоположение монстра, чтобы что-то пройти. Я бы только обновил вектор положения пользователя и действия. Пусть остальная часть информации обрабатывается и проверяется сервером.

Ответ 4

Да, я думаю, что вы правы в отношении единственного соединения, и, конечно, клиент не будет отправлять какие-либо фактические данные на сервер, больше как простые команды, такие как "двигаться вперед", "повернуть налево" и т.д., и сервер переместит символ на карту и отправит новые координаты обратно клиенту.