Atualmente, parece que quase todos os estúdios de jogos estão trabalhando em jogos em rede nos quais os jogadores podem interagir e cooperar com seus amigos e outros jogadores do mundo todo. Considerando minha experiência anterior na criação de servidores desse tipo e que o Couchbase se encaixa como um armazenamento de backup para um sistema como esse, pensei que talvez esse fosse um excelente tópico para escrever! Escreverei este artigo em várias partes, cada uma delas implementando um aspecto específico do servidor de jogos. Além disso, farei o mesmo tutorial usando nossa biblioteca de cliente PHP para mostrá-lo também.
Layout do projeto
Para começar, precisamos configurar algumas coisas básicas que nos permitam enviar e receber solicitações HTTP, bem como nos conectar ao nosso cluster do Couchbase. Se ainda não tiver certeza de como fazer isso, dê uma olhada no meu artigo postagem anterior do blog onde explico com um pouco mais de detalhes do que farei a seguir. Iniciaremos nosso projeto com uma estrutura de diretórios típica do Node.js com algumas pastas extras para ajudar a organizar o código do servidor de jogos.
/lib/models/
/lib/app.js
/lib/database.js
/package.json
De acordo com a estrutura normal do Node.js, começamos com a pasta 'lib' para armazenar todos os nossos arquivos de origem, com um lib/server.js para atuar como o arquivo principal e, por fim, nosso package.json para descrever as dependências do projeto e outros metadados. Além disso, adicionamos um database.js que gerenciará centralmente nossa conexão com o banco de dados para evitar que tenhamos de instanciar uma nova conexão para cada solicitação, bem como a pasta /lib/models/ que usaremos para manter o código-fonte de nossos vários modelos de banco de dados.
O básico
Aqui estão alguns conteúdos para seu package.json. Damos um nome ao nosso projeto, apontamos para o arquivo Javascript principal e, em seguida, definimos alguns módulos de pré-requisitos de que precisaremos mais tarde. Depois de salvar esse arquivo, execute npm install no diretório raiz do seu projeto deve instalar as dependências referenciadas.
"principal": "./lib/app",
"licença" : "Apache2",
"name" (nome): "gameapi-couchbase",
"dependencies" (dependências): {
"couchbase": “~1.0.0”,
"expresso": “~3.4.0”,
"uuid": “~1.4.1”
},
"devDependencies": {
},
"versão": “0.0.1”
}
Nossa próxima etapa é configurar o núcleo do nosso servidor de jogos. Isso é colocado em nosso arquivo /lib/app.js. Examinarei as seções desse arquivo bloco por bloco e explicarei o que cada uma delas faz.
Primeiro, precisamos importar os módulos de que precisaremos nesse arquivo. No momento, precisamos apenas do módulo express para roteamento e análise de HTTP, mas, mais adiante neste tutorial, adicionaremos mais coisas a ele.
Em seguida, vamos configurar o Express. Além disso, anexamos o submódulo bodyParser do Express para que possamos analisar os corpos JSON POST e PUT. Isso será útil mais tarde, quando nossos clientes de jogos precisarem nos passar blocos de dados JSON.
aplicativo.uso(expresso.bodyParser());
Apenas para fins de demonstração, vamos adicionar uma rota simples ao nosso servidor HTTP para lidar com solicitações à raiz do nosso servidor.
res.enviar({lacaios: Curvem-se diante de mim, pois eu sou a RAIZ!});
});
Por fim, vamos colocar nosso servidor HTTP escutando na porta 3000.
console.registro('Listening on port 3000' (Escutando na porta 3000));
});
Esta é uma ideia aproximada de como deve ser seu app.js até o momento:
var expresso = exigir("expresso);
var aplicativo = expresso();
aplicativo.uso(expresso.bodyParser());
aplicativo.obter(‘/’, função(req, res, próxima) {
res.enviar({lacaios: Curvem-se diante de mim, pois eu sou a RAIZ!});
});
aplicativo.ouvir(3000, função () {
console.registro('Listening on port 3000' (Escutando na porta 3000));
});
Para a última parte dos fundamentos do nosso projeto, vamos configurar nossa conexão com o banco de dados. O código é bastante simples: importamos o módulo couchbase e, em seguida, exportamos uma nova conexão para o nosso servidor hospedado localmente e o bucket "gameapi" por meio de uma propriedade do módulo chamada mainBucket.
var couchbase = exigir('couchbase');
// Conectar-se ao nosso servidor Couchbase
Nesse ponto, se você abrir um terminal na raiz do projeto e executar node lib/app.jsVocê deverá ver a mensagem "Listening on port 3000" (Escutando na porta 3000). Agora você também pode apontar seu navegador para https://localhost:3000 e veja nosso trabalho em ação até o momento.
É nesse ponto que sugiro que você instale um aplicativo que permita criar solicitações HTTP específicas. Pessoalmente, adoro a extensão POSTman para o Google Chrome. Isso será importante mais tarde, quando você quiser testar endpoints que não sejam simples solicitações GET!
Criação de conta - Modelo de conta
Agora que temos nosso servidor básico em execução, vamos começar a trabalhar na parte "jogo" do nosso servidor de jogos. Começaremos implementando o ponto de extremidade de criação de conta, que será acessado por meio de uma solicitação POST para o endereço /usuários URI. Para iniciar esse processo, vamos primeiro criar um modelo para o nosso manipulador de ponto de extremidade lidar com a abstração de alguns dos detalhes da implementação do nosso banco de dados. Esses modelos são onde ocorrerá a maior parte de nossas interações com o Couchbase Server.
Vamos começar criando um novo arquivo em nossa pasta /lib/modelos chamado "accountmodel.js". Depois de ter o arquivo accountmodel.js pronto e aberto, vamos começar importando alguns dos módulos de que precisaremos.
var couchbase = exigir('couchbase');
var db = exigir('./../database').mainBucket;
Como você pode ver, há quatro módulos de que precisaremos neste momento. Usaremos o módulo uuid para gerar UUIDs para nossos objetos de banco de dados. Vi muitas pessoas usando contadores de sequência implementados usando o sistema incr/decr do Couchbase, mas prefiro muito mais o método UUID que usarei aqui, pois ele evita a necessidade de fazer uma operação adicional no banco de dados. Em seguida, importamos o módulo couchbase, que usaremos para acessar várias constantes de que precisaremos (principalmente erros). E, por fim, importamos o módulo de banco de dados e obtemos a conexão com o bucket gameapi que criamos anteriormente.
Em seguida, definimos uma função auxiliar simples que ajudará a remover quaisquer propriedades em nível de banco de dados de que nosso modelo precise e que não sejam importantes para o restante do servidor. No momento, a propriedade "type" é a única propriedade que será removida. Essa propriedade será usada pela gameapi para identificar que tipo de objeto é um determinado item em nosso balde ao fazer reduções de mapa posteriormente.
excluir obj.tipo;
retorno obj;
}
Agora, definimos nossa classe AccountModel.
}
E exportar a classe para outros arquivos que importam essa classe. Sugiro que você mantenha essa declaração sempre na parte inferior do arquivo para facilitar a localização quando estiver tentando identificar o que um determinado arquivo exporta.
Agora que nosso modelo boilerplate está pronto, podemos criar nossa função create, que nos permitirá criar um objeto de usuário. Dividirei essa função em partes menores para simplificar a explicação.
Vamos começar com a definição da função em si.
};
Em seguida, vamos criar um objeto que será inserido em nosso bucket do Couchbase. Especificamos um tipo para o objeto, que, conforme mencionado acima, será usado posteriormente. Geramos um UID para o usuário, o que nos ajudará a fazer referência ao nosso usuário em todo o processo. Por fim, copiamos os detalhes do usuário que foram passados para a função de criação. Você pode notar que não realizamos nenhuma validação nos dados que estão sendo passados para o nosso modelo. Isso ocorre porque, na maioria das vezes, nosso código de tratamento de solicitações terá uma ideia melhor do que aceitar ou não aceitar, e nosso modelo é responsável apenas por armazenar os dados. Por último, geramos uma chave que será usada para fazer referência a esse documento. Para isso, usamos o tipo de documento e o UID do usuário.
var userDoc = {
tipo: 'usuário',
uid: uuid.v4(),
nome: usuário.nome,
nome de usuário: usuário.nome de usuário,
senha: usuário.senha
};
var userDocName = 'usuário-' + userDoc.uid;
Para que possamos encontrar esse usuário no futuro pelo seu nome de usuário (provavelmente não é uma boa ideia fazer com que seus usuários se lembrem de seus UIDs!), criaremos um "documento referencial", ou seja, um documento com uma chave baseada no nome de usuário que aponta para o documento do nosso usuário (usando seu UID). Isso também tem o benefício adicional de evitar que vários usuários tenham o mesmo nome de usuário.
tipo: 'nome de usuário',
uid: userDoc.uid
};
var refDocName = 'nome de usuário-' + userDoc.nome de usuário;
Por fim, precisamos inserir esses documentos em nosso bucket do Couchbase. Primeiro, inserimos o documento referencial e tratamos o erro keyAlreadyExists especificamente retornando uma mensagem avisando ao usuário que o nome de usuário já foi usado e simplesmente repassando quaisquer outros erros (provavelmente deveríamos envolver nossos erros do Couchbase no nível do modelo, mas isso não é importante neste ponto da série). O fato de inserirmos os documentos referenciais primeiro aqui é importante porque ++TODO++ Por que isso é importante mesmo? -TODO-. Em seguida, inserimos o próprio documento do usuário e, por fim, invocamos o retorno de chamada que nos foi passado. Primeiro, higienizamos o objeto retornado usando a função que criamos anteriormente para garantir que nenhuma das nossas propriedades no nível do banco de dados vaze para outras camadas do nosso aplicativo. Você também pode notar que estamos passando um valor 'cas' por meio do nosso retorno de chamada. Isso será importante mais tarde, quando precisarmos executar bloqueio otimista em nosso objeto Account.
db.adicionar(refDocName, refDoc, função(erro) {
se (erro && err.código === couchbase.erros.keyAlreadyExists) {
retorno retorno de chamada('O nome de usuário especificado já existe');
} mais se (erro) {
retorno retorno de chamada(erro);
}
db.adicionar(userDocName, userDoc, função(erro, resultado) {
se (erro) {
retorno retorno de chamada(erro);
}
retorno de chamada(nulo, cleanUserObj(userDoc), resultado.cas);
});
});
Este é o aspecto que seu arquivo accountmodel.js deve ter até o momento:
var uuid = exigir('uuid');
var couchbase = exigir('couchbase');
var db = exigir('./../database').mainBucket;
função cleanUserObj(obj) {
excluir obj.tipo;
retorno obj;
}
função AccountModel() {
}
AccountModel.criar = função(usuário, retorno de chamada) {
var userDoc = {
tipo: 'usuário',
uid: uuid.v4(),
nome: usuário.nome,
nome de usuário: usuário.nome de usuário,
senha: usuário.senha
};
var userDocName = 'usuário-' + userDoc.uid;
var refDoc = {
tipo: 'nome de usuário',
uid: userDoc.uid
};
var refDocName = 'nome de usuário-' + userDoc.nome de usuário;
db.adicionar(refDocName, refDoc, função(erro) {
se (erro && err.código === couchbase.erros.keyAlreadyExists) {
retorno retorno de chamada('O nome de usuário especificado já existe');
} mais se (erro) {
retorno retorno de chamada(erro);
}
db.adicionar(userDocName, userDoc, função(erro, resultado) {
se (erro) {
retorno retorno de chamada(erro);
}
retorno de chamada(nulo, cleanUserObj(userDoc), resultado.cas);
});
});
};
módulo.exportações = AccountModel;
Criação de conta - Tratamento de solicitações
Agora que concluímos a função create em nosso modelo de conta, podemos escrever uma rota expressa para lidar com solicitações de criação de contas e passar essas solicitações para a nossa função. Primeiro, precisamos definir uma rota.
// Os próximos bits vão para cá!
});
E... execute alguma validação para garantir que os dados necessários foram passados para o endpoint.
retorno res.enviar(400, 'Deve especificar um nome');
}
se (!req.corpo.nome de usuário) {
retorno res.enviar(400, 'Deve especificar um nome de usuário');
}
se (!req.corpo.senha) {
retorno res.enviar(400, 'Deve especificar uma senha');
}
Uma vez que os dados tenham sido *tussa* "validados" */tussa*, podemos gerar o hash SHA1 para a senha do usuário (nunca armazene as senhas de um usuário em texto simples!) e, em seguida, executar a função de criação que criamos anteriormente neste post. Você também pode notar que eu removo a senha do usuário do objeto user antes de passá-la de volta para o cliente. Isso é novamente para segurança, pois queremos limitar a transmissão da senha do usuário (em qualquer formato) o máximo possível.
var novoUsuário = req.corpo;
newUser.senha = cripta.sha1(newUser.senha);
accountModel.criar(req.corpo, função(erro, usuário) {
se (erro) {
retorno próxima(erro);
}
excluir usuário.senha;
res.enviar(usuário);
});
Para resumir, toda a rota de criação de conta deve ter a seguinte aparência:
aplicativo.postagem('/usuários', função(req, res, próxima) {
se (!req.corpo.nome) {
retorno res.enviar(400, 'Deve especificar um nome');
}
se (!req.corpo.nome de usuário) {
retorno res.enviar(400, 'Deve especificar um nome de usuário');
}
se (!req.corpo.senha) {
retorno res.enviar(400, 'Deve especificar uma senha');
}
var novoUsuário = req.corpo;
newUser.senha = cripta.sha1(newUser.senha);
accountModel.criar(novoUsuário, função(erro, usuário) {
se (erro) {
retorno próxima(erro);
}
excluir usuário.senha;
res.enviar(usuário);
});
});
Final
Bem, finalmente chegamos ao final da parte 1 de nosso tutorial. Muitos dos conceitos básicos já foram abordados, portanto, as próximas partes da série deverão ser um pouco mais curtas (mas não prometemos nada!). Neste ponto, você deve ser capaz de executar uma solicitação POST para o ponto de extremidade /users e criar um novo usuário da seguinte forma:
{
"nome": "Brett Lawson",
"nome de usuário": "brett19",
"senha": "success!"
}
< 200 OK
{
"uid": “b836d211-425c-47de-9faf-5d0adc078edc”,
"nome": "Brett Lawson",
"nome de usuário": "brett19"
}
Infelizmente, neste momento não há muito que você possa fazer com suas novas contas, exceto, talvez, maravilhar-se com a existência delas em nosso banco de dados. Espero que fique para a Parte 2, na qual apresentarei as sessões e a autenticação dos usuários que agora podem se registrar.
A fonte completa desse aplicativo está disponível aqui: https://github.com/brett19/node-gameapi
Aproveite! Brett
Eu incentivaria qualquer pessoa que esteja fazendo isso a usar o hapi em vez do express. https://github.com/spumko/hapi...
Olá, talvez eu possa perguntar por que você sugere isso? Escolhi o express por ser uma biblioteca bem conhecida e estável e por ter alguns recursos que faltavam no restify e em outras bibliotecas semelhantes.
Abraços, Brett
Ele foi criado por Eran, trabalhou no Oath1 e foi o líder do Oauth2.
Há problemas com o Express que ele resolveu. Você provavelmente deveria assistir a este vídeo https://www.youtube.com/watch?...
Obrigado por isso, talvez. É um vídeo bastante interessante :) Infelizmente, minha série de blogs já começou, então seria perigoso pensar em mudar a essa altura!
Que bom que você gostou! Talvez na próxima vez :)
Olá! Tutorial muito útil e amigável para iniciantes. O que notei: o bucket gameapi não existe, você está usando o módulo crypt antes de ele ser discutido e está passando o accountModel.create req.body e não newUser. Aguardo ansiosamente a parte 2 da série! Obrigado pela atenção
Você não deveria estar passando newUser (que tem a senha criptografada) para accountModel.create() em vez do req.body original não modificado que conteria a senha em texto simples?
Também me pergunto se não seria melhor fazer o hash das senhas no nível do modelo, pois isso poderia ser tecnicamente considerado uma etapa de "preparação de dados" necessária para o armazenamento adequado dos dados. Em outras palavras, você *nunca* gostaria de armazenar um objeto de usuário com uma senha que não tenha sido hash primeiro ou, pelo menos, criptografada.
Portanto, em accountmodel.js, eu sugeriria definir a propriedade userDoc.password para require(\'crypto\').createHash(\'sha1\').update(user.password).digest(\'base64\') e, em seguida, simplesmente se livrar completamente da variável newUser em app.js, passando req.body para accountModel.create(), conforme mostrado no tutorial.
Você também precisa inicializar o accountmodel.js antes de chamar o método create() nele, como segue:
var AccountModel = require(\'./models/accountmodel.js\');
Por fim, para este tutorial, eu recomendaria indicar que a conexão com o Couchbase deve estar em database.js, pois a leitura dessa seção implicou naturalmente em app.js, uma vez que esse foi o último arquivo do qual o leitor estava trabalhando e não foi sinalizado para entrar em database.js para "configurar a conexão com o banco de dados".
O motivo pelo qual lidei com a criptografia da senha no aplicativo foi porque eu estava tentando projetar os modelos de forma que o que você armazena é o que você pode recuperar. Além disso, se você trocasse meus modelos personalizados por um ODM real do Node.js, como o ottoman (https://github.com/couchbasela..., você não teria a capacidade de criptografar automaticamente a senha no modelo, de qualquer forma. Com relação à primeira questão, você tem razão, atualizei o tutorial.
Abraços, Brett
Uma pergunta muito trivial....Mas por que não podemos armazenar o userDocName : \'user-username\', já que o nome de usuário deve ser exclusivo. Isso nos pouparia gravações de dados extras e também leituras. Ou há algum motivo que eu esteja perdendo?
Olá, Saransh,
Em minha implementação original, e potencialmente nesta, se eu a expandisse. Há mais de um método único para autenticar um usuário em sua conta. Você pode oferecer suporte a nome de usuário/senha, mas também a contas do Facebook ou do Google Play. Nesse caso, você precisa de chaves de pesquisa separadas para cada uma, mas que ainda apontem para o mesmo tipo de conta genérica.
Abraços, Brett
Tenho uma pergunta simples. Sou novo no Couchbase e em todos os bancos de dados NoSQL, então talvez seja uma pergunta estúpida.
Quero verificar o nome de usuário e o e-mail no banco de dados, para que não haja dois e-mails ou nomes de usuário iguais. Você já fez isso para o nome de usuário, com um documento de referência. Você faria isso da mesma forma para o e-mail? Se você tiver 100 mil usuários, isso não incharia o banco de dados?
Qual seria a melhor abordagem? Documentos de referência? Obter todos os usuários e verificar seus nomes de usuário e e-mails? Reduzir o mapa?
Olá, José,
O uso de documentos de referência é a maneira mais fácil de implementar qualquer tipo de tabela de pesquisa exclusiva. Você poderia usar o map/reduce, mas, como a indexação ocorre de forma assíncrona, é possível que você acabe com usuários duplicados. Do ponto de vista de espaço, o uso de uma visualização de mapa/redução ou de documentos referenciais provavelmente usará quantidades semelhantes de espaço.
Abraços, Brett
[...] Blog da semana: Servidores de jogos e Couchbase com Node.js - Parte 1 [...]
[...] se você ainda não leu a Parte 1 desta série, sugiro que o faça, pois ela define o layout básico do projeto, bem como o usuário básico [...]
[...] Postagem do blog da semana #2a: Servidores de jogos e Couchbase com Nodejs (parte 1) [...]