Usa 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, usar 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 de dispositivos móviles sin una conexión de transmisión persistente con el servidor o un sondeo largo. Esta capacidad es útil para las apps que brindan actualizaciones a los usuarios sobre información nueva en tiempo real. Los casos prácticos de ejemplo incluyen apps de colaboración, 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.

En este artículo, se muestra cómo completar las siguientes tareas en el servidor:

  • Configurar el servidor para usar Firebase Realtime Database
  • Crear una referencia de base de datos única de Firebase para cada cliente web que se conecte a tu servicio
  • Enviar actualizaciones en tiempo real a los clientes web y, para ello, cambiar los datos que figuran en una referencia de base de datos particular
  • Mejorar la seguridad cuando se accede a los mensajes y, a fin de lograrlo, crear tokens únicos para cada cliente web y usar las reglas de seguridad de la base de datos de Firebase
  • Recibir 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.
  • Actualizar de manera dinámica la interfaz cuando se actualice la referencia de base de datos
  • Enviar mensajes de actualización al servidor para que puedan pasarse a clientes remotos

Comienza a usar Firebase Realtime Database

Con Firebase Realtime Database, puedes compilar apps enriquecidas y de colaboración, ya que permite el acceso a la base de datos directamente desde el código del cliente. Los datos se conservan de forma local. Además, los eventos en tiempo real se siguen activando, incluso sin conexión, lo que proporciona una experiencia receptiva 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.

Crea 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 Agrega el SDK de Firebase a tu página web.

Crea una base de datos de 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.

Trabaja 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 de 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 de 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 Estructura tu base de datos en la documentación de Firebase.

Los backends de Java pueden usar la API de REST o el SDK de Firebase Admin. Si usas 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 usa 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 Firebase usa estas operaciones, consulta Guarda datos y Recupera datos en la documentación de Firebase.

Escribe datos y envía mensajes

Para realizar solicitudes autenticadas, las apps deben obtener las credenciales predeterminadas de la aplicación de App Engine con los permisos necesarios de la autorización y usarla para crear un objeto HTTP. Las solicitudes realizadas con ese objeto HTTP incluyen de forma automática los encabezados Authorization necesarios para realizar solicitudes a Firebase. Para obtener más información sobre las credenciales predeterminadas de la aplicación, consulta Configura 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();
    }

A fin de 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.

A fin de leer datos, realiza una solicitud GET de 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 Haz una revisión general de la app de muestra para ver ejemplos en los que se usan PATCH y DELETE.

Escucha eventos en tiempo real desde un navegador web

Para escuchar 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, sigue estos pasos:

Escucha los cambios en los datos

Recupera los datos de Firebase y, para ello, 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 cada vez que los datos cambian. Existen dos tipos de cambios principales que el cliente puede escuchar:

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

En esta aplicación de ejemplo, se mantiene un solo objeto de estado de juego sincronizado entre el servidor y el cliente. Por este motivo, el foco de este artículo es 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 Recupera datos en la Web.

En el siguiente código, se muestra cómo escuchar 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. Mediante el código primero se crea una referencia de base de datos, que es la ruta del objeto que escucha la app. A continuación, el objeto de escucha se agrega a la referencia con el método reference.on(), en el que la función de devolución de llamada se pasa como un argumento.

En este ejemplo, una app de tres en línea en tiempo real envía información del juego a los clientes a la escucha en canales específicos, que corresponden al juego del usuario, y actualiza la IU cuando esta información cambia en la base de datos. Con el fragmento de JavaScript que se agregó en Agrega Firebase a tu página web se crea de forma automática 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 onMessage(), la función de devolución de llamada, se muestra más adelante en este artículo.

Desconecta objetos de escucha

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

Publica 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.

Restringe 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 y que autorices a los clientes web para el acceso de solo lectura. El código que figura a continuación muestra cómo realizar lo siguiente:

  • 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

