Servidores de jogos e Couchbase com Node.js - Parte 3

Nesta parte da série, configuraremos um sistema de armazenamento de dados do jogo para permitir que você armazene o estado do jogo do jogador ao longo de sua experiência com o jogo. Para isso, criaremos alguns pontos de extremidade /state e /states que representarão blocos individuais de dados de estado. Permitiremos vários blocos de estado nomeados para permitir que o jogo divida os dados de estado em blocos atualizáveis separadamente para evitar a necessidade de gravar muitos blocos de estado quando apenas uma parte tiver sido alterada.

Se você ainda não leu Parte 1 e Parte 2 desta série, sugiro que você o faça, pois esta parte e as futuras se baseiam nelas!

Ajuda rápida - Renovação da sessão

Algo que deveria estar na minha postagem anterior do blog, mas que é importante, é a renovação da sessão dos usuários sempre que eles a acessam. Sem isso, é garantido que a sessão expire após 60 minutos, independentemente de o jogador ainda estar jogando. Obviamente, essa não é a nossa intenção, portanto, vamos corrigir isso!

Primeiro, precisamos adicionar uma nova função ao nosso SessionModel, portanto, abra o arquivo sessionmodel.js e vamos adicionar o seguinte bloco. É uma função bastante simples; ela recebe um ID de sessão e executa uma operação de toque para redefinir o tempo de expiração para 3600 novamente (a partir do momento da execução do toque, não quando a chave foi originalmente inserida).

SessionModel.toque = função(lado, retorno de chamada) {
var sessDocName = 'sess-' + lado;

db.toque(sessDocName, {expiração: 3600}, função(erro, resultado) {
retorno de chamada(erro);
});
};

Agora que temos nossa função de modelo para atualizar a sessão, vamos encontrar um bom lugar para chamá-la. Nosso método authUser parece ser uma boa opção, pois é executado em qualquer endpoint que exija que o usuário seja autenticado. Vamos fazer isso agora. Aqui está nossa nova função authUser com nossa chamada de toque adicionada.

função authUser(req, res, próxima) {
req.uid = nulo;
se (req.cabeçalhos.autorização) {
var authInfo = req.cabeçalhos.autorização.dividir(‘ ‘);
se (authInfo[0] === "Portador) {
var lado = authInfo[1];
sessionModel.obter(lado, função(erro, uid) {
se (erro) {
próxima('Seu ID de sessão é inválido');
} mais {
sessionModel.toque(lado, função(){});
req.uid = uid;
próxima();
}
});
} mais {
próxima('Deve ser autorizado a acessar esse endpoint');
}
} mais {
próxima('Deve ser autorizado a acessar esse endpoint');
}
}

Como parte dessa correção de expiração de sessão, talvez seja necessário extrair a versão do couchnode diretamente do GitHub devido a um erro em nossa implementação de toque que foi corrigido antes da publicação deste blog, mas após o lançamento do ciclo mais recente.

Estados do jogo - O modelo

Agora que já resolvemos o pequeno problema da parte anterior, vamos implementar o salvamento do estado do jogo! Como eu disse acima, vamos permitir que os jogos armazenem dados em vários blocos de estado para reduzir o tráfego de rede. Do ponto de vista do armazenamento, armazenaremos todos esses blocos de estado em um único documento do Couchbase, e esse documento será criado de forma preguiçosa na primeira solicitação para salvar informações para o usuário. Até o momento em que houver algo salvo, emularemos uma lista de estados vazia para o usuário, como você verá em breve.

Para começar, vamos configurar nosso layout de arquivo de modelo padrão em model/statemodel.js. Importamos nossos módulos necessários e configuramos um modelo sem métodos chamado StateModel.

var db = exigir('./../database').mainBucket;
var couchbase = exigir('couchbase');

função Modelo de Estado() {
}

módulo.exportações = Modelo de Estado;

