Существует ли шаблон для записи пошагового сервера, взаимодействующего с n клиентами через сокеты?

14

Я работаю над общим игровым сервером, который управляет играми для произвольного числа клиентов, подключенных к TCP-сокету. У меня есть «дизайн», взломанный вместе с клейкой лентой, который работает, но кажется хрупким и негибким. Существует ли устоявшаяся модель для написания надежных и гибких коммуникаций между клиентом и сервером? (Если нет, как бы вы улучшили то, что у меня ниже?)

Примерно у меня есть это:

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

Вот схема того, что я имею в настоящее время; щелкните для увеличения / читаемой версии, или PDF на 66 КБ .

Диаграмма последовательности потока

Проблемы:

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

Окончательные требования:

  • Производительность не имеет первостепенного значения. Это будет в основном использоваться для игр не в реальном времени, и в основном для противостояния ИИ друг против друга, а не раздражительных людей.

  • Игра всегда будет пошаговой (даже в очень высоком разрешении). Каждый игрок всегда получает один ход, прежде чем все остальные игроки получают ход.

Реализация сервера происходит в Ruby, если это имеет значение.

Phrogz
источник
Если вы используете TCP-сокет для каждого клиента, не будет ли это ограничение для (65535-1024) клиентов?
о0 '.
1
Почему это проблема, Лохорис? Большинство людей будут изо всех сил
пытаться
@ Kylotan Действительно. Если у меня будет более 10 одновременных ИИ, я буду удивлен. :)
Phrogz
Из любопытства, какой инструмент вы использовали для создания этой диаграммы?
Матиас
@matthias К сожалению, это была слишком большая работа в Adobe Illustrator.
Phrogz

Ответы:

10

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

Чтобы быть более конкретным: когда клиенты отправляют сообщения на сервер, не обрабатывайте их немедленно. Скорее разберите их и поставьте в очередь для этого конкретного клиента. Затем в каком-то главном цикле (возможно, даже в другом потоке) по очереди перебирайте всех клиентов, извлекайте сообщения из очереди и обрабатывайте их. Когда обработка указывает, что ход этого клиента закончен, переходите к следующему.

Таким образом, клиенты не обязаны работать строго по очереди; только достаточно быстро, чтобы у вас было что-то в очереди к моменту обработки клиента (вы, конечно, можете либо дождаться клиента, либо пропустить его очередь, если он отстает). И вы можете добавить поддержку асинхронных запросов, добавив «асинхронную» очередь: когда клиент отправляет специальный запрос, он добавляется в эту специальную очередь; эта очередь проверяется и обрабатывается чаще, чем клиентская.

Ничего
источник
1

Аппаратные потоки недостаточно хорошо масштабируются, чтобы сделать «по одному на игрока» разумной идеей для трехзначного числа игроков, и логика знания, когда их разбудить - сложность, которая будет расти. Лучшая идея - найти пакет асинхронного ввода-вывода для Ruby, который позволит вам отправлять и получать данные без необходимости приостанавливать весь программный поток во время операции чтения или записи. Это также решает проблему ожидания ответа игроков, поскольку у вас не будет никаких потоков, висящих на операции чтения. Вместо этого ваш сервер может просто проверить, истек ли срок, а затем уведомить об этом другого игрока.

По сути, «асинхронный ввод-вывод» - это тот «шаблон», который вы ищете, хотя на самом деле это не шаблон, а скорее подход. Вместо явного вызова read для сокета и приостановки программы до получения данных, вы настраиваете систему на вызов вашего обработчика onRead всякий раз, когда он готов к данным, и продолжаете обработку до этого времени.

каждая розетка имеет поворот

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

Kylotan
источник
1

Конечно, есть несколько способов сделать это, но лично я бы полностью пропустил отдельные потоки и просто использовал цикл обработки событий. То, как вы это сделаете, будет в некоторой степени зависеть от используемой вами библиотеки ввода-вывода, но в основном ваш основной цикл сервера будет выглядеть так:

  1. Настройте пул соединений и сокет прослушивания для новых соединений.
  2. Подождите, пока что-нибудь случится.
  3. Если что-то является новым соединением, добавьте его в пул.
  4. Если что-то является запросом от клиента, проверьте, что это то, что вы можете обработать немедленно. Если да, сделай так; если нет, поместите его в очередь и (необязательно) отправьте подтверждение клиенту.
  5. Также проверьте, есть ли что-то в очереди, что вы можете обрабатывать сейчас; если так, сделай это.
  6. Вернитесь к шагу 2.

Например, скажем, у вас есть n клиентов, вовлеченных в игру. Когда затем первые n-1 из них отправляют свои ходы, вы просто проверяете, что ход выглядит верным, и отправляете обратно сообщение о том, что ход получен, но вы все еще ждете, чтобы другие игроки перешли. После того как все n игроков переместились, вы обрабатываете все накопленные ходы и отправляете результаты всем игрокам.

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

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

Илмари Каронен
источник