Utiliser Firebase pour des événements en temps réel sur App Engine

Cet article explique comment connecter App Engine à Firebase, puis comment exploiter Firebase pour envoyer des mises à jour en temps réel pour un jeu de morpion (en anglais, "tic-tac-toe") multijoueur interactif. Vous pouvez obtenir le code d'exemple du projet pour Python ou Java.

Vous pouvez utiliser App Engine conjointement avec Firebase Realtime Database pour envoyer des mises à jour immédiates aux navigateurs et aux clients mobiles sans nécessiter de connexion persistante en flux continu au serveur ou de scrutation de longue durée. Cette fonctionnalité est utile pour les applications qui envoient aux utilisateurs des mises à jour relatives à de nouvelles informations en temps réel. Des applications collaboratives, des jeux multijoueurs et des salons de discussion constituent quelques exemples de cas d'utilisation.

L'utilisation de Firebase est un meilleur choix que la scrutation dans les situations où les mises à jour ne peuvent pas être prédites ou scriptées, par exemple lorsqu'il y a transmission d'informations entre des utilisateurs humains ou lorsque des événements ne sont pas générés de façon systématique.

Cet article explique comment réaliser les tâches suivantes sur le serveur :

  • Configurer votre serveur pour utiliser Firebase Realtime Database.
  • Créer une référence de base de données Firebase unique pour chaque client Web qui se connecte à votre service.
  • Envoyer des mises à jour en temps réel aux clients Web en modifiant les données d'une référence de base de données particulière.
  • Améliorer la sécurité lors de l'accès aux messages en créant des jetons uniques pour chaque client Web et en utilisant les règles de sécurité de la base de données Firebase.
  • Recevoir les messages des clients Web via HTTP.
  • Lorsque la partie est terminée, supprimer ses données dans la base de données.

Cet article explique également comment réaliser les tâches suivantes dans le navigateur Web :

  • Se connecter à Firebase en utilisant un jeton unique en provenance du serveur.
  • Mettre à jour dynamiquement l'interface lorsque la référence de base de données est mise à jour.
  • Envoyer des messages de mise à jour au serveur afin qu'ils soient transmis aux clients distants.

Premiers pas avec Firebase Realtime Database

La base de données Firebase Realtime Database vous permet de créer des applications collaboratives riches en autorisant l'accès à la base de données directement depuis le code côté client. Les données sont conservées localement et les événements en temps réel continuent de se déclencher même en mode hors connexion, offrant ainsi une expérience dynamique à l'utilisateur final. Lorsque l'appareil rétablit sa connexion, Realtime Database synchronise les modifications de données locales avec les mises à jour distantes survenues pendant que le client était hors ligne, en gérant automatiquement tous les conflits.

Créer un projet Firebase

  1. Créez un compte Firebase ou connectez-vous à un compte existant.

  2. Cliquez sur Ajouter un projet.

  3. Saisissez un nom dans le champ Nom du projet.

  4. Suivez les autres étapes d'installation, puis cliquez sur Créer un projet.

  5. Une fois que l'assistant a provisionné votre projet, cliquez sur Continuer.

  6. Sur la page Présentation de votre projet, cliquez sur l'icône Paramètres (en forme de roue dentée), puis sur Paramètres du projet.

  7. Cliquez sur Ajouter Firebase à votre application Web.

  8. Faites une copie de l'extrait de code d'initialisation comme requis dans la section Ajouter le SDK Firebase à votre page Web.

Créer une base de données Realtime Database

  1. Dans le menu de gauche de la console Firebase, sélectionnez Base de données dans le groupe Développer.

  2. Sur la page Base de données, accédez à la section Realtime Database, puis cliquez sur Créer une base de données.

  3. Dans la boîte de dialogue Règles de sécurité pour Realtime Database, sélectionnez Commencer en mode test, puis cliquez sur Activer.

Travailler avec des données dans Firebase

Toutes les données de Firebase Realtime Database sont stockées en tant qu'objets JSON. Vous pouvez considérer la base de données comme une arborescence JSON hébergée dans le cloud. Contrairement à une base de données SQL, il n'y a ni tables, ni enregistrements. Lorsque vous ajoutez des données à l'arborescence JSON, elles prennent la forme d'un nouveau nœud et d'une clé associée dans la structure JSON existante. Vous pouvez fournir vos propres clés, par exemple des identifiants d'utilisateur ou des noms sémantiques.