Agora que temos os fundamentos do nosso modelo, vamos começar a implementar alguns dos métodos que serão necessários. Vamos começar com um modelo que nos permita salvar um novo bloco de estado. Essa função tratará tanto da criação quanto da atualização de um bloco de estado. Isso torna a lógica do lado do cliente muito mais simples, pois não precisamos nos preocupar se o bloco de estado já existe em um nível de API. Usaremos uma forma de bloqueio otimista em que um número de versão será armazenado com cada bloco de estado. Sempre que um bloco de estado for atualizado, você precisará passar o número da versão existente no servidor antes que o servidor aceite os novos dados. Isso serve para evitar que várias cópias do jogo em execução simultânea atropelem os dados umas das outras. Esse também é o primeiro lugar em que usaremos a função bloqueio otimista para garantir que não estejamos fazendo alterações simultâneas em nosso objeto de estados a partir de duas chamadas de endpoint.

Vamos começar com nosso protótipo de função de salvamento.

StateModel.salvar = função(uid, nome, preVer, dados, retorno de chamada) {
};

Nossa primeira etapa real é criar um nome para o nosso documento de armazenamento de estado e, em seguida, solicitar esse documento ao Couchbase para verificar se ele já existe.

var stateDocName = 'usuário-' + uid + '-estado';
db.obter(stateDocName, função(erro, resultado) {
// O código abaixo vai para cá!
});

Agora, verificamos se houve algum erro ao solicitar um documento de estado existente. Se encontrarmos um erro, verificaremos se não foi um erro "não encontrado". Se o documento não foi encontrado, ignoramos esse erro e continuamos, pois isso se deve à natureza de criação preguiçosa do nosso documento de estados.

se (erro) {
se (err.código !== couchbase.erros.keyNotFound) {
retorno retorno de chamada(erro);
}
}

Em seguida, movemos nosso documento de estado existente (ou um novo documento, caso não tenha sido encontrado) para uma variável separada para facilitar o acesso e para que possamos lidar com os documentos existentes e com o novo documento da mesma maneira.

var documento de estado = {
tipo: "estado,
uid: uid,
estados: {}
};
se (resultado.valor) {
documento de estado = resultado.valor;
}

Agora, faremos a mesma coisa com nosso bloco de estado. Você notará que garantimos que nossa variável stateBlock faça referência à matriz de estados dos documentos de estado reais. Outro ponto que vale a pena mencionar é que nossa versão padrão do bloco de estado é 0. Isso significa que, ao executar um salvamento pela primeira vez, espera-se que o cliente especifique a versão 0 para esclarecer que está ciente de que esse será um novo bloco de estado.

var stateBlock = {
versão: 0,
dados: nulo
};
se (stateDoc.estados[nome]) {
stateBlock = stateDoc.estados[nome];
} mais {
stateDoc.estados[nome] = stateBlock;
}

Em seguida, precisamos verificar se a versão especificada pelo chamador ainda corresponde à que está armazenada em nosso cluster. Se esse não for o caso, outro usuário deve ter feito alterações desde a última vez em que o cliente recuperou os dados salvos. Espera-se que, se houver uma incompatibilidade de versão, o cliente recupere os novos dados, faça as mesclagens necessárias e tente atualizar novamente.

Se (stateBlock.version !== preVer) {
return callback('Sua versão não corresponde à versão do servidor.');
} else {
stateBlock.version++;
stateBlock.data = data;
}

Como mencionei no início desta seção, também usaremos o bloqueio otimista incorporado ao Couchbase para garantir que nossas gravações de documentos de estado sejam executadas em ordem. Devido ao fato de que realizamos nosso get anteriormente, depois fazemos nossa comparação de versões e, finalmente, fazemos a gravação novamente aqui, há uma chance de que outra chamada ao nosso endpoint de salvamento de estado tenha alterado o objeto desde o nosso get original, mas antes do nosso set. Para saber mais sobre os valores cas, consulte a seção Manual do Couchbase sobre valores cas.

var setOptions = {};
se (resultado.valor) {
setOptions.cas = resultado.cas;
}

Por último, para esse método específico, pré-formamos nosso conjunto, todos os erros que ocorrem são propagados para o chamador (isso provavelmente deve ser envolvido no nível do modelo, conforme mencionado anteriormente) e o retorno de chamada é invocado com o bloco de estado que armazenamos.

db.definir(stateDocName, documento de estado, setOptions, função(erro, resultado) {
se (erro) {
retorno retorno de chamada(erro);
}

retorno de chamada(nulo, stateBlock);
});

