Utilizar Firebase para los eventos en tiempo real en App Engine

En este artículo se muestra cómo conectar App Engine a Firebase y, a continuación, utilizar Firebase para enviar actualizaciones en tiempo real en un juego interactivo multijugador de tres en línea. Puedes obtener el código de muestra en el proyecto de tres en línea para Python o Java.

Puedes usar App Engine junto con Firebase Realtime Database para enviar actualizaciones inmediatas al navegador y a los clientes móviles sin una conexión de transmisión persistente con el servidor o un sondeo largo. Esta capacidad es útil para las aplicaciones que brindan actualizaciones a los usuarios sobre información nueva en tiempo real. Los casos prácticos de ejemplo incluyen aplicaciones colaborativas, juegos multijudador y salas de chat.

Usar Firebase es mejor que realizar sondeos cuando las actualizaciones no pueden predecirse o programarse como cuando se retransmite información entre usuarios humanos o cuando los eventos no se generan de manera sistemática.

Este artículo muestra cómo completar las siguientes tareas en el servidor:

  • Configura el servidor para utilizar Firebase Realtime Database.
  • Crea una referencia de base de datos única de Firebase para cada cliente web que se conecte a tu servicio.
  • Envía actualizaciones en tiempo real a los clientes web; para ello, cambia los datos que figuran en una referencia de base de datos particular.
  • Mejora la seguridad cuando se acceda a los mensajes; para ello, crea tokens únicos para cada cliente web y utiliza las reglas de seguridad de la base de datos de Firebase.
  • Recibe mensajes de los clientes web a través de HTTP.
  • Borra los datos del juego de la base de datos después de que haya terminado la partida.

Este artículo también muestra cómo completar las siguientes tareas en el navegador web:

  • Conéctate a Firebase con un token único del servidor.
  • Actualiza de manera dinámica la interfaz cuando se actualice la referencia de base de datos.
  • Envía mensajes de actualización al servidor para que puedan pasarse a clientes remotos.

Comenzar a usar Firebase Realtime Database

Con Firebase Realtime Database, puedes compilar aplicaciones avanzadas y colaborativas, ya que permite el acceso a la base de datos directamente desde el código del cliente. Los datos persisten de forma local. Además, los eventos en tiempo real se siguen activando, incluso sin conexión, lo que proporciona una experiencia adaptable al usuario final. Cuando el dispositivo vuelve a conectarse, Realtime Database sincroniza los cambios de los datos locales con las actualizaciones remotas que ocurrieron mientras el cliente estaba sin conexión, lo que combina los conflictos de forma automática.

Crear un proyecto de Firebase

  1. Crea una cuenta de Firebase o inicia sesión en una cuenta existente.

  2. Haz clic en Agregar proyecto.

  3. Ingresa un nombre en el campo Nombre del proyecto.

  4. Sigue los pasos de configuración restantes y haz clic en Crear proyecto.

  5. Después de que el asistente aprovisione tu proyecto, haz clic en Continuar.

  6. En la página Descripción general de tu proyecto, haz clic en el ícono de ajustes Configuración y, a continuación, haz clic en Configuración del proyecto.

  7. Selecciona la opción Agregar Firebase a tu aplicación web.

  8. Haz una copia del fragmento de código de inicialización, que se requiere en la sección Agregar el SDK de Firebase a tu página web.

Crear Realtime Database

  1. En el menú de la izquierda de Firebase console, selecciona Base de datos en el grupo Desarrollar.

  2. En la página Base de datos, ve a la sección Realtime Database y haz clic en Crear base de datos.

  3. En el cuadro de diálogo Reglas de seguridad para Realtime Database, selecciona Comenzar en modo de prueba y haz clic en Habilitar.

Trabajar con datos en Firebase

Todos los datos de Firebase Realtime Database se almacenan como objetos JSON. La base de datos puede conceptualizarse como un árbol JSON alojado en la nube. A diferencia de una base de datos de SQL, no hay tablas ni registros. Cuando le agregas datos al árbol JSON, estos se convierten en un nodo de la estructura JSON existente con una clave asociada. Puedes proporcionar tus propias claves como ID de usuario o nombres semánticos.

