Existe um padrão para escrever um servidor baseado em turnos que se comunica com n clientes por soquetes?

14

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 .

Diagrama de sequência do fluxo

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.

Phrogz
fonte
Se você usa um soquete TCP por cliente, esse limite não é para (65535-1024) clientes?
o0 '.
1
Por que isso é um problema, Lohoris? A maioria das pessoas vai lutar para obter 6.000 usuários simultâneos, não importa 60000
Kylotan
@Kylotan De fato. Se eu tiver mais de 10 IAs concorrentes, ficarei surpreso. :)
Phrogz 26/10/11
Por curiosidade, que ferramenta você usou para criar esse diagrama?
21814
@matthias Infelizmente, isso foi muito trabalho personalizado no Adobe Illustrator.
21413 Phrogz

Respostas:

10

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.

deixa pra lá
fonte
1

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 tomada tem uma vez

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.

Kylotan
fonte
1

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:

  1. Configure um pool de conexões e um soquete de escuta para novas conexões.
  2. Aguarde que algo aconteça.
  3. Se o algo for uma nova conexão, adicione-o ao pool.
  4. Se algo é uma solicitação de um cliente, verifique se é algo com o qual você possa lidar imediatamente. Se sim, faça; caso contrário, coloque-o em uma fila e (opcionalmente) envie uma confirmação ao cliente.
  5. Verifique também se há algo na fila com o qual você possa lidar agora; Se sim, faça.
  6. Volte ao passo 2.

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ê.

Ilmari Karonen
fonte