Cette structure facilite la lecture des données, car les clients ont uniquement besoin d'accéder à un chemin donné et l'objet entier leur est renvoyé au format JSON. Cela ne requiert aucun traitement spécial des champs de la base de données. Pour plus d'informations, découvrez comment structurer votre base de données dans la documentation de Firebase.

Les backends Java peuvent utiliser l'API REST ou le SDK d'administration Firebase. Si vous utilisez le SDK d'administration Firebase pour Java sur App Engine, veillez à bien utiliser l'environnement d'exécution Java 8. Les versions 6.0.0 et ultérieures du SDK dépendent de la compatibilité avec le multithreading disponible dans l'environnement d'exécution App Engine Java 8.

Dans le cadre de l'API REST, Firebase utilise les méthodes HTTP standards PUT, PATCH, POST, GET et DELETE pour effectuer des opérations en base de données. Pour plus d'informations sur l'utilisation de ces opérations par Firebase, consultez les pages Enregistrer des données et Récupérer des données dans la documentation de Firebase.

Écrire des données et envoyer des messages

Pour exécuter des requêtes authentifiées, les applications doivent extraire les identifiants par défaut de l'application depuis App Engine avec les champs d'application d'autorisation requis, puis les utiliser pour créer un objet HTTP. Les requêtes effectuées à l'aide de cet objet HTTP incluent automatiquement les en-têtes d'Authorization nécessaires pour adresser des requêtes à Firebase. Pour plus d'informations sur les identifiants par défaut des applications, consultez la documentation Configurer l'authentification pour des applications de production serveur à serveur.

Vous pouvez utiliser la méthode HTTP PUT pour écrire ou remplacer des données enregistrées dans un chemin Firebase spécifique, comme indiqué dans l'exemple suivant :

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();
}

Pour voir des exemples sur GitHub mettant en œuvre les méthodes PATCH et POST, cliquez sur le bouton Afficher sur GitHub dans l'exemple de code précédent.

Pour lire des données, envoyez une requête HTTP GET pour un chemin donné. La réponse contient l'objet JSON stocké à l'emplacement demandé.

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();
}

Pour voir des exemples utilisant PATCH et DELETE, reportez-vous à la section Assembler toutes les briques dans l'exemple d'application.

Écouter des événements en temps réel depuis un navigateur Web

Pour écouter des événements en temps réel, vous devez :

  • ajouter le SDK Firebase à votre page Web ;
  • ajouter une référence au chemin Firebase où vos données sont stockées ;
  • ajouter un écouteur ;
  • mettre en œuvre une fonction de rappel.

Ajouter le SDK Firebase à votre page Web

Pour ajouter le SDK Firebase à votre page Web :

Écouter les changements dans les données

Récupérez les données Firebase en rattachant un écouteur asynchrone à un objet firebase.database.Reference. L'écouteur est déclenché une fois pour l'état initial des données, puis chaque fois que les données changent. Le client peut écouter deux principaux types de modifications :

  • Événements de valeur : lire et écouter les modifications apportées à l'ensemble du contenu d'un chemin. Les événements de valeur sont le plus souvent utilisés pour surveiller les modifications apportées à un seul objet.
  • Événements enfants : lire et écouter les modifications apportées aux éléments enfants d'un chemin particulier. Les événements enfants sont le plus souvent utilisés pour surveiller les modifications dans les listes d'objets. Il est possible d'écouter les événements enfants suivants : child_added, child_changed, child_moved et child_removed.

Cet exemple d'application permet de synchroniser un objet unique d'état de jeu entre le serveur et le client. De ce fait, cet article se concentre donc sur l'utilisation d'un système d'écoute centrée sur les événements de valeur. Des écouteurs d'événements enfants ainsi que des techniques plus avancées de récupération de données sont décrits dans Récupérer des données sur le Web.

Le code suivant montre comment écouter un événement de modification de valeur et ajouter une fonction de rappel à exécuter lorsque l'événement est déclenché. Le code crée d'abord une référence de base de données qui correspond au chemin d'accès à l'objet écouté par votre application. Ensuite, l'écouteur est ajouté à la référence à l'aide de la méthode reference.on(), où la fonction de rappel est transmise en tant qu'argument.

Dans cet exemple, une application de morpion en temps réel envoie des informations de jeu aux clients écoutant des canaux particuliers, qui correspondent à la partie de l'utilisateur. Elle met à jour l'interface utilisateur lorsque ces informations changent dans la base de données. La variable firebase est automatiquement créée par l'extrait de code JavaScript ajouté dans la section Ajouter Firebase à votre page Web.

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 mise en œuvre de la fonction de rappel, onMessage(), est décrite plus loin dans cet article.

