Servidores de juegos y Couchbase con Node.js - Parte 1

Parece que hoy en día casi todos los estudios de videojuegos están trabajando en juegos en red en los que los jugadores pueden interactuar y cooperar con sus amigos y con otros jugadores de todo el mundo. Teniendo en cuenta mi experiencia previa en la construcción de este tipo de servidores y que Couchbase encaja como un almacén de respaldo para un sistema como este, pensé que tal vez este podría ser un excelente tema para escribir. Estaré escribiendo esto en múltiples partes con cada parte implementando un aspecto específico del servidor de juego, adicionalmente, estaré haciendo el mismo tutorial usando nuestra librería cliente PHP para mostrar eso también.

Diseño del proyecto

Para empezar, necesitamos configurar algunas cosas básicas que nos permitan enviar y recibir peticiones HTTP así como conectarnos a nuestro cluster de Couchbase. Si aún no estás seguro de cómo hacer esto, por favor echa un vistazo a mi entrada anterior donde lo explico con un poco más de detalle de lo que lo haré a continuación. Vamos a empezar nuestro proyecto con una estructura de directorios típica de Node.js con algunas carpetas adicionales para ayudar a organizar el código del servidor del juego.

/lib/
/lib/models/
/lib/app.js
/lib/database.js
/paquete.json

Siguiendo la estructura normal de Node.js, empezamos con nuestra carpeta 'lib' para guardar todos nuestros archivos fuente, con lib/server.js para actuar como archivo principal y finalmente nuestro package.json para describir las dependencias del proyecto y otros metadatos. Además, añadimos un database.js que gestionará de forma centralizada nuestra conexión a la base de datos para evitar que tengamos que instanciar una nueva conexión para cada solicitud, así como la carpeta /lib/models/ que utilizaremos para guardar el código fuente de nuestros distintos modelos de base de datos.

Conceptos básicos

Aquí tienes algo de contenido para tu package.json. Le damos un nombre a nuestro proyecto, apuntamos a su archivo Javascript principal y luego definimos un par de módulos prerrequisitos que necesitaremos más adelante. Una vez que hayas guardado este archivo, ejecuta npm instalar en el directorio raíz de su proyecto debería instalar las dependencias referenciadas.

{
"principal": "./lib/app",
"licencia" : "Apache2",
"nombre": "gameapi-couchbase",
"dependencias": {
"couchbase": “~1.0.0”,
"express": “~3.4.0”,
"uuid": “~1.4.1”
},
"devDependencias": {
},
"version": “0.0.1”
}

Nuestro siguiente paso es configurar el núcleo de nuestro servidor de juegos. Esto se coloca en nuestro /lib/app.js. Voy a ir a través de las secciones de este archivo bloque por bloque, y proporcionar una explicación de lo que está haciendo para cada uno.

Primero tenemos que importar los módulos que vamos a necesitar en este archivo. En este momento sólo necesitamos el módulo express para el enrutamiento HTTP y el análisis sintáctico, pero más adelante en este tutorial vamos a añadir más.

var express = requiere(exprés);

A continuación, vamos a configurar express, además adjuntamos el sub-módulo bodyParser de express para que podamos analizar los cuerpos JSON POST y PUT. Esto nos ayudará más adelante cuando nuestros clientes de juegos necesiten pasarnos bloques de datos JSON.

var aplicación = express();
app.utilice(exprés.bodyParser());

Para el único propósito de demostración, vamos a añadir una ruta simple a nuestro servidor HTTP para manejar las peticiones a la raíz de nuestro servidor.