Esta estructura facilita la lectura de datos porque los clientes solo necesitan navegar a una ruta determinada y todo el objeto se muestra en el formato JSON. No se requiere un procesamiento especial de los campos de la base de datos. Para obtener más información, consulta cómo Estructurar tu base de datos en la documentación de Firebase.

Los backends de Java pueden utilizar la API de REST o el SDK de Firebase Admin. Si utilizas el SDK de Firebase Admin para Java en App Engine, asegúrate de usar el entorno de ejecución de Java 8. La versión 6.0.0 y superior del SDK dependen de la compatibilidad para multiprocesos disponible en el entorno de ejecución de Java 8 en App Engine.

Cuando se utiliza la API de REST, Firebase maneja los métodos PUT, PATCH, POST, GET y DELETE de HTTP estándar para realizar las operaciones de la base de datos. Para obtener más información sobre cómo utiliza Firebase estas operaciones, consulta Guardar datos y Recuperar datos en la documentación de Firebase.

Escribir datos y enviar mensajes

Para realizar solicitudes autenticadas, las aplicaciones deben obtener las Credenciales predeterminadas de aplicación de App Engine con los alcances necesarios de la autorización y utilizarlas para crear un objeto HTTP. Las solicitudes que se realizan con el objeto HTTP automáticamente incluyen los encabezados Authorization que se necesitan para realizar solicitudes a Firebase. Para obtener más información sobre las Credenciales predeterminadas de la aplicación, consulta Configurar la autenticación para aplicaciones de producción de servidor a servidor.

Puedes usar el método PUT de HTTP para escribir o reemplazar datos en una ruta específica de Firebase, como se muestra en el siguiente ejemplo:

Python

def _get_http():
    """Provides an authed http object."""
    http = httplib2.Http()
    # Use application default credentials to make the Firebase calls
    # https://firebase.google.com/docs/reference/rest/database/user-auth
    creds = GoogleCredentials.get_application_default().create_scoped(
        _FIREBASE_SCOPES)
    creds.authorize(http)
    return http
...
def firebase_put(path, value=None):
    """Writes data to Firebase.

    An HTTP PUT writes an entire object at the given database path. Updates to
    fields cannot be performed without overwriting the entire object

    Args:
        path - the url to the Firebase object to write.
        value - a json string.
    """
    response, content = _get_http().request(path, method='PUT', body=value)
    return json.loads(content)

Java

public HttpResponse firebasePut(String path, Object object) throws IOException {
  // Make requests auth'ed using Application Default Credentials
  Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
  HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);

  String json = new Gson().toJson(object);
  GenericUrl url = new GenericUrl(path);

  return requestFactory
      .buildPutRequest(url, new ByteArrayContent("application/json", json.getBytes()))
      .execute();
}

Para ver implementaciones de muestra en GitHub para PATCH y POST, haz clic en el botón Ver en GitHub en el ejemplo de código anterior.

Para leer datos, realiza una solicitud GET HTTP para una ruta específica. La respuesta contiene el objeto JSON en la ubicación solicitada.

Python

def firebase_get(path):
    """Read the data at the given path.

    An HTTP GET request allows reading of data at a particular path.
    A successful request will be indicated by a 200 OK HTTP status code.
    The response will contain the data being retrieved.

    Args:
        path - the url to the Firebase object to read.
    """
    response, content = _get_http().request(path, method='GET')
    return json.loads(content)

Java

public HttpResponse firebaseGet(String path) throws IOException {
  // Make requests auth'ed using Application Default Credentials
  Credential credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
  HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);

  GenericUrl url = new GenericUrl(path);

  return requestFactory.buildGetRequest(url).execute();
}

Consulta Hacer una revisión general en la aplicación de muestra para ver ejemplos que utilizan PATCH y DELETE.

Detectar eventos en tiempo real desde un navegador web

Para detectar eventos en tiempo real, necesitas:

  • Agregar el SDK de Firebase a tu página web
  • Agregar una referencia a la ruta de Firebase donde se almacenan los datos
  • Agregar un objeto de escucha
  • Implementar una función de devolución de llamada

