Sin categoría

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

En esta parte de la serie, vamos a configurar un sistema de almacenamiento de datos del juego que te permita almacenar el estado del jugador a lo largo de su disfrute del juego. Para ello, vamos a crear algunos puntos finales /state y /states que representarán bloques individuales de datos de estado. Vamos a permitir múltiples bloques de estado con nombre para permitir que el juego divida los datos de estado en bloques actualizables por separado para evitar la necesidad de escribir muchos bloques de estado cuando sólo una parte ha cambiado.

Si aún no ha leído Parte 1 y Parte 2 de esta serie, le sugiero que lo haga, ya que esta parte y las futuras se basan en ellas.

Quick Aside - Renovación de la sesión

Algo que debería haber estado en mi anterior entrada del blog, y que es importante, es renovar la sesión de los usuarios cada vez que acceden a ella. Sin esto, la sesión está garantizada para expirar después de 60 minutos, independientemente de si el jugador sigue jugando. Obviamente, esta no es nuestra intención, ¡así que vamos a arreglarlo!

Primero tenemos que añadir una nueva función a nuestro SessionModel, así que abre tu sessionmodel.js y vamos a añadir el siguiente bloque. Es una función bastante sencilla; toma un identificador de sesión y realiza una operación de toque contra él para restablecer el tiempo de caducidad a 3600 de nuevo (a partir del momento de ejecución del toque, no cuando la clave se insertó originalmente).

SessionModel.toque = función(sid, devolución de llamada) {
var sessDocName = "sess- + sid;

db.toque(sessDocName, {caducidad: 3600}, función(err, resultado) {
devolución de llamada(err);
});
};

Ahora que tenemos nuestra función modelo para actualizar la sesión, vamos a encontrar un buen lugar para llamar. Nuestro método authUser parece una buena opción, ya que se ejecuta en cualquier endpoint que requiera que el usuario se autentique. Hagámoslo ahora. Aquí está nuestra nueva función authUser con nuestra llamada touch añadida.

