Я работаю над общим игровым сервером, который управляет играми для произвольного числа клиентов, подключенных к TCP-сокету. У меня есть «дизайн», взломанный вместе с клейкой лентой, который работает, но кажется хрупким и негибким. Существует ли устоявшаяся модель для написания надежных и гибких коммуникаций между клиентом и сервером? (Если нет, как бы вы улучшили то, что у меня ниже?)
Примерно у меня есть это:
- При настройке игры сервер имеет один поток для каждого сокета игрока, обрабатывающий синхронные запросы от клиента и ответы от сервера.
- Однако, как только игра начнется, все потоки, за исключением одного сна, будут проходить циклически по всем игрокам, сообщая об их движении (в обратном запросе-ответе).
Вот схема того, что я имею в настоящее время; щелкните для увеличения / читаемой версии, или PDF на 66 КБ .
Проблемы:
- Это требует, чтобы игроки отвечали точно по очереди с точно правильным сообщением. (Полагаю, я мог позволить каждому игроку отвечать случайным дерьмом и двигаться дальше только после того, как он даст мне правильный ход.)
- Это не позволяет игрокам общаться с сервером, если только не их очередь. (Я мог бы заставить сервер отправлять им обновления о других игроках, но не обрабатывать асинхронный запрос.)
Окончательные требования:
Производительность не имеет первостепенного значения. Это будет в основном использоваться для игр не в реальном времени, и в основном для противостояния ИИ друг против друга, а не раздражительных людей.
Игра всегда будет пошаговой (даже в очень высоком разрешении). Каждый игрок всегда получает один ход, прежде чем все остальные игроки получают ход.
Реализация сервера происходит в Ruby, если это имеет значение.
Ответы:
Я не уверен, чего именно ты хочешь достичь. Но есть один шаблон, который постоянно используется на игровых серверах и может вам помочь. Используйте очереди сообщений.
Чтобы быть более конкретным: когда клиенты отправляют сообщения на сервер, не обрабатывайте их немедленно. Скорее разберите их и поставьте в очередь для этого конкретного клиента. Затем в каком-то главном цикле (возможно, даже в другом потоке) по очереди перебирайте всех клиентов, извлекайте сообщения из очереди и обрабатывайте их. Когда обработка указывает, что ход этого клиента закончен, переходите к следующему.
Таким образом, клиенты не обязаны работать строго по очереди; только достаточно быстро, чтобы у вас было что-то в очереди к моменту обработки клиента (вы, конечно, можете либо дождаться клиента, либо пропустить его очередь, если он отстает). И вы можете добавить поддержку асинхронных запросов, добавив «асинхронную» очередь: когда клиент отправляет специальный запрос, он добавляется в эту специальную очередь; эта очередь проверяется и обрабатывается чаще, чем клиентская.
источник
Аппаратные потоки недостаточно хорошо масштабируются, чтобы сделать «по одному на игрока» разумной идеей для трехзначного числа игроков, и логика знания, когда их разбудить - сложность, которая будет расти. Лучшая идея - найти пакет асинхронного ввода-вывода для Ruby, который позволит вам отправлять и получать данные без необходимости приостанавливать весь программный поток во время операции чтения или записи. Это также решает проблему ожидания ответа игроков, поскольку у вас не будет никаких потоков, висящих на операции чтения. Вместо этого ваш сервер может просто проверить, истек ли срок, а затем уведомить об этом другого игрока.
По сути, «асинхронный ввод-вывод» - это тот «шаблон», который вы ищете, хотя на самом деле это не шаблон, а скорее подход. Вместо явного вызова read для сокета и приостановки программы до получения данных, вы настраиваете систему на вызов вашего обработчика onRead всякий раз, когда он готов к данным, и продолжаете обработку до этого времени.
У каждого игрока есть свой ход, и у каждого игрока есть гнездо для отправки данных, которое немного отличается. Однажды вам может не понадобиться одна розетка на игрока. Вы можете не использовать сокеты вообще. Держите зоны ответственности отдельно. Извините, если это звучит как неважная деталь, но когда вы объединяете концепции в своем дизайне, которые должны отличаться, становится труднее находить и обсуждать лучшие подходы.
источник
Конечно, есть несколько способов сделать это, но лично я бы полностью пропустил отдельные потоки и просто использовал цикл обработки событий. То, как вы это сделаете, будет в некоторой степени зависеть от используемой вами библиотеки ввода-вывода, но в основном ваш основной цикл сервера будет выглядеть так:
Например, скажем, у вас есть n клиентов, вовлеченных в игру. Когда затем первые n-1 из них отправляют свои ходы, вы просто проверяете, что ход выглядит верным, и отправляете обратно сообщение о том, что ход получен, но вы все еще ждете, чтобы другие игроки перешли. После того как все n игроков переместились, вы обрабатываете все накопленные ходы и отправляете результаты всем игрокам.
Вы также можете уточнить эту схему, включив тайм-ауты - большинство библиотек ввода / вывода должны иметь некоторый механизм для ожидания поступления новых данных или истечения заданного времени.
Конечно, вы могли бы также реализовать нечто подобное с отдельными потоками для каждого соединения, если бы эти потоки передавали любые запросы, которые они не могут обработать напрямую, в центральный поток (один на игру или один на сервер), который выполняет цикл, подобный показано выше, за исключением того, что он взаимодействует с потоками обработчика соединений, а не напрямую с клиентами. Считаете ли вы, что это проще или сложнее, чем однопотоковый подход, действительно зависит от вас.
источник