Agregar el SDK de Firebase a tu página web

Para agregar el SDK de Firebase a tu página web:

Detectar los cambios en los datos

Para recuperar datos en Firebase, adjunta un objeto de escucha asíncrono a firebase.database.Reference. El objeto de escucha se activa una vez para el estado inicial de los datos y otra vez cuando los datos cambian. Existen dos tipos de cambios principales que el cliente puede detectar:

  • Eventos de valor: lee y detecta los cambios en todo el contenido de una ruta. Por lo general, los eventos de valor se utilizan para supervisar los cambios en un solo objeto.
  • Eventos secundarios: lee y detecta los cambios en los elementos secundarios en una ruta específica. Por lo general, los eventos secundarios se utilizan para supervisar los cambios dentro de las listas de objetos. Es posible detectar los siguientes eventos secundarios: child_added, child_changed, child_moved y child_removed.

Esta aplicación de ejemplo mantiene un solo objeto de estado de juego sincronizado entre el servidor y el cliente. Por este motivo, este artículo se centra en la detección de eventos de valor. Los objetos de escucha de eventos secundarios y las técnicas de recuperación de datos más avanzadas se describen en Recuperar datos en la Web.

El siguiente código muestra cómo detectar un evento de cambio de valor y agregar una función de devolución de llamada para realizar el trabajo cuando se activa el evento. El código primero crea una referencia de base de datos, que es la ruta del objeto que detecta la aplicación. A continuación, se agrega el objeto de escucha a la referencia con el método reference.on(), en donde la función de devolución de llamada se pasa como un argumento.

En este ejemplo, una aplicación de tres en línea en tiempo real envía información del juego a los clientes que se detectan en canales específicos, que corresponden al juego del usuario, y actualiza la IU cuando esta información cambia en la base de datos. El fragmento de JavaScript que se agregó en Agregar Firebase a tu página web crea automáticamente la variable firebase.

Python

// setup a database reference at path /channels/channelId
channel = firebase.database().ref('channels/' + channelId);
// add a listener to the path that fires any time the value of the data changes
channel.on('value', function(data) {
  onMessage(data.val());
});

Java

// setup a database reference at path /channels/channelId
channel = firebase.database().ref('channels/' + channelId);
// add a listener to the path that fires any time the value of the data changes
channel.on('value', function(data) {
  onMessage(data.val());
});

La implementación de la función de devolución de llamada, onMessage(), se muestra más adelante en este artículo.

Separar objetos de escucha

Para quitar las devoluciones de llamada, llama al método off() de tu referencia de base de datos de Firebase.

Publicar eventos de nuevo en App Engine

App Engine no admite actualmente conexiones HTTP de transmisión bidireccional. Si un cliente necesita actualizar el servidor, debe enviar una solicitud HTTP explícita.

Restringir el acceso a tus datos

Firebase te permite restringir el acceso a los datos a usuarios específicos mediante el uso de tokens de autorización. Recomendamos que solo tu servidor tenga acceso de escritura, al tiempo que autoriza a los clientes web para el acceso de solo lectura. El código que figura a continuación muestra cómo:

  • Generar credenciales para el servidor de App Engine
  • Generar tokens para cada cliente
  • Pasar tokens a los clientes web que pueden usar para leer datos

Establecer la comunicación entre App Engine y Firebase

Puedes usar las Credenciales predeterminadas de la aplicación integradas de App Engine para realizar llamadas autenticadas de REST a tu base de datos de Firebase desde el servidor de App Engine.

Python

Crea una instancia para un objeto GoogleCredentials, que proporciona el paquete oauth2client, con los alcances adecuados para otorgar acceso a Firebase. A continuación, utiliza ese objeto para autorizar un objeto httplib2.Http.

def _get_http():
    """Provides an authed http object."""
    http = httplib2.Http()
    # Use application default credentials to make the Firebase calls
    # https://firebase.google.com/docs/reference/rest/database/user-auth
    creds = GoogleCredentials.get_application_default().create_scoped(
        _FIREBASE_SCOPES)
    creds.authorize(http)
    return http