función authUser(consulte, res, siguiente) {
req.uid = null;
si (req.cabeceras.autorización) {
var authInfo = req.cabeceras.autorización.dividir(‘ ‘);
si (authInfo[0] === Portador) {
var sid = authInfo[1];
sessionModel.consiga(sid, función(err, uid) {
si (err) {
siguiente(Su identificador de sesión no es válido);
} si no {
sessionModel.toque(sid, función(){});
req.uid = uid;
siguiente();
}
});
} si no {
siguiente(Debe estar autorizado para acceder a este punto final".);
}
} si no {
siguiente(Debe estar autorizado para acceder a este punto final".);
}
}

Como parte de esta corrección de la caducidad de la sesión, es posible que tenga que extraer su versión couchnode directamente de GitHub debido a un error con nuestra implementación táctil que se solucionó antes de la publicación de este blog, pero después de nuestro último lanzamiento del ciclo.

Estados del juego - El modelo

Ahora que hemos aclarado nuestro pequeño problema de la parte anterior, ¡vamos a pasar a implementar nuestro guardado de estado del juego! Como he dicho antes, vamos a permitir que los juegos almacenen datos en múltiples bloques de estado para reducir el tráfico de red. Desde el punto de vista del almacenamiento, almacenaremos todos estos bloques de estado en un único documento de Couchbase, y este documento se creará perezosamente en la primera petición de guardar información para el usuario. Hasta el momento en que haya algo guardado, emularemos una lista de estados vacía para el usuario, como verás en breve.

Para empezar, vamos a configurar nuestro diseño de archivo de modelo estándar en model/statemodel.js. Importamos nuestros módulos necesarios y creamos un modelo sin métodos llamado StateModel.

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

función StateModel() {
}

módulo.exportaciones = StateModel;

Ahora que tenemos lo básico para nuestro modelo, vamos a empezar a implementar algunos de los métodos que serán necesarios. Empecemos con un modelo que nos permita guardar un nuevo bloque de estado. Esta función se encargará tanto de la creación como de la actualización de un bloque de estado. Esto hace que la lógica del cliente sea mucho más sencilla, ya que no tenemos que preocuparnos de si el bloque de estado ya existe a nivel de API. Utilizaremos una forma de bloqueo optimista en la que se almacenará un número de versión con cada bloque de estado. Cada vez que se actualice un bloque de estado, será necesario pasar el número de versión existente en el servidor antes de que éste acepte los nuevos datos. Esto es para evitar que múltiples copias del juego que se ejecutan simultáneamente pisoteen los datos de los demás. Este es también el primer lugar en el que utilizaremos la función de Couchbase bloqueo optimista para asegurarnos de que no estamos realizando cambios simultáneos en nuestro objeto states desde dos llamadas a endpoint.

Empecemos con nuestro prototipo de la función guardar.

StateModel.guardar = función(uid, nombre, preVer, datos, devolución de llamada) {
};

Nuestro primer paso real es construir un nombre para nuestro documento de almacenamiento de estado, y luego solicitar este documento a Couchbase para comprobar si ya existe uno.

var stateDocName = usuario + uid + Estado;
db.consiga(stateDocName, función(err, resultado) {
// ¡El código de abajo va aquí!
});

Ahora comprobamos si se ha producido algún error al solicitar un documento de estado existente. Si encontramos un error, verificamos que no sea un error 'no encontrado'. Si el documento no fue encontrado, ignoramos este error y continuamos, esto es debido a la naturaleza de creación perezosa de nuestro documento de estados.

si (err) {
si (err.código !== couchbase.errores.keyNotFound) {
devolver devolución de llamada(err);
}
}

A continuación movemos nuestro documento de estado existente (o un nuevo documento si no se encontró ninguno), a una variable separada para un acceso más fácil, y para que podamos manejar tanto los documentos existentes como los nuevos documentos de la misma manera.

var stateDoc = {
tipo: Estado,
uid: uid,
estados: {}
};
si (resultado.valor) {
stateDoc = resultado.valor;
}

Ahora haremos lo mismo con nuestro bloque de estado. Verás que nos aseguramos de que nuestra var stateBlock haga referencia a la matriz de estados de los documentos de estado reales. Otro punto que vale la pena mencionar es que nuestra versión de bloque de estado por defecto es 0. Esto significa que al ejecutar un guardado por primera vez, se espera que el cliente especifique la versión 0 para aclarar que es consciente de que se trata de un nuevo bloque de estado.

var stateBlock = {
versión: 0,
datos: null
};
si (stateDoc.estados[nombre]) {
stateBlock = stateDoc.estados[nombre];
} si no {
stateDoc.estados[nombre] = stateBlock;
}

A continuación, tenemos que comprobar que la versión especificada por el autor de la llamada sigue coincidiendo con la almacenada en nuestro clúster. Si no es así, otro usuario debe haber realizado cambios desde la última vez que el cliente recuperó los datos guardados. Se espera que si hay un desajuste de versión, el cliente recupere los nuevos datos, realice cualquier fusión necesaria y vuelva a intentar su actualización.

if (stateBlock.version !== preVer) {
return callback('Su versión no coincide con la versión del servidor.');
} else {
stateBlock.version++;
stateBlock.data = datos;
}

Como mencioné al principio de esta sección, también usaremos el bloqueo optimista integrado en Couchbase para asegurarnos de que las escrituras de nuestros documentos de estado se realizan en orden. Debido al hecho de que preformamos nuestro get anterior, luego hacemos nuestra comparación de versiones y finalmente hacemos la escritura de nuevo aquí, existe la posibilidad de que otra llamada a nuestro endpoint de guardado de estado haya alterado el objeto desde nuestro get original, pero antes de nuestro set, el bloqueo optimista usando valores cas previene esto. Para aprender más acerca de los valores cas, por favor revisa el artículo Manual Couchbase sobre valores cas.

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

Por último, para este método en particular, preformamos nuestro conjunto, cualquier error que se produzca se propaga a la persona que llama (esto probablemente debería ser envuelto en el nivel de modelo, como se mencionó anteriormente), y la devolución de llamada invocada con el bloque de estado que hemos almacenado.

db.configure(stateDocName, stateDoc, setOptions, función(err, resultado) {
si (err) {
devolver devolución de llamada(err);
}

devolución de llamada(null, stateBlock);
});

Por último, aquí está todo nuestro método de guardado. Es bastante largo, ¡pero espero que relativamente comprensible!

StateModel.guardar = función(uid, nombre, preVer, datos, devolución de llamada) {
var stateDocName = usuario + uid + Estado;
db.consiga(stateDocName, función(err, resultado) {
si (err) {
si (err.código !== couchbase.errores.keyNotFound) {
devolver devolución de llamada(err);
}
}

var stateDoc = {
tipo: Estado,
uid: uid,
estados: {}
};
si (resultado.valor) {
stateDoc = resultado.valor;
}

var stateBlock = {
versión: 0,
datos: null
};
si (stateDoc.estados[nombre]) {
stateBlock = stateDoc.estados[nombre];
} si no {
stateDoc.estados[nombre] = stateBlock;
}

si (stateBlock.versión !== preVer) {
devolver devolución de llamada('Su versión no coincide con la versión del servidor'.);
} si no {
stateBlock.versión++;
stateBlock.datos = datos;
}

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

db.configure(stateDocName, stateDoc, setOptions, función(err, resultado) {
si (err) {
devolver devolución de llamada(err);
}

devolución de llamada(null, stateBlock);
});
});
};

El siguiente método que incluiremos es findByUserId. Este método nos permitirá construir un endpoint que devuelva todos los bloques de estado para cualquier usuario en particular. Esto es principalmente una optimización del lado del cliente para permitir la obtención de todos los bloques de estado a la vez en lugar de realizar múltiples solicitudes. La función es extremadamente sencilla. Usando el mismo nombre de documento que nuestra función de guardar, intentamos cargar el documento de estado desde nuestro cluster, si existe, devolvemos la lista de estados dentro de este bloque, si falta el documento, devolvemos una lista vacía al usuario. Cualquier otro error es reenviado a la persona que llama.

StateModel.findByUserId = función(uid, devolución de llamada) {
var stateDocName = usuario + uid + Estado;
db.consiga(stateDocName, función(err, resultado) {
si (err) {
si (err.código === couchbase.errores.keyNotFound) {
devolver devolución de llamada(null, {});
} si no {
devolver devolución de llamada(err);
}
}
var stateDoc = resultado.valor;

devolución de llamada(null, stateDoc.estados);
});
};

La última función modelo que tenemos que construir es nuestro método para acceder a un único bloque de estado para un usuario. Esta función es casi idéntica a nuestra función findByUserId, salvo que además desglosamos un bloque de estado específico por nombre en lugar de devolver toda la lista.

StateModel.consiga = función(uid, nombre, devolución de llamada) {
var stateDocName = usuario + uid + Estado;

db.consiga(stateDocName, función(err, resultado) {
si (err) {
devolver devolución de llamada(err);
}
var stateDoc = resultado.valor;

si (!stateDoc.estados[nombre]) {
devolver devolución de llamada('No existe ningún bloque de estado con este nombre.');
}

devolución de llamada(null, stateDoc.estados[nombre]);
});
};

Estados del juego - Gestión de solicitudes

¡Ahora que tenemos nuestro modelo todo envuelto y listo para usar, vamos a empezar a construir los manejadores de solicitud para nuestros 3 puntos finales! Vamos a construir un punto final para solicitar todos los estados de un usuario, un punto final para solicitar un bloque de estado en particular y, finalmente, un punto final para actualizar un bloque de estado específico.

Antes de que podamos crear nuestros gestores de peticiones, tenemos que añadir una referencia al archivo statemodel.js que hemos creado anteriormente.

var stateModel = requiere('./modelos/statemodelo');

Ahora, vamos a empezar con nuestro punto final de guardado. Como en las partes anteriores, nuestros gestores de peticiones son extremadamente simples y simplemente envían las partes pertinentes de nuestra petición al modelo. Esperamos el nombre del bloque de estado como parte del URI, el número de versión como parte de nuestra consulta y, por último, los datos reales del bloque de estado en el cuerpo de la solicitud.

app.poner('/estado/:nombre', authUser, función(consulte, res, siguiente) {
stateModel.guardar(req.uid, req.parámetros.nombre, parseInt(req.consulta.preVer, 10),
req.cuerpo, función(err, estado) {
si (err) {
devolver siguiente(err);
}

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

A continuación necesitamos la capacidad de recuperar un bloque de estado que hayamos almacenado previamente.

app.consiga('/estado/:nombre', authUser, función(consulte, res, siguiente) {
stateModel.consiga(req.uid, req.parámetros.nombre, función(err, estado) {
si (err) {
devolver siguiente(err);
}

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

Por último, pero no menos importante, el punto final para solicitar todos los bloques de estado almacenados para un usuario en particular. Como he dicho antes, esto es principalmente para optimizar la secuencia de carga del juego donde generalmente necesitamos recuperar todos los bloques de estado.

app.consiga(/estados, authUser, función(consulte, res, siguiente) {
stateModel.findByUserId(req.uid, función(err, estados) {
si (err) {
devolver siguiente(err);
}

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

¡Finito!

Ahora que hemos construido nuestro modelo e implementado los gestores de peticiones necesarios, deberías poder iniciar tu aplicación como en las partes anteriores y realizar algunas peticiones a nuestro servidor de juegos para ver que nuestro duro trabajo está dando sus frutos.

> POST /estado/prueba?preVer=0
Cabecera(Autorización): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7
{
"name": "We Rock!",
"nivel": "13"
}
< 200 OK
{
"version": 1,
"datos": {
"name": "We Rock!",
"nivel": "13"
}
}

> GET /estado/prueba
Cabecera(Autorización): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7
< 200 OK
{
"version": 1,
"datos": {
"name": "We Rock!",
"nivel": "13"
}
}

> GET /estados
Cabecera(Autorización): Bearer 0e9dd36c-5e2c-4f0e-9c2c-bffeea72d4f7
< 200 OK
{
"test": {
"version": 1,
"datos": {
"name": "We Rock!",
"nivel": "13"
}
}
}

¡Éxito!

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.

6 Comentarios

  1. Escribí un cliente demo en Javascript que pedirá autenticación y guardará la posición del cursor usando un receptor onMouseStop personalizado cada 15ms: https://gist.github.com/rdev5/

  2. ¿Alguna razón en especial por la que haces la sesión-mantener-vivo con la función de toque en lugar de la de obtener y tocar?

    en el modelo de Sesión, en el get, podrías hacer:

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

    ¿No?

    1. Hola José,
      Esta es, de hecho, una optimización que se puede hacer, por desgracia me había perdido el toque por completo en la primera parte de la aplicación de las sesiones y por lo tanto acaba de hilvanar en ella, como un toque por separado, para simplificar.
      Saludos, Brett

      1. De acuerdo. Gracias, es un pequeño tutorial genial. He aprendido mucho.

  3. [...] Blog post of the week: Servidores de juegos y Couchbase con Node.js - Parte 3 [...]

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.