Por fim, aqui está todo o nosso método de salvamento. Ele é bastante longo, mas espero que seja relativamente compreensível!

StateModel.salvar = função(uid, nome, preVer, dados, retorno de chamada) {
var stateDocName = 'usuário-' + uid + '-estado';
db.obter(stateDocName, função(erro, resultado) {
se (erro) {
se (err.código !== couchbase.erros.keyNotFound) {
retorno retorno de chamada(erro);
}
}

var documento de estado = {
tipo: "estado,
uid: uid,
estados: {}
};
se (resultado.valor) {
documento de estado = resultado.valor;
}

var stateBlock = {
versão: 0,
dados: nulo
};
se (stateDoc.estados[nome]) {
stateBlock = stateDoc.estados[nome];
} mais {
stateDoc.estados[nome] = stateBlock;
}

se (stateBlock.versão !== preVer) {
retorno retorno de chamada('Sua versão não corresponde à versão do servidor'.);
} mais {
stateBlock.versão++;
stateBlock.dados = dados;
}

var setOptions = {};
se (resultado.valor) {
setOptions.cas = resultado.cas;
}

db.definir(stateDocName, documento de estado, setOptions, função(erro, resultado) {
se (erro) {
retorno retorno de chamada(erro);
}

retorno de chamada(nulo, stateBlock);
});
});
};

O próximo método que incluiremos é o findByUserId. Esse método nos permitirá criar um ponto de extremidade que retorne todos os blocos de estado para qualquer usuário específico. Trata-se principalmente de uma otimização no lado do cliente para permitir a busca de todos os blocos de estado de uma só vez, em vez de executar várias solicitações. A função é extremamente simples. Usando o mesmo nome de documento da nossa função de salvamento, tentamos carregar o documento de estado do nosso cluster; se ele existir, retornamos a lista de estados dentro desse bloco; se o documento estiver ausente, retornamos uma lista vazia para o usuário. Quaisquer outros erros são encaminhados para o chamador.

StateModel.findByUserId = função(uid, retorno de chamada) {
var stateDocName = 'usuário-' + uid + '-estado';
db.obter(stateDocName, função(erro, resultado) {
se (erro) {
se (err.código === couchbase.erros.keyNotFound) {
retorno retorno de chamada(nulo, {});
} mais {
retorno retorno de chamada(erro);
}
}
var documento de estado = resultado.valor;

retorno de chamada(nulo, stateDoc.estados);
});
};

A última função de modelo que precisamos criar é o nosso método para acessar um único bloco de estado para um usuário. Essa função é quase idêntica à nossa função findByUserId, exceto pelo fato de que, além disso, detalhamos um bloco de estado específico por nome em vez de retornar a lista inteira.

StateModel.obter = função(uid, nome, retorno de chamada) {
var stateDocName = 'usuário-' + uid + '-estado';

db.obter(stateDocName, função(erro, resultado) {
se (erro) {
retorno retorno de chamada(erro);
}
var documento de estado = resultado.valor;

se (!stateDoc.estados[nome]) {
retorno retorno de chamada('Não existe nenhum bloco de estado com esse nome'.);
}

retorno de chamada(nulo, stateDoc.estados[nome]);
});
};

Estados do jogo - Tratamento de solicitações

Agora que temos nosso modelo pronto para ser usado, vamos começar a criar os manipuladores de solicitação para nossos três pontos de extremidade! Vamos criar um ponto de extremidade para solicitar todos os estados de um usuário, um ponto de extremidade para solicitar um bloco de estado específico e, finalmente, um ponto de extremidade para atualizar um bloco de estado específico.

Antes de criarmos nossos manipuladores de solicitações, precisamos primeiro adicionar uma referência ao arquivo statemodel.js que criamos anteriormente!

var modelo de estado = exigir('./models/statemodel');

Agora, vamos começar com nosso endpoint de salvamento. Como nas partes anteriores, nossos manipuladores de solicitação são extremamente simples e simplesmente encaminham as partes pertinentes da nossa solicitação para o modelo. Esperamos o nome do bloco de estado como parte do URI, o número da versão como parte da nossa consulta e, finalmente, os dados reais do bloco de estado no corpo da solicitação.