Détacher les écouteurs

Vous pouvez supprimer les fonctions de rappel en appelant la méthode off() de votre référence de base de données Firebase.

Enregistrer des événements dans App Engine

À l'heure actuelle, App Engine n'est pas compatible avec les connexions HTTP en flux continu bidirectionnel. Si un client doit mettre à jour le serveur, il doit envoyer une requête HTTP explicite.

Restreindre l'accès à vos données

Firebase vous permet de restreindre l'accès aux données à des utilisateurs spécifiques à l'aide de jetons d'autorisation. Nous vous recommandons d'accorder l'accès en écriture à votre serveur uniquement, et de limiter les clients Web à un accès en lecture seulement. Le code ci-dessous montre comment :

  • générer des informations d'identification pour votre serveur App Engine ;
  • générer des jetons pour chaque client ;
  • transmettre aux clients Web les jetons qu'ils peuvent alors utiliser pour lire les données.

Établir la communication entre App Engine et Firebase

Vous pouvez utiliser les identifiants par défaut de l'application intégrés à App Engine pour effectuer des appels REST authentifiés vers votre base de données Firebase depuis votre serveur App Engine.

Python

Instanciez un objet GoogleCredentials, fourni par le package oauth2client, avec les champs d'application adéquats pour accorder l'accès à Firebase. Utilisez ensuite cet objet pour autoriser un objet 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

Cet objet inclut automatiquement le jeton d'authentification approprié et vous pouvez utiliser l'objet httplib2.Http directement pour effectuer des appels REST. La fonction suivante utilise l'objet renvoyé par _get_http() pour envoyer une requête PATCH ou DELETE. Notez que cet exemple appelle la méthode _get_http() dans le cadre de son instruction return pour récupérer l'objet HTTP autorisé :

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

Étant donné qu'App Engine limite la possibilité de créer des threads d'arrière-plan, vous devez utiliser directement l'API Firebase au lieu du SDK Firebase Server.

Pour commencer, récupérez les identifiants par défaut de l'application et initialisez l'objet UrlFetchTransport.

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

Ensuite, utilisez ces identifiants pour créer un objet HttpRequestFactory autorisé, à l'aide du service de récupération d'URL requis par App Engine pour les requêtes HTTP sortantes. Pour envoyer des appels vers la base de données Firebase, vous devez créer un chemin et exécuter la requête :

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();
    }
  }
}

Les exemples ci-dessus utilisent la bibliothèque httplib2 pour Python et la bibliothèque cliente HTTP Google pour Java pour gérer les requêtes REST. Toutefois, vous pouvez utiliser la méthode de votre choix pour exécuter les requêtes REST.

Générer le(s) jeton(s) d'authentification du client

Firebase utilise des jetons Web JSON (JWT) pour autoriser les utilisateurs. Pour créer des jetons JWT, l'application tire parti du service intégré App Identity de App Engine pour signer les revendications à l'aide des identifiants par défaut de l'application.

La méthode suivante montre comment créer des jetons JWT personnalisés :

  1. Le service App Identity est utilisé pour récupérer automatiquement l'e-mail du compte de service.
  2. L'en-tête du jeton est construit.
  3. Les revendications sont ajoutées à la charge utile du jeton, comme décrit dans la documentation sur l'authentification Firebase. La plus importante de ces revendications est uid, qui correspond à l'identifiant unique du canal restreignant l'accès à un utilisateur unique.
  4. Enfin, le service App Identity est à nouveau utilisé pour signer le jeton.

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()));
}

Le serveur exécute la fonction précédente et transmet les jetons au modèle pour restitution sur le client. Dans cet exemple d'application de morpion, l'ID utilisateur et la clé de partie correspondant au joueur comportent l'uid d'autorisation Firebase.

Authentifier le client JavaScript

Une fois que les jetons ont été générés sur le serveur et transmis dans le code HTML envoyé au client, ils peuvent être utilisés pour autoriser le client à effectuer des commandes de lecture et d'écriture. Une seule authentification du client est nécessaire, tous les appels ultérieurs réalisés à l'aide de l'objet firebase sont alors authentifiés. L'exemple de code illustre la mise en œuvre :

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);
});

Mettre à jour les règles de sécurité de la base de données Firebase