Ese objeto incluye el token de autenticación adecuado de forma automática, y puedes utilizar el objeto httplib2.Http directamente para realizar llamadas de REST. La siguiente función utiliza el objeto que se devuelve desde _get_http() para enviar una solicitud PATCH o DELETE. Ten en cuenta que este ejemplo llama a _get_http(), como parte de la instrucción de devolución, para obtener el objeto HTTP autorizado:

def _send_firebase_message(u_id, message=None):
    """Updates data in firebase. If a message is provided, then it updates
     the data at /channels/<channel_id> with the message using the PATCH
     http method. If no message is provided, then the data at this location
     is deleted using the DELETE http method
     """
    url = '{}/channels/{}.json'.format(_get_firebase_db_url(), u_id)

    if message:
        return _get_http().request(url, 'PATCH', body=message)
    else:
        return _get_http().request(url, 'DELETE')

Java

Debido a que App Engine restringe la capacidad de crear conversaciones en segundo plano, debes utilizar la API de Firebase en lugar del SDK del servidor de Firebase directamente.

Primero, obtén las Credenciales predeterminadas de la aplicación y, luego, inicializa el objeto UrlFetchTransport.

credential = GoogleCredential.getApplicationDefault().createScoped(FIREBASE_SCOPES);
      httpTransport = UrlFetchTransport.getDefaultInstance();

A continuación, utiliza esa credencial para crear un objeto HttpRequestFactory autorizado, con el uso del servicio de recuperación de URL que App Engine requiere para las solicitudes HTTP de ida. Para realizar llamadas a la base de datos de Firebase, construye una ruta y realiza la solicitud:

public void sendFirebaseMessage(String channelKey, Game game) throws IOException {
  // Make requests auth'ed using Application Default Credentials
  HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
  GenericUrl url =
      new GenericUrl(String.format("%s/channels/%s.json", firebaseDbUrl, channelKey));
  HttpResponse response = null;

  try {
    if (null == game) {
      response = requestFactory.buildDeleteRequest(url).execute();
    } else {
      String gameJson = new Gson().toJson(game);
      response =
          requestFactory
              .buildPatchRequest(
                  url, new ByteArrayContent("application/json", gameJson.getBytes()))
              .execute();
    }

    if (response.getStatusCode() != 200) {
      throw new RuntimeException(
          "Error code while updating Firebase: " + response.getStatusCode());
    }

  } finally {
    if (null != response) {
      response.disconnect();
    }
  }
}

Los ejemplos anteriores utilizan la biblioteca httplib2 para Python y la biblioteca cliente HTTP de Google para Java para manejar las solicitudes de REST. Sin embargo, puedes usar el método que prefieras para realizar las solicitudes de REST.

Generar tokens de autenticación de clientes

Firebase utiliza tokens web JSON (JWT) para autorizar a los usuarios. Para crear JWT, la aplicación aprovecha el servicio integrado App Identity de App Engine para firmar las reclamaciones con el uso de las Credenciales predeterminadas de la aplicación.

El siguiente método demuestra cómo crear JWT personalizados:

  1. El servicio App Identity se utiliza para obtener automáticamente el correo electrónico de la cuenta de servicio.
  2. Se construye el encabezado del token.
  3. Las reclamaciones se agregan a la carga útil del token de acuerdo con la documentación de autenticación de Firebase. La más importante de estas reclamaciones es uid, que es el identificador de canal de usuario único que restringe el acceso a un solo usuario.
  4. Por último, el servicio App Identity se utiliza nuevamente para firmar el token.

Python