aplicativo.colocar('/estado/:nome', authUser, função(req, res, próxima) {
stateModel.salvar(req.uid, req.parâmetros.nome, parseInt(req.consulta.preVer, 10),
req.corpo, função(erro, estado) {
se (erro) {
retorno próxima(erro);
}

res.enviar(estado);
});
});

Em seguida, precisamos da capacidade de recuperar um bloco de estado que armazenamos anteriormente.

aplicativo.obter('/estado/:nome', authUser, função(req, res, próxima) {
stateModel.obter(req.uid, req.parâmetros.nome, função(erro, estado) {
se (erro) {
retorno próxima(erro);
}

res.enviar(estado);
});
});

Por último, mas não menos importante, o ponto de extremidade para solicitar todos os blocos de estado armazenados para um determinado usuário. Como eu disse antes, isso serve principalmente para otimizar a sequência de carregamento do jogo, em que geralmente precisamos recuperar todos os blocos de estado.

aplicativo.obter('/estados', authUser, função(req, res, próxima) {
stateModel.findByUserId(req.uid, função(erro, estados) {
se (erro) {
retorno próxima(erro);
}

res.enviar(estados);
});
});

Terminou!

Agora que criamos nosso modelo e implementamos os manipuladores de solicitação necessários, você poderá iniciar o aplicativo como nas partes anteriores e executar algumas solicitações no servidor do jogo para ver o resultado do nosso trabalho árduo!

> POST /state/test?preVer=0
Cabeçalho (Autorização): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7
{
"name": "We Rock!",
"Nível": "13"
}
< 200 OK
{
"versão": 1,
"data": {
"name": "We Rock!",
"Nível": "13"
}
}

> GET /estado/teste
Cabeçalho (Autorização): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7
< 200 OK
{
"versão": 1,
"data": {
"name": "We Rock!",
"Nível": "13"
}
}

> GET /states
Cabeçalho (Autorização): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7
< 200 OK
{
"teste": {
"versão": 1,
"data": {
"name": "We Rock!",
"Nível": "13"
}
}
}

Sucesso!

A fonte completa desse aplicativo está disponível aqui: https://github.com/brett19/node-gameapi

Aproveite! Brett

Compartilhe este artigo
Receba atualizações do blog do Couchbase em sua caixa de entrada
Esse campo é obrigatório.

Autor

Postado por Brett Lawson, engenheiro de software principal, Couchbase

Brett Lawson é engenheiro de software principal da Couchbase. Brett é responsável pelo projeto e desenvolvimento dos clientes Node.js e PHP do Couchbase, além de desempenhar um papel no projeto e desenvolvimento da biblioteca C, libcouchbase.

6 Comentários

  1. Escreveu um cliente de demonstração em Javascript que solicitará a autenticação e salvará a posição do cursor usando um ouvinte onMouseStop personalizado a cada 15 ms: https://gist.github.com/rdev5/

  2. Há algum motivo especial para você fazer a sessão manter-se ativa com a função de toque em vez de obter e tocar?

    no modelo Session, no get, você poderia fazer:

    db.get(sessDocName, {expiry: 3600}, function(err, result) {

    não?

    1. Olá, José,
      Essa é, de fato, uma otimização que você pode fazer. Infelizmente, eu não havia percebido o toque na primeira parte das sessões de implementação e, portanto, apenas o adicionei a ela, como um toque separado, para simplificar.
      Abraços, Brett

      1. Muito bem. Obrigado, é um pequeno tutorial muito legal. Aprendi muito.

  3. [...] Postagem do blog da semana: Servidores de jogos e Couchbase com Node.js - Parte 3 [...]

Deixe um comentário

Pronto para começar a usar o Couchbase Capella?

Iniciar a construção

Confira nosso portal do desenvolvedor para explorar o NoSQL, procurar recursos e começar a usar os tutoriais.

Use o Capella gratuitamente

Comece a trabalhar com o Couchbase em apenas alguns cliques. O Capella DBaaS é a maneira mais fácil e rápida de começar.

Entre em contato

Deseja saber mais sobre as ofertas do Couchbase? Deixe-nos ajudar.