Dans la section Base de données de la console Firebase, sélectionnez l'onglet Règles. Mettez à jour les règles de sécurité de Firebase pour limiter l'accès aux seules connexions autorisées. Plus spécifiquement, limitez les droits d'écriture au serveur seulement et autorisez les lectures uniquement quand l'identifiant unique de l'utilisateur, uid, qui apparaît dans la charge utile d'authentification, correspond à l'identifiant unique figurant dans le chemin de la base de données.

{
    "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
        }
      }
    }
}

Assembler toutes les briques dans l'exemple d'application

Pour mieux illustrer l'utilisation de Firebase pour créer des applications en temps réel, consultez l'exemple d'application de jeu de morpion. Le code source complet de l'application en version Python est disponible sur la page GitHub Fire Tac Toe. Le code source complet de l'application en version Java est disponible sur la page GitHub Fire Tac Toe.

L'application permet aux utilisateurs de créer un jeu, d'inviter un autre joueur par l'envoi d'une URL et de jouer ensemble en temps réel. L'application met à jour les vues du plateau pour les deux joueurs, en temps réel, dès que l'un des joueurs intervient.

Le reste de cet article passe en revue les principaux points forts illustrant les fonctionnalités essentielles.

Se connecter à un chemin Firebase

Lorsqu'un utilisateur consulte le jeu pour la première fois, il se produit deux événements :

  • Le serveur de jeu injecte un jeton dans la page HTML envoyée au client. Le client utilise ce jeton pour ouvrir une connexion à Firebase et écouter les mises à jour. Le chemin unique que l'utilisateur écoute est /channels/[USER_ID + GAME_KEY].
  • Le serveur de jeu fournit à l'utilisateur une URL qu'il peut partager avec un ami afin de l'inviter à rejoindre la partie.

Le code côté serveur figurant ci-dessous crée le chemin Firebase pour l'application de jeu et l'injecte dans le code HTML envoyé au client :

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);

À partir de maintenant, le client Web utilise le jeton pour écouter le chemin /channels/[USER_ID + GAME_KEY]. Cela intervient dans la fonction openChannel(). Un écouteur est ajouté pour suivre les modifications de la valeur associée au chemin. La méthode <reference>.on() de Firebase est utilisée pour configurer un rappel qui, dans le cas présent, s'exécute en cas de modification de valeur. Lorsque la valeur associée au chemin de la base de données change, la méthode onMessage() est appelée pour mettre à jour l'état de la partie et l'interface de l'utilisateur.

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');
}

Envoyer des mises à jour vers le serveur

Lorsque l'utilisateur joue, la méthode moveInSquare() est appelée. Elle vérifie que le coup est valide, puis envoie une requête HTTP POST au serveur avec l'emplacement du coup joué.

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});
  }
}

Le serveur reçoit la requête HTTP POST et le gestionnaire associé au chemin /move est appelé. Ce gestionnaire identifie l'utilisateur et extrait de la requête l'emplacement de la case. Il concrétise ensuite le coup joué en appelant la méthode sur l'objet 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();
    }
  }
}

La méthode qui réalise le coup effectue un dernier tour de validation sur le coup joué par l'utilisateur, vérifie si l'utilisateur a gagné la partie et écrit l'état de la partie dans le magasin de données. Enfin, elle appelle une méthode pour mettre à jour l'état de la partie dans 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;
}

La méthode de mise à jour appelle ensuite la méthode que vous avez définie ci-dessus pour exécuter la requête HTTP vers 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);
}

Pour conclure, elle envoie la requête 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();
    }
  }
}

Après cet appel à Firebase, les clients Web reçoivent immédiatement l'état de la partie mis à jour sans avoir besoin d'interroger le serveur.

Supprimer des écouteurs et supprimer des données

Une fois qu'un joueur a gagné la partie, le serveur n'a plus besoin d'envoyer la moindre information supplémentaire. Vous devez alors supprimer les écouteurs ainsi que les données de jeu dans Firebase, car le stockage n'est pas nécessairement gratuit. Pour ce faire, appelez la méthode off() sur la référence Firebase écoutée par les clients. Enfin, le client envoie un message vers le point de terminaison /delete du serveur, ce qui supprime les données enregistrées pour la partie qui vient de s'achever.

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');
}

Supprimez des données dans Firebase en cliquant sur le gestionnaire du point de terminaison /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);
  }
}

Étape suivante

  • Testez d'autres fonctionnalités de Google Cloud Platform. Découvrez nos tutoriels.
Cette page vous a-t-elle été utile ? Évaluez-la :

Envoyer des commentaires concernant…