Establece 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 de un objeto GoogleCredentials proporcionado por el paquete oauth2client con los permisos adecuados para otorgar acceso a Firebase. Luego, usa 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 de forma automática el token de autenticación adecuado. Puedes usar el objeto httplib2.Http directamente para realizar llamadas de REST. En la siguiente función, se usa el objeto que muestra _get_http() para enviar una solicitud PATCH o DELETE. Ten en cuenta que, en este ejemplo, se llama a _get_http() como parte de su declaración de muestra, para recuperar 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 subprocesos en segundo plano, debes usar la API de Firebase en lugar del SDK del servidor de Firebase directamente.

Primero, recupera las credenciales predeterminadas de la aplicación y, luego, inicializa el objeto UrlFetchTransport.

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

Luego, usa esa credencial para crear un objeto HttpRequestFactory autorizado con el servicio de recuperación de URL que App Engine requiere para las solicitudes HTTP salientes. 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();
        }
      }
    }

En los ejemplos anteriores, se usa la biblioteca httplib2 para Python y la biblioteca cliente HTTP de Google para Java a fin de manejar las solicitudes de REST. Sin embargo, puedes usar el método que prefieras para realizar las solicitudes de REST.

Genera tokens de autenticación de clientes

Firebase usa tokens web JSON (JWT) para autorizar a los usuarios. A fin de crear JWT, la app aprovecha el servicio integrado App Identity de App Engine para firmar las reclamaciones mediante las credenciales predeterminadas de la aplicación.

Con el siguiente método, se demuestra cómo crear JWT personalizados:

  1. El servicio App Identity se usa para obtener de manera automática 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 Firebase Authentication. La más importante de estas reclamaciones es uid, que es el identificador de canal único que restringe el acceso a un solo usuario.
  4. Por último, el servicio App Identity se usa de nuevo 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 los tokens a la plantilla que se va a procesar en el cliente. En esta app de tres en línea de ejemplo, el ID de usuario del jugador y la clave del juego incluyen el uid para la autorización de Firebase.

Autentica el cliente de JavaScript

Después de que los tokens se hayan generado en el servidor y pasado al HTML del cliente, este puede usarlos para autorizar los comandos de lectura y escritura. Solo es necesario autenticar al cliente una vez y, a continuación, se autenticarán todas las llamadas subsiguientes que usan el objeto firebase. Mediante el código de muestra, se 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. De manera más específica, restringe las operaciones de escritura al servidor y permite solo las operaciones de lectura cuando el ID único del usuario, uid, en la carga útil de autorización coincida 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
            }
          }
        }
    }
    

Haz una revisión general de la app de muestra

Para comprender mejor cómo usar Firebase a fin de compilar apps en tiempo real, consulta la app de muestra del juego tres en línea. El código fuente completo de la app de Python está disponible en la página de GitHub Fire Tac Toe. El código fuente completo de la app de Java está disponible en la página de GitHub 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 app actualiza las vistas de ambos jugadores del tablero, en tiempo real, en cuanto 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.

Conéctate 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 usa este token para abrir una conexión a Firebase y escuchar las actualizaciones. La ruta de acceso única que el usuario escucha 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.

Mediante el siguiente código del servidor, se crea una ruta de Firebase para la app 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 usa el token para escuchar la ruta /channels/[USER_ID + GAME_KEY]. Esto sucede en la función openChannel(). Se agrega un objeto de escucha para hacer un seguimiento de los cambios en el valor de la ruta. El método <reference>.on() de Firebase se usa para configurar una devolución de llamada que se ejecute en los cambios de valores 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 que ve el 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');
    }

Envía actualizaciones al servidor

Cuando el usuario realiza un movimiento, se llama al método moveInSquare(), que verifica que el movimiento sea válido y, luego, envía una solicitud POST de 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 el POST 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 mediante una llamada 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_UNAUTHORIZED);
        } 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 antes 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.

Quita objetos de escucha y borra 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 ello, llama al método off() en la referencia de Firebase que los clientes escucharon. 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);
      }
    }

Próximos pasos

  • Prueba otras funciones de Google Cloud. Consulta nuestros instructivos.