Estou trabalhando em um servidor de jogos genérico que gerencia jogos para um número arbitrário de clientes em rede de soquete TCP jogando um jogo. Eu tenho um 'design' hackeado com fita adesiva que está funcionando, mas parece frágil e inflexível. Existe um padrão bem estabelecido de como escrever a comunicação cliente / servidor robusta e flexível? (Se não, como você melhoraria o que tenho abaixo?)
Aproximadamente eu tenho isso:
- Durante a configuração de um jogo, o servidor possui um encadeamento para cada soquete do jogador que lida com solicitações síncronas de um cliente e respostas do servidor.
- No entanto, quando o jogo termina, todos os threads, exceto um sono, passam por todos os jogadores, um de cada vez, comunicando sobre sua jogada (em resposta-pedido invertida).
Aqui está um diagrama do que eu tenho atualmente; clique para uma versão maior / legível ou PDF de 66kB .
Problemas:
- Exige que os jogadores respondam exatamente por sua vez, com a mensagem certa. (Suponho que posso permitir que cada jogador responda com porcaria aleatória e só siga em frente depois que me derem uma jogada válida.)
- Não permite que os jogadores conversem com o servidor, a menos que seja a vez deles. (O servidor poderia enviar atualizações sobre outros players, mas não processar uma solicitação assíncrona.)
Requisitos finais:
O desempenho não é fundamental. Isso será usado principalmente para jogos que não são em tempo real e, principalmente, para colocar AIs uns contra os outros, e não contra humanos.
O jogo sempre será baseado em turnos (mesmo que em uma resolução muito alta). Cada jogador sempre processa uma jogada antes de todos os outros jogadores terem uma vez.
A implementação do servidor está em Ruby, se isso faz diferença.
Respostas:
Não tenho certeza do que é exatamente o que você deseja alcançar. Porém, há um padrão usado constantemente nos servidores de jogos e que pode ajudá-lo. Use filas de mensagens.
Para ser mais específico: quando os clientes enviam mensagens para o servidor, não as processe imediatamente. Em vez disso, analise-os e coloque-os em uma fila para esse cliente específico. Então, em algum loop principal (talvez até em outro thread), revise todos os clientes, recupere as mensagens da fila e processe-as. Quando o processamento indicar que a vez deste cliente está concluída, passe para a próxima.
Dessa forma, não é necessário que os clientes trabalhem estritamente turn-by-turn; apenas rápido o suficiente para que você tenha algo na fila no momento em que o cliente é processado (você pode, é claro, esperar pelo cliente ou pular sua vez, se houver). E você pode adicionar suporte para solicitações assíncronas adicionando uma fila "assíncrona": quando um cliente envia uma solicitação especial, ela é adicionada a essa fila especial; essa fila é verificada e processada com mais frequência do que a dos clientes.
fonte
Os threads de hardware não são suficientemente escaláveis para tornar "um por jogador" uma idéia razoável para um número de três dígitos, e a lógica de saber quando despertá-los é uma complexidade que aumentará. Uma idéia melhor é encontrar um pacote de E / S assíncrono para Ruby que permita enviar e receber dados sem precisar pausar um segmento de programa inteiro enquanto a operação de leitura ou gravação ocorre. Isso também resolve o problema de esperar que os jogadores respondam, pois você não terá nenhum tópico pendurado em uma operação de leitura. Em vez disso, seu servidor pode apenas verificar se um prazo expirou e notificar o outro jogador de acordo.
Basicamente, 'E / S assíncrona' é o 'padrão' que você está procurando, embora não seja realmente um padrão, mas uma abordagem. Em vez de chamar explicitamente 'ler' em um soquete e pausar o programa até que os dados cheguem, você configura o sistema para chamar seu manipulador 'onRead' sempre que ele tiver dados prontos e continua o processamento até esse momento.
Cada jogador tem uma vez e cada jogador tem um soquete que envia dados, que é um pouco diferente. Um dia você pode não querer um soquete por jogador. Você pode não usar soquetes. Mantenha as áreas de responsabilidade separadas. Desculpe se isso soa como um detalhe sem importância, mas quando você confunde conceitos em seu design que devem ser diferentes, fica mais difícil encontrar e discutir melhores abordagens.
fonte
Certamente há mais de uma maneira de fazer isso, mas pessoalmente, eu pularia completamente os threads separados e usaria apenas um loop de eventos. A maneira como você faz isso dependerá um pouco da biblioteca de E / S que você está usando, mas basicamente o loop principal do servidor será parecido com este:
Por exemplo, digamos que você tenha n clientes envolvidos em um jogo. Quando então o primeiro n-1 envia suas jogadas, basta verificar se a jogada parece válida e enviar uma mensagem dizendo que a jogada foi recebida, mas você ainda está esperando outros jogadores se moverem. Depois que todos os n jogadores se moverem, você processa todos os movimentos que você salvou e envia os resultados para todos os jogadores.
Você também pode refinar esse esquema para incluir tempos limite - a maioria das bibliotecas de E / S deve ter algum mecanismo para aguardar a chegada de novos dados ou o tempo decorrido.
Obviamente, você também pode implementar algo assim com threads individuais para cada conexão, fazendo com que esses threads passem todas as solicitações que eles não podem manipular diretamente para um thread central (um por jogo ou um por servidor) que executa um loop como mostrado acima, exceto que ele fala com os threads do manipulador de conexão, e não diretamente com os clientes. Se você acha isso mais simples ou mais complicado do que a abordagem de thread único, cabe a você.
fonte