def create_custom_token(uid, valid_minutes=60):
    """Create a secure token for the given id.

    This method is used to create secure custom JWT tokens to be passed to
    clients. It takes a unique id (uid) that will be used by Firebase's
    security rules to prevent unauthorized access. In this case, the uid will
    be the channel id which is a combination of user_id and game_key
    """

    # use the app_identity service from google.appengine.api to get the
    # project's service account email automatically
    client_email = app_identity.get_service_account_name()

    now = int(time.time())
    # encode the required claims
    # per https://firebase.google.com/docs/auth/server/create-custom-tokens
    payload = base64.b64encode(json.dumps({
        'iss': client_email,
        'sub': client_email,
        'aud': _IDENTITY_ENDPOINT,
        'uid': uid,  # the important parameter, as it will be the channel id
        'iat': now,
        'exp': now + (valid_minutes * 60),
    }))
    # add standard header to identify this as a JWT
    header = base64.b64encode(json.dumps({'typ': 'JWT', 'alg': 'RS256'}))
    to_sign = '{}.{}'.format(header, payload)
    # Sign the jwt using the built in app_identity service
    return '{}.{}'.format(to_sign, base64.b64encode(
        app_identity.sign_blob(to_sign)[1]))

Java

public String createFirebaseToken(Game game, String userId) {
  final AppIdentityService appIdentity = AppIdentityServiceFactory.getAppIdentityService();
  final BaseEncoding base64 = BaseEncoding.base64();

  String header = base64.encode("{\"typ\":\"JWT\",\"alg\":\"RS256\"}".getBytes());

  // Construct the claim
  String channelKey = game.getChannelKey(userId);
  String clientEmail = appIdentity.getServiceAccountName();
  long epochTime = System.currentTimeMillis() / 1000;
  long expire = epochTime + 60 * 60; // an hour from now

  Map<String, Object> claims = new HashMap<String, Object>();
  claims.put("iss", clientEmail);
  claims.put("sub", clientEmail);
  claims.put("aud", IDENTITY_ENDPOINT);
  claims.put("uid", channelKey);
  claims.put("iat", epochTime);
  claims.put("exp", expire);

  String payload = base64.encode(new Gson().toJson(claims).getBytes());
  String toSign = String.format("%s.%s", header, payload);
  AppIdentityService.SigningResult result = appIdentity.signForApp(toSign.getBytes());
  return String.format("%s.%s", toSign, base64.encode(result.getSignature()));
}

El servidor ejecuta la función anterior y pasa el token a la plantilla que se va a procesar en el cliente. En esta aplicación de tres en línea de ejemplo, el ID de usuario del jugador y la clave del juego comprenden el uid para la autorización de Firebase.

Autenticar el cliente de JavaScript

Después de que los tokens se hayan generado en el servidor y pasado al HTML del cliente, el cliente puede usarlos para autorizar los comandos de lectura y escritura. Solo es necesario autenticar al cliente una vez y, a continuación, se autentican todas las llamadas subsiguientes que utilizan el objeto firebase. El código de muestra ilustra cómo hacerlo:

Python

// sign into Firebase with the token passed from the server
firebase.auth().signInWithCustomToken(token).catch(function(error) {
  console.log('Login Failed!', error.code);
  console.log('Error message: ', error.message);
});

Java

// sign into Firebase with the token passed from the server
firebase.auth().signInWithCustomToken(token).catch(function(error) {
  console.log('Login Failed!', error.code);
  console.log('Error message: ', error.message);
});

Actualizar las reglas de seguridad de la base de datos de Firebase

En la sección Base de datos de Firebase console, selecciona la pestaña Reglas. A continuación, actualiza las reglas de seguridad de Firebase para restringir el acceso solo a las conexiones autorizadas. Específicamente, restringe las escrituras al servidor y permite solo las lecturas cuando el ID único del usuario, uid, en la carga útil de autorización coincide con el ID único en la ruta de la base de datos.

{
    "rules": {
      ".read": false,
      ".write": false,
      "channels": {
        "$channelId": {
          // Require auth token ID to match the path the client is trying to read
          ".read": "auth.uid == $channelId",
          ".write": false
        }
      }
    }
}

Hacer una revisión general en la aplicación de muestra

Para ilustrar mejor cómo usar Firebase a fin de compilar aplicaciones en tiempo real, observa la aplicación del juego de tres en línea de muestra. El código fuente completo de la aplicación para Python está disponible en la página de GitHub de Fire Tac Toe. El código fuente completo de la aplicación para Java está disponible en la página de GitHub de Fire Tac Toe.