app.consiga(‘/’, función(consulte, res, siguiente) {
res.enviar({esbirros: "¡Inclínate ante mí porque soy la RAÍZ!});
});

Por último, vamos a hacer que nuestro servidor HTTP escuche en el puerto 3000.

app.escuche(3000, función () {
consola.registro(Escuchando en el puerto 3000);
});

Aquí tienes una idea aproximada de cómo debería ser tu app.js hasta ahora:

var express = requiere(exprés);

var aplicación = express();
app.utilice(exprés.bodyParser());

app.consiga(‘/’, función(consulte, res, siguiente) {
res.enviar({esbirros: "¡Inclínate ante mí porque soy la RAÍZ!});
});

app.escuche(3000, función () {
consola.registro(Escuchando en el puerto 3000);
});

Para la última parte de nuestro proyecto básico, vamos a configurar nuestra conexión de base de datos. El código es bastante sencillo, importamos el módulo couchbase y posteriormente exportamos una nueva conexión a nuestro servidor alojado localmente y al bucket 'gameapi' a través de una propiedad del módulo llamada cubo principal.

var couchbase = requiere(couchbase);

// Conectar con nuestro servidor Couchbase

módulo.exportaciones.cubo principal = nuevo couchbase.Conexión({cubo:gameapi}, función(){});

En este punto, si abre un terminal en la raíz de su proyecto y ejecuta node lib/app.jsdebería aparecer el mensaje "Escuchando en el puerto 3000". Ahora también puede dirigir su navegador a http://localhost:3000 y vea nuestro trabajo hasta ahora en acción.

Es en este punto donde te sugiero que instales una aplicación que te permita elaborar peticiones HTTP específicas, a mí personalmente me encanta la extensión POSTman para Google Chrome. Esto será importante más adelante cuando quieras probar endpoints que no sean simples peticiones GET.

Creación de cuentas - Modelo de cuenta

Ahora que tenemos nuestro servidor básico en funcionamiento, vamos a empezar a trabajar en la parte "juego" de nuestro servidor de juegos. Vamos a empezar por la aplicación de la creación de la cuenta de punto final que será accesible por hacer una solicitud POST a la /usuarios URI. Para empezar este proceso, primero vamos a construir un modelo para que nuestro manejador de punto final trate con él y abstraiga algunos de los detalles de la implementación de nuestra base de datos. Estos modelos son donde la mayor parte de nuestras interacciones con Couchbase Server ocurrirán.

Empecemos por crear un nuevo archivo en nuestra base de datos /lib/modelos llamado 'accountmodel.js'. Una vez que tengas tu archivo accountmodel.js listo y abierto, vamos a empezar por importar algunos de los módulos que necesitaremos.

var uuid = requiere(uuid);
var couchbase = requiere(couchbase);
var db = requiere('./../base de datos').cubo principal;

Como puedes ver, hay 4 módulos que vamos a necesitar en este momento. Usaremos el módulo uuid para generar UUID's para nuestros objetos de base de datos. He visto a mucha gente usando contadores de secuencia implementados usando el sistema incr/decr de Couchbase, pero prefiero el método UUID que usaré aquí ya que evita la necesidad de hacer una operación adicional en la base de datos. A continuación importamos el módulo couchbase que utilizaremos para acceder a varias constantes que necesitaremos (errores principalmente). Y por último importamos el módulo de base de datos y cogemos la conexión a nuestro bucket gameapi que creamos anteriormente.

A continuación definiremos una sencilla función de ayuda que nos ayudará a eliminar cualquier propiedad a nivel de base de datos que nuestro modelo necesite y que no sea importante para el resto del servidor. En este momento la propiedad 'type' es la única propiedad que vamos a eliminar. Esta propiedad será utilizada por el gameapi para identificar qué tipo de objeto es un elemento en particular en nuestro cubo al hacer map-reduces más adelante.

función cleanUserObj(obj) {
borrar obj.tipo;
devolver obj;
}

Ahora definimos nuestra clase AccountModel.

función AccountModel() {
}

Y exportar la clase a otros archivos que importan este. Le sugiero que mantenga esta declaración siempre en la parte inferior de su archivo para que sea más fácil de encontrar cuando usted está tratando de identificar lo que exporta un archivo en particular.

módulo.exportaciones = AccountModel;

Ahora que nuestro Modelo está hecho, podemos construir nuestra función de creación que nos permitirá crear objetos de usuario. Voy a dividir esta función en trozos más pequeños para simplificar su explicación.

Empecemos por la definición de la propia función.

Modelo de cuenta.crear = función(usuario, devolución de llamada) {
};

A continuación, vamos a crear un objeto que se insertará en nuestro cubo Couchbase. Especificamos un tipo para el objeto, que como se mencionó anteriormente se utilizará más adelante. Generamos un UID para el usuario que nos ayudará a referirnos a él en todo momento. Finalmente, copiamos los detalles del usuario que fueron pasados a la función de creación. Usted puede notar que no preform cualquier validación de los datos que se pasan a nuestro modelo, esto se debe a que la mayor parte de nuestra solicitud de manejo de código tendrá una mejor idea de lo que aceptar o no aceptar, y nuestro modelo es sólo responsable de obtener los datos almacenados. Por último, generamos una clave para referirnos a este documento, usamos el tipo de documento y el UID del usuario para este propósito.

var userDoc = {
tipo: usuario,
uid: uuid.v4(),
nombre: usuario.nombre,
nombre de usuario: usuario.nombre de usuario,
contraseña: usuario.contraseña
};

var nombreDocUsuario = usuario + usuarioDoc.uid;

Para permitirnos encontrar a este usuario en el futuro por su nombre de usuario (¡probablemente no sea una buena idea hacer que tus usuarios recuerden sus UIDs!), crearemos un 'documento referencial', es decir, un documento con una clave basada en el nombre de usuario que apunta al documento de nuestro usuario (usando su UID). Esto también tiene la ventaja añadida de evitar que varios usuarios tengan el mismo nombre de usuario.

var refDoc = {
tipo: nombre de usuario,
uid: usuarioDoc.uid
};
var refDocName = nombre de usuario + usuarioDoc.nombre de usuario;

Finalmente, necesitamos insertar estos documentos en nuestro bucket de Couchbase. Primero, insertamos el documento referencial y manejamos el error keyAlreadyExists específicamente devolviendo un mensaje avisando al usuario de que el nombre de usuario está cogido y simplemente pasando cualquier otro error (probablemente deberíamos envolver nuestros errores de Couchbase a nivel de Modelo, pero eso no es importante en este punto de la serie). El hecho de que insertemos los documentos referenciales primero aquí es importante porque ++TODO++ ¿Por qué es importante otra vez? -TODO-. A continuación insertamos el documento de usuario propiamente dicho y finalmente invocamos el callback que se nos pasó. Primero desinfectamos el objeto devuelto usando la función que creamos antes para asegurarnos de que ninguna de nuestras propiedades a nivel de base de datos se filtre a otras capas de nuestra aplicación. También puedes notar que estamos pasando un valor 'cas' a través de nuestro callback. Esto será importante más adelante cuando necesitemos realizar bloqueo optimista en nuestro objeto Cuenta.

db.añada(refDocName, refDoc, función(err) {
si (err && err.código === couchbase.errores.claveYaExiste) {
devolver devolución de llamada('El nombre de usuario especificado ya existe');
} si no si (err) {
devolver devolución de llamada(err);
}

db.añada(nombreDocUsuario, userDoc, función(err, resultado) {
si (err) {
devolver devolución de llamada(err);
}

devolución de llamada(null, cleanUserObj(userDoc), resultado.cas);
});
});

Este es el aspecto que debería tener su archivo accountmodel.js hasta el momento:

var uuid = requiere(uuid);
var couchbase = requiere(couchbase);
var db = requiere('./../base de datos').cubo principal;

función cleanUserObj(obj) {
borrar obj.tipo;
devolver obj;
}

función AccountModel() {
}

Modelo de cuenta.crear = función(usuario, devolución de llamada) {
var userDoc = {
tipo: usuario,
uid: uuid.v4(),
nombre: usuario.nombre,
nombre de usuario: usuario.nombre de usuario,
contraseña: usuario.contraseña
};
var nombreDocUsuario = usuario + usuarioDoc.uid;

var refDoc = {
tipo: nombre de usuario,
uid: usuarioDoc.uid
};
var refDocName = nombre de usuario + usuarioDoc.nombre de usuario;

db.añada(refDocName, refDoc, función(err) {
si (err && err.código === couchbase.errores.claveYaExiste) {
devolver devolución de llamada('El nombre de usuario especificado ya existe');
} si no si (err) {
devolver devolución de llamada(err);
}

db.añada(nombreDocUsuario, userDoc, función(err, resultado) {
si (err) {
devolver devolución de llamada(err);
}

devolución de llamada(null, cleanUserObj(userDoc), resultado.cas);
});
});
};

módulo.exportaciones = AccountModel;

Creación de cuentas - Gestión de solicitudes

Ahora que hemos completado la función de creación en nuestro modelo de cuenta, podemos escribir una ruta express para manejar las solicitudes de creación de cuentas y pasar estas solicitudes a través de nuestra función. Primero necesitamos definir una ruta.

app.Correo electrónico:(/usuarios, función(consulte, res, siguiente) {
// ¡Los siguientes trozos van aquí!
});

Y... realizar alguna validación para asegurar que los datos necesarios fueron pasados al endpoint.

si (!req.cuerpo.nombre) {
devolver res.enviar(400, Debe especificar un nombre);
}
si (!req.cuerpo.nombre de usuario) {
devolver res.enviar(400, Debe especificar un nombre de usuario);
}
si (!req.cuerpo.contraseña) {
devolver res.enviar(400, Debe especificar una contraseña);
}

Una vez que los datos han sido *cough* "validados" */cough*, podemos generar el hash SHA1 para la contraseña del usuario (¡nunca almacenes las contraseñas de un usuario en texto plano!) y luego ejecutar la función create que construimos anteriormente en este post. También puedes notar que elimino la contraseña del usuario del objeto user antes de pasarlo de vuelta al cliente. Esto es de nuevo por seguridad, ya que queremos limitar la transmisión de la contraseña del usuario (en cualquier formato) tanto como sea posible.

var nuevoUsuario = req.cuerpo;
nuevoUsuario.contraseña = cripta.sha1(nuevoUsuario.contraseña);

accountModel.crear(req.cuerpo, función(err, usuario) {
si (err) {
devolver siguiente(err);
}

borrar usuario.contraseña;
res.enviar(usuario);
});

En resumen, toda la ruta de creación de la cuenta debería tener este aspecto:

app.Correo electrónico:(/usuarios, función(consulte, res, siguiente) {
si (!req.cuerpo.nombre) {
devolver res.enviar(400, Debe especificar un nombre);
}
si (!req.cuerpo.nombre de usuario) {
devolver res.enviar(400, Debe especificar un nombre de usuario);
}
si (!req.cuerpo.contraseña) {
devolver res.enviar(400, Debe especificar una contraseña);
}

var nuevoUsuario = req.cuerpo;
nuevoUsuario.contraseña = cripta.sha1(nuevoUsuario.contraseña);

accountModel.crear(nuevoUsuario, función(err, usuario) {
si (err) {
devolver siguiente(err);
}

borrar usuario.contraseña;
res.enviar(usuario);
});
});

Finale

Bueno, por fin hemos llegado al final de la parte 1 de nuestro tutorial. Hemos sacado muchas cosas básicas del camino, así que las futuras partes de la serie deberían ser un poco más cortas (¡aunque no prometo nada!). En este punto, deberías ser capaz de ejecutar una petición POST a tu endpoint /users y crear un nuevo usuario como este:

> POST /usuarios
{
"name": "Brett Lawson",
"nombre de usuario": "brett19",
"contraseña": "¡éxito!"
}
< 200 OK
{
"uid": “b836d211-425c-47de-9faf-5d0adc078edc”,
"name": "Brett Lawson",
"nombre de usuario": "brett19"
}

Desafortunadamente, en este punto no hay mucho que puedas hacer con tus nuevas cuentas, excepto quizás maravillarte de su existencia en nuestra base de datos. Espero que te quedes para la Parte 2, donde introduciré las sesiones y la autenticación de los usuarios que ahora pueden registrarse.

La fuente completa de esta aplicación está disponible aquí: https://github.com/brett19/node-gameapi

¡Que aproveche! Brett

Comparte este artículo
Recibe actualizaciones del blog de Couchbase en tu bandeja de entrada
Este campo es obligatorio.

Autor

Publicado por Brett Lawson, Ingeniero de Software Principal, Couchbase

Brett Lawson es Ingeniero de Software Principal en Couchbase. Brett es responsable del diseño y desarrollo de los clientes Node.js y PHP de Couchbase, además de participar en el diseño y desarrollo de la biblioteca C, libcouchbase.

16 Comentarios

  1. Yo animaría a cualquiera que haga esto a buscar el uso de hapi en lugar de express. https://github.com/spumko/hapi

    1. ¿Puedo preguntar por qué sugieres esto? Elegí express porque es una librería conocida, estable y tenía algunas características que le faltaban a restify y otras librerías similares.
      Saludos, Brett

      1. Fue creado por Eran, trabajó en Oath1 y fue el líder en Oauth2.
        Hay problemas con Express que él ha resuelto. Usted probablemente debería ver este video https://www.youtube.com/watch?…

        1. Gracias. Un vídeo muy interesante :) Desgraciadamente, mi serie de blogs ya ha empezado, ¡así que sería peligroso plantearse un cambio en este momento!

          1. Me alegro de que te haya gustado. Tal vez la próxima vez :)

  2. Lo que he notado: el cubo gameapi no existe, que está utilizando el módulo de cripta antes de que se discute y que está pasando el req.body accountModel.create no newUser. ¡Espero con interés la parte 2 de la serie! Gracias

  3. ¿No deberías pasar newUser (que tiene la contraseña encriptada) a accountModel.create() en lugar del req.body original sin modificar que contendría la contraseña en texto plano?

    1. También me pregunto si no sería mejor hacer hash de las contraseñas a nivel de modelo, ya que técnicamente podría considerarse un paso de "preparación de datos" necesario para el correcto almacenamiento de datos. En otras palabras, usted *nunca* querría almacenar un objeto de usuario con una contraseña que no ha sido hasheada primero o al menos encriptada.

      Así que en accountmodel.js, yo sugeriría establecer la propiedad userDoc.password a require(\'crypto\').createHash(\'sha1\').update(user.password).digest(\'base64\') y luego simplemente deshacerse de la variable newUser por completo en app.js, pasando req.body a accountModel.create() como se muestra en el tutorial.

      También es necesario inicializar accountmodel.js antes de llamar al método create() como se indica a continuación:

      var AccountModel = require(\'./models/accountmodel.js\');

      Por último, para este tutorial, yo recomendaría indicar que la conexión Couchbase debe estar en database.js como la lectura a través de esa sección naturalmente implicado app.js ya que fue el último archivo que el lector estaba trabajando fuera de y no estaba señalado a pop en database.js para "configurar su conexión de base de datos".

      1. La razón por la que manejé el cifrado de la contraseña en la aplicación fue porque estaba tratando de diseñar los modelos de tal manera que lo que almacenas es lo que puedes recuperar, además, si cambias mis modelos personalizados por un ODM Node.js real como otomano (https://github.com/couchbasela..., de todas formas no tendrías la posibilidad de encriptar automáticamente la contraseña en el modelo. En cuanto a la primera cuestión, tienes razón, he actualizado el tutorial.
        Saludos, Brett

  4. Saransh Mohapatra febrero 6, 2014 a 9:06 pm

    Una pregunta muy trivial....Pero ¿por qué no podemos almacenar el userDocName : \user-username' ya que se supone que el nombre de usuario es único. Esto nos ahorraría datos adicionales escribe y también lee. ¿O es por alguna razón que me estoy perdiendo?

    1. Hey Saransh,
      En mi implementación original, y potencialmente en esta si la ampliara. Hay más de un método para autenticar a un usuario en su cuenta. Usted puede apoyar nombre de usuario / contraseña, pero también potencialmente Facebook o Google Play cuentas. En este caso necesitas claves de búsqueda separadas para cada una, pero que sigan apuntando al mismo tipo de cuenta genérica.
      Saludos, Brett

  5. Tengo una pregunta simple. Soy nuevo en Couchbase y en todas las bases de datos NoSQL, así que quizás sea una pregunta estúpida.

    Quiero comprobar el nombre de usuario y el email en la base de datos, para que no haya dos emails o nombres de usuario iguales. Ya lo hiciste para el nombre de usuario, con un documento de referencia. ¿Podría hacerlo de la misma manera para el correo electrónico? ¿Si tienes 100k usuarios no se inflaría la base de datos?
    ¿Cuál sería el mejor enfoque? ¿documentos de referencia? ¿conseguir todos los usuarios y comprobar sus nombres de usuario y correos electrónicos? ¿Map reduce?

    1. Hola José,
      El uso de documentos de referencia es la forma más sencilla de implementar cualquier tipo de tabla de búsqueda única. Se podría utilizar map/reduce, pero debido al hecho de que la indexación se produce de forma asíncrona, es posible que se terminara con usuarios duplicados. Desde una perspectiva de espacio, el uso de una vista map/reduce o el uso de documentos referenciales probablemente utilizarán cantidades similares de espacio.
      Saludos, Brett

  6. [...] Blog de la Semana: Servidores de juegos y Couchbase con Node.js - Parte 1 [...]

  7. [...] aún no ha leído la Parte 1 de esta serie, le sugiero que lo haga, ya que establece el diseño básico del proyecto, así como [...]

  8. [...] Entrada de blog de la semana #2a: Servidores de juegos y Couchbase con Nodejs (parte 1) [...]

Deja un comentario

¿Listo para empezar con Couchbase Capella?

Empezar a construir

Consulte nuestro portal para desarrolladores para explorar NoSQL, buscar recursos y empezar con tutoriales.

Utilizar Capella gratis

Ponte manos a la obra con Couchbase en unos pocos clics. Capella DBaaS es la forma más fácil y rápida de empezar.

Póngase en contacto

¿Quieres saber más sobre las ofertas de Couchbase? Permítanos ayudarle.