El juego permite a los usuarios crear un juego, invitar a otro jugador mediante el envío de una URL y jugar de manera conjunta en tiempo real. La aplicación actualiza las vistas de ambos jugadores del tablero, en tiempo real, tan pronto como el otro jugador hace un movimiento.

En el resto de este artículo, se revisan los aspectos más destacados que ilustran las capacidades clave.

Conectarse a una ruta de Firebase

Cuando un usuario visita el juego por primera vez, se producen dos situaciones:

  • El servidor del juego inyecta un token en la página HTML que se envía al cliente. El cliente utiliza este token para abrir una conexión a Firebase y detectar las actualizaciones. La única ruta que el usuario detecta es /channels/[USER_ID + GAME_KEY].
  • El servidor del juego proporciona al usuario una URL que puede compartir con un amigo para invitarlo a unirse al juego.

El siguiente código del lado del servidor crea una ruta de Firebase para la aplicación del juego y lo inyecta en el HTML del cliente:

Python

# choose a unique identifier for channel_id
channel_id = user.user_id() + game_key
# encrypt the channel_id and send it as a custom token to the
# client
# Firebase's data security rules will be able to decrypt the
# token and prevent unauthorized access
client_auth_token = create_custom_token(channel_id)
_send_firebase_message(channel_id, message=game.to_json())

# game_link is a url that you can open in another browser to play
# against this player
game_link = '{}?g={}'.format(request.base_url, game_key)

# push all the data to the html template so the client will
# have access
template_values = {
    'token': client_auth_token,
    'channel_id': channel_id,
    'me': user.user_id(),
    'game_key': game_key,
    'game_link': game_link,
    'initial_message': urllib.unquote(game.to_json())
}

return flask.render_template('fire_index.html', **template_values)

Java

// The 'Game' object exposes a method which creates a unique string based on the game's key
// and the user's id.
String token = FirebaseChannel.getInstance().createFirebaseToken(game, userId);
request.setAttribute("token", token);

// 4. More general template values
request.setAttribute("game_key", gameKey);
request.setAttribute("me", userId);
request.setAttribute("channel_id", game.getChannelKey(userId));
request.setAttribute("initial_message", new Gson().toJson(game));
request.setAttribute("game_link", getGameUriWithGameParam(request, gameKey));
request.getRequestDispatcher("/WEB-INF/view/index.jsp").forward(request, response);

Ahora, el cliente web utiliza el token para detectar la ruta /channels/[USER_ID + GAME_KEY]. Esto sucede en la función openChannel(). Se agrega un objeto de escucha para seguir los cambios en el valor de la ruta. El método <reference>.on() de Firebase se utiliza para configurar una devolución de llamada que se ejecuta en los cambios de valor en este caso. Cuando cambia el valor de la ruta de la base de datos, se llama al método onMessage() para actualizar el estado del juego y la IU del usuario.

Python

function openChannel() {
  // sign into Firebase with the token passed from the server
  firebase.auth().signInWithCustomToken(token).catch(function(error) {
    console.log('Login Failed!', error.code);
    console.log('Error message: ', error.message);
  });

  // setup a database reference at path /channels/channelId
  channel = firebase.database().ref('channels/' + channelId);
  // add a listener to the path that fires any time the value of the data changes
  channel.on('value', function(data) {
    onMessage(data.val());
  });
  onOpened();
  // let the server know that the channel is open
}
...
function onOpened() {
  $.post('/opened');
}

Java

function openChannel() {
  // sign into Firebase with the token passed from the server
  firebase.auth().signInWithCustomToken(token).catch(function(error) {
    console.log('Login Failed!', error.code);
    console.log('Error message: ', error.message);
  });

  // setup a database reference at path /channels/channelId
  channel = firebase.database().ref('channels/' + channelId);
  // add a listener to the path that fires any time the value of the data changes
  channel.on('value', function(data) {
    onMessage(data.val());
  });
  onOpened();
  // let the server know that the channel is open
}
...
function onOpened() {
  $.post('/opened');
}

Enviar actualizaciones al servidor

Cuando el usuario realiza un movimiento, se llama al método moveInSquare(), que verifica que el movimiento sea válido, y envía una solicitud POST HTTP al servidor con la ubicación del movimiento.

Python

function moveInSquare(e) {
  var id = $(e.currentTarget).index();
  if (isMyMove() && state.board[id] === ' ') {
    $.post('/move', {i: id});
  }
}

Java

function moveInSquare(e) {
  var id = $(e.currentTarget).index();
  if (isMyMove() && state.board[id] === ' ') {
    $.post('/move', {cell: id});
  }
}

El servidor recibe la solicitud POST de HTTP y se llama al controlador para la ruta /move. Este controlador identifica al usuario y extrae la ubicación del cuadrado de la solicitud. Luego, realiza el movimiento; para ello, llama al método en el objeto Game:

Python

def move():
    game = Game.get_by_id(request.args.get('g'))
    position = int(request.form.get('i'))
    if not (game and (0 <= position <= 8)):
        return 'Game not found, or invalid position', 400
    game.make_move(position, users.get_current_user())
    return ''

Java

public class MoveServlet extends HttpServlet {

  @Override
  public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String gameId = request.getParameter("gameKey");
    Objectify ofy = ObjectifyService.ofy();
    Game game = ofy.load().type(Game.class).id(gameId).safe();

    UserService userService = UserServiceFactory.getUserService();
    String currentUserId = userService.getCurrentUser().getUserId();

    int cell = new Integer(request.getParameter("cell"));
    if (!game.makeMove(cell, currentUserId)) {
      response.sendError(HttpServletResponse.SC_FORBIDDEN);
    } else {
      ofy.save().entity(game).now();
    }
  }
}

El método que realiza el movimiento completa una ronda final de validación en el movimiento del usuario, comprueba si el usuario ganó el juego y escribe el estado del juego en el almacén de datos. Por último, llama a un método para actualizar el estado del juego en Firebase.

Python

def make_move(self, position, user):
    # If the user is a player, and it's their move
    if (user in (self.userX, self.userO)) and (
            self.moveX == (user == self.userX)):
        boardList = list(self.board)
        # If the spot you want to move to is blank
        if (boardList[position] == ' '):
            boardList[position] = 'X' if self.moveX else 'O'
            self.board = ''.join(boardList)
            self.moveX = not self.moveX
            self._check_win()
            self.put()
            self.send_update()
            return

Java

public boolean makeMove(int position, String userId) {
  String currentMovePlayer;
  char value;
  if (getMoveX()) {
    value = 'X';
    currentMovePlayer = getUserX();
  } else {
    value = 'O';
    currentMovePlayer = getUserO();
  }

  if (currentMovePlayer.equals(userId)) {
    char[] boardBytes = getBoard().toCharArray();
    boardBytes[position] = value;
    setBoard(new String(boardBytes));
    checkWin();
    setMoveX(!getMoveX());
    try {
      sendUpdateToClients();
    } catch (IOException e) {
      LOGGER.log(Level.SEVERE, "Error sending Game update to Firebase", e);
      throw new RuntimeException(e);
    }
    return true;
  }

  return false;
}

El método de actualización llama entonces al método que definiste anteriormente para realizar la solicitud HTTP en Firebase:

Python

def send_update(self):
    """Updates Firebase's copy of the board."""
    message = self.to_json()
    # send updated game state to user X
    _send_firebase_message(
        self.userX.user_id() + self.key.id(), message=message)
    # send updated game state to user O
    if self.userO:
        _send_firebase_message(
            self.userO.user_id() + self.key.id(), message=message)

Java

public String getChannelKey(String userId) {
  return userId + id;
}

/**
 * deleteChannel.
 * @param userId .
 * @throws IOException .
 */
public void deleteChannel(String userId) throws IOException {
  if (userId != null) {
    String channelKey = getChannelKey(userId);
    FirebaseChannel.getInstance().sendFirebaseMessage(channelKey, null);
  }
}

private void sendUpdateToUser(String userId) throws IOException {
  if (userId != null) {
    String channelKey = getChannelKey(userId);
    FirebaseChannel.getInstance().sendFirebaseMessage(channelKey, this);
  }
}

/**
 * sendUpdateToClients.
 * @throws IOException if we had some kind of network issue.
 */
public void sendUpdateToClients() throws IOException {
  sendUpdateToUser(userX);
  sendUpdateToUser(userO);
}

Por último, realiza la solicitud HTTP:

Python

def _send_firebase_message(u_id, message=None):
    """Updates data in firebase. If a message is provided, then it updates
     the data at /channels/<channel_id> with the message using the PATCH
     http method. If no message is provided, then the data at this location
     is deleted using the DELETE http method
     """
    url = '{}/channels/{}.json'.format(_get_firebase_db_url(), u_id)

    if message:
        return _get_http().request(url, 'PATCH', body=message)
    else:
        return _get_http().request(url, 'DELETE')

Java

public void sendFirebaseMessage(String channelKey, Game game) throws IOException {
  // Make requests auth'ed using Application Default Credentials
  HttpRequestFactory requestFactory = httpTransport.createRequestFactory(credential);
  GenericUrl url =
      new GenericUrl(String.format("%s/channels/%s.json", firebaseDbUrl, channelKey));
  HttpResponse response = null;

  try {
    if (null == game) {
      response = requestFactory.buildDeleteRequest(url).execute();
    } else {
      String gameJson = new Gson().toJson(game);
      response =
          requestFactory
              .buildPatchRequest(
                  url, new ByteArrayContent("application/json", gameJson.getBytes()))
              .execute();
    }

    if (response.getStatusCode() != 200) {
      throw new RuntimeException(
          "Error code while updating Firebase: " + response.getStatusCode());
    }

  } finally {
    if (null != response) {
      response.disconnect();
    }
  }
}

Después de esta llamada a Firebase, los clientes web reciben el estado actualizado del juego de inmediato, sin sondear el servidor.

Quitar objetos de escucha y borrar datos

Cuando un jugador ganó el juego, no hay información adicional para enviar desde el servidor. En este caso, debes quitar los objetos de escucha y borrar los datos del juego desde Firebase porque el almacenamiento no necesariamente es gratuito. Para hacerlo, llama al método off() en la referencia de Firebase que los clientes han detectado. Por último, el cliente envía un mensaje al servidor en el extremo /delete, que quita los datos almacenados para el juego que terminó.

Python

function onMessage(newState) {
  updateGame(newState);

  // now check to see if there is a winner
  if (channel && state.winner && state.winningBoard) {
    channel.off(); //stop listening on this path
    deleteChannel(); //delete the data we wrote
  }
}
...
function deleteChannel() {
  $.post('/delete');
}

Java

function onMessage(newState) {
  updateGame(newState);

  // now check to see if there is a winner
  if (channel && state.winner && state.winningBoard) {
    channel.off(); //stop listening on this path
    deleteChannel(); //delete the data we wrote
  }
}
...
function deleteChannel() {
  $.post('/delete');
}

Borra los datos en Firebase; para ello, presiona el controlador del extremo /delete:

Python

def delete():
    game = Game.get_by_id(request.args.get('g'))
    if not game:
        return 'Game not found', 400
    user = users.get_current_user()
    _send_firebase_message(user.user_id() + game.key.id(), message=None)
    return ''

Java

public class DeleteServlet extends HttpServlet {
  @Override
  public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String gameId = request.getParameter("gameKey");
    Objectify ofy = ObjectifyService.ofy();
    Game game = ofy.load().type(Game.class).id(gameId).safe();

    UserService userService = UserServiceFactory.getUserService();
    String currentUserId = userService.getCurrentUser().getUserId();

    // TODO(you): In practice, first validate that the user has permission to delete the Game
    game.deleteChannel(currentUserId);
  }
}

Pasos siguientes

  • Prueba otras características de Google Cloud Platform por ti mismo. Revisa nuestros instructivos.
¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...