Como usar o Firebase nos eventos em tempo real no App Engine

Neste artigo, você aprende a conectar o App Engine ao Firebase. Além disso, você verá como usar o Firebase para enviar atualizações em tempo real de um jogo da velha interativo no modo multijogador. É possível conseguir o código de amostra no projeto do jogo da velha em Python ou Java.

Use o App Engine com o Firebase Realtime Database para enviar atualizações imediatas ao navegador e aos clientes de dispositivos móveis sem utilizar uma conexão persistente de streaming ao servidor ou realizar uma pesquisa longa. Esse recurso é útil para aplicativos que atualizam os usuários sobre novas informações em tempo real. Os casos de uso de exemplo incluem aplicativos colaborativos, jogos com vários participantes e salas de bate-papo.

Usar o Firebase é melhor do que realizar a pesquisa nos casos em que as atualizações não podem ser previstas ou colocadas em scripts. Por exemplo, ao redirecionar informações entre usuários humanos ou quando os eventos não são gerados sistematicamente.

Neste artigo, mostraremos como concluir as tarefas a seguir no servidor:

  • Configurar o servidor para usar o Firebase Realtime Database.
  • Criar uma referência exclusiva do banco de dados do Firebase para cada cliente Web que se conectar ao serviço.
  • Alterar dados em uma referência de banco de dados específica para enviar atualizações em tempo real a clientes Web.
  • Melhorar a segurança ao acessar mensagens, com a criação de tokens exclusivos para cada cliente Web e o uso das regras de segurança do banco de dados do Firebase.
  • Receber mensagens de clientes Web por meio de HTTP.
  • Excluir dados do jogo do banco de dados após o término da partida.

Neste artigo, também mostraremos como concluir as tarefas a seguir no navegador da Web:

  • Usar um token exclusivo do servidor para se conectar ao Firebase com segurança.
  • Atualizar a interface dinamicamente de acordo com as alterações na referência do banco de dados.
  • Enviar mensagens de atualização ao servidor para transmiti-las a clientes remotos.

Primeiros passos com o Firebase Realtime Database

Com o Firebase Realtime Database, é possível criar aplicativos avançados e colaborativos ao conceder acesso ao banco de dados diretamente do código do lado do cliente. Os dados são mantidos no local, e os eventos em tempo real continuam a ser ativados mesmo off-line. Isso garante ao usuário final uma experiência responsiva. Quando a conexão do dispositivo é restabelecida, o Realtime Database sincroniza as alterações nos dados locais com as atualizações remotas que ocorreram enquanto o cliente estava off-line. Ele também mescla quaisquer conflitos automaticamente.

Como criar um projeto do Firebase

  1. Crie uma conta do Firebase ou faça login em uma conta atual.

  2. Clique em Adicionar projeto.

  3. Digite um nome no campo Nome do projeto.

  4. Siga as etapas de configuração restantes e clique em Criar projeto.

  5. Depois que o assistente provisionar o projeto, clique em Continuar.

  6. Na página Visão geral do projeto, clique na engrenagem Configurações e em Configurações do projeto.

  7. Clique em Adicionar o Firebase ao app da Web.

  8. Faça uma cópia do snippet do código de inicialização, que é necessário na seção Como adicionar o SDK do Firebase à sua página da Web.

Como criar um banco de dados em tempo real

  1. No menu à esquerda do Console do Firebase, selecione Banco de dados no grupo Desenvolver.

  2. Na página Banco de dados, acesse a seção Realtime Database e clique em Criar banco de dados.

  3. Na caixa de diálogo Regras de segurança para o Realtime Database, selecione Iniciar no modo de teste e clique em Ativar.

Como trabalhar com dados no Firebase

Todos os dados do Firebase Realtime Database são armazenados como objetos JSON. Pense no banco de dados como uma árvore JSON hospedada na nuvem. Ao contrário de um banco de dados SQL, não há tabelas nem registros. Quando você adiciona dados à árvore JSON, eles se tornam um nó na estrutura JSON existente com uma chave associada. É possível fornecer suas próprias chaves, como códigos de usuário ou nomes semânticos.

Essa estrutura proporciona uma leitura de dados mais fácil porque os clientes só precisam navegar até um determinado caminho. Além disso, o objeto inteiro é retornado no formato JSON. Os campos do banco de dados não precisam de processamento especial. Para saber mais, veja como Estruturar o banco de dados na documentação do Firebase.

Os back-ends Java usam a API REST ou o SDK Admin do Firebase. Ao usar o SDK Admin do Firebase para Java no Google App Engine, verifique se está sendo utilizado o tempo de execução do Java 8. A versão 6.0.0 ou superior do SDK depende da compatibilidade da multissegmentação disponível no tempo de execução do Java 8 do App Engine.

Com a API REST, o Firebase usa os métodos PUT, PATCH, POST, GET e DELETE padrão de HTTP para executar operações do banco de dados. Para saber mais detalhes sobre como essas operações são usadas pelo Firebase, consulte Como salvar dados e Como recuperar dados na documentação do serviço.

Como gravar dados e enviar mensagens

Para realizar solicitações autenticadas, os aplicativos precisam recuperar Credenciais padrão do aplicativo do App Engine com os escopos de autorização necessários e usá-las para criar um objeto HTTP. As solicitações posteriores feitas com esse objeto incluem automaticamente os cabeçalhos Authorization necessários para fazer solicitações ao Firebase. Para saber mais informações sobre Credenciais padrão do aplicativo, consulte Como configurar a autenticação para aplicativos de produção de servidor para servidor.

É possível usar o método HTTP PUT para gravar ou substituir dados em um caminho específico do Firebase, conforme mostrado no exemplo a seguir:

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 implementações de amostra no GitHub para PATCH e POST, clique no botão Veja no GitHub no exemplo de código anterior.

Para ler os dados, faça uma solicitação GET de HTTP de um caminho específico. A resposta contém o objeto JSON no local solicitado.

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

Consulte Como realizar esses processos no aplicativo de amostra para ver exemplos com PATCH e DELETE.

Como detectar eventos em tempo real usando um navegador da Web

Para detectar eventos em tempo real, você precisa:

  • Adicionar o SDK do Firebase à sua página da Web.
  • Adicionar uma referência ao caminho do Firebase em que os dados estão armazenados.
  • Adicionar um listener.
  • Implementar uma função de retorno de chamada.

Adicionar o SDK do Firebase à sua página da Web

Para fazer isso:

Detectar alterações nos dados

Recupere os dados do Firebase ao anexar um listener assíncrono a um firebase.database.Reference. Ele é acionado uma vez no estado inicial dos dados e posteriormente quando há alterações. Há dois tipos principais de alteração que o cliente pode detectar:

  • Eventos value: leia e detecte alterações em todo o conteúdo de um caminho. Esses eventos costumam ser mais usados para monitorar alterações em um único objeto.
  • Eventos child: leia e detecte alterações em elementos filho em um caminho específico. Esses eventos costumam ser mais usados para monitorar alterações em listas de objetos. É possível detectar estes eventos child: child_added, child_changed, child_moved e child_removed.

Neste exemplo de aplicativo, mantemos um único objeto de estado de jogo sincronizado entre o servidor e o cliente. Por isso, este artigo enfatiza o uso de detecção de eventos value. Veja Recuperar dados na Web para saber informações sobre detectores de eventos child e técnicas mais avançadas de recuperação de dados.

O código a seguir mostra como detectar um evento de alteração value e adicionar uma função de retorno de chamada para execução quando o evento for acionado. Primeiro, o código cria uma referência do banco de dados, que é o caminho do objeto que seu app detectará. Em seguida, o método reference.on() adiciona o detector à referência. A função de retorno de chamada é transmitida como um argumento.

Neste exemplo, um aplicativo de jogo da velha em tempo real envia informações de jogo a clientes que realizam detecção em canais específicos, o que corresponde ao jogo do usuário. O aplicativo também atualiza a IU quando as informações são alteradas no banco de dados. A variável firebase é criada automaticamente pelo snippet do JavaScript, incluído em Como adicionar o Firebase a uma página da 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());
});

A implementação da função de retorno de chamada, onMessage(), ainda será abordada neste arquivo.

Remover detectores

Chame o método off() da sua referência do banco de dados do Firebase para remover retornos de chamada.

Como publicar eventos de volta ao App Engine

Atualmente, o App Engine não é compatível com conexões HTTP de fluxo bidirecional. Caso o cliente precise atualizar o servidor, será necessário enviar uma solicitação HTTP explícita.

Como restringir o acesso aos seus dados

No Firebase, é possível usar tokens de autorização para restringir o acesso de usuários específicos aos dados. Recomendamos que apenas o seu servidor tenha acesso de gravação, enquanto autoriza os clientes da Web para acesso somente de leitura. O código abaixo demonstra como:

  • gerar credenciais para seu servidor do App Engine;
  • gerar tokens para cada cliente;
  • transmitir tokens aos clientes da Web que possam ser usados para ler dados.

Como estabelecer comunicação entre o App Engine e o Firebase

Use o Application Default Credentials integrado ao App Engine para fazer chamadas REST autenticadas ao banco de dados do Firebase a partir do servidor do App Engine.

Python

Com os escopos apropriados para conceder acesso ao Firebase, crie a instância de um objeto GoogleCredentials fornecido pelo pacote oauth2client. Em seguida, use esse objeto para autorizar um 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

Ele inclui automaticamente o token de autenticação adequado. Use diretamente o objeto httplib2.Http para fazer chamadas REST. A função a seguir usa o objeto retornado de _get_http() para enviar uma solicitação PATCH ou DELETE. Este exemplo chama _get_http() como parte de sua instrução de retorno para recuperar o 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

O App Engine restringe a criação de threads de segundo plano. Por isso, é necessário usar a API Firebase em vez do SDK do servidor do Firebase diretamente.

Primeiro, recupere o Application Default Credentials e inicialize o objeto UrlFetchTransport.

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

Em seguida, use a credencial para criar um objeto HttpRequestFactory autorizado. Para isso, utilize o serviço de busca de URL exigido pelo App Engine em solicitações HTTP de saída. Para fazer chamadas ao banco de dados do Firebase, crie um caminho e realize a solicitação:

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

Os exemplos acima usam a biblioteca httplib2 para Python e a biblioteca de cliente HTTP do Google para Java para processar as solicitações REST. No entanto, é possível usar o método da sua preferência para executar essas solicitações.

Como gerar os tokens de autenticação do cliente

O Firebase usa JSON Web Tokens (JWTs) para autorizar usuários. Para criar JWTs, o aplicativo aproveita o serviço App Identity incorporado do App Engine para assinar as declarações usando o Application Default Credentials.

No método a seguir, mostramos como criar JWTs personalizados:

  1. O serviço App Identity é usado para recuperar automaticamente o e-mail da conta de serviço.
  2. O cabeçalho do token é criado.
  3. As declarações são incluídas no payload do token de acordo com a documentação de autenticação do Firebase. A mais importante delas é uid, o identificador de canal exclusivo que restringe o acesso a um único usuário.
  4. Por último, o serviço App Identity é usado novamente para sinalizar o 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()));
}

O servidor executa a função acima e transmite os tokens para o modelo a fim de renderizá-los no cliente. Neste exemplo de aplicativo de jogo da velha, o código de usuário do jogador e a chave do jogo consistem no uid para autorização do Firebase.

Como autenticar o cliente JavaScript

Use os tokens para autorizar os comandos de leitura e gravação pelo cliente. Isso é possível após gerar os tokens no servidor e transmiti-los no HTML cliente. É necessário autenticar o cliente apenas uma vez. Todas as chamadas posteriores usando o objeto firebase serão autenticadas. O código de amostra exemplifica como isso é feito:

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

Como atualizar as regras de segurança do banco de dados do Firebase

Na seção Banco de dados do console do Firebase, clique na guia Regras. Atualize as regras de segurança do Firebase para restringir o acesso apenas para conexões autorizadas. Melhor dizendo, restrinja gravações no servidor e permita leituras apenas quando uid, o código exclusivo do usuário, for igual no payload de autenticação e no caminho do banco de dados.

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

Como realizar esses processos no app de amostra

Para ilustrar melhor como usar o Firebase na criação de aplicativos em tempo real, veja o exemplo de aplicativo de jogo da velha. O código-fonte completo do aplicativo Python está disponível na página Fire Tac Toe do GitHub. Você também encontra o código-fonte completo do aplicativo Java na mesma página.

No jogo, os usuários criam uma partida, convidam outro jogador enviando um URL e jogam em tempo real. O aplicativo atualiza as visualizações do tabuleiro para ambos os jogadores em tempo real assim que um usuário faz uma jogada.

O resto do artigo analisa os principais destaques das funcionalidades mais importantes.

Como conectar a um caminho do Firebase

Quando um usuário visita o jogo pela primeira vez, duas ações são realizadas:

  • O servidor do jogo injeta um token na página HTML enviada ao cliente. Ele usa esse token para abrir uma conexão ao Firebase e detectar atualizações. O caminho exclusivo que o usuário detecta é /channels/[USER_ID + GAME_KEY].
  • O servidor do jogo fornece um URL ao usuário. Ele é compartilhado com um amigo para convidá-lo a participar do jogo.

O código de servidor Python a seguir cria o caminho do Firebase para o aplicativo do jogo e o injeta no HTML do 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);

Agora, o cliente Web usa o token para detectar o caminho /channels/[USER_ID + GAME_KEY]. Isso acontece na função openChannel(). É adicionado um listener para acompanhar as alterações no valor do caminho. Nesse caso, o método <reference>.on() do Firebase é usado para configurar um retorno de chamada, executado nas alterações de valor. Quando o valor do caminho do banco de dados sofre alterações, o método onMessage() é chamado para atualizar o estado do jogo e a IU para o usuário.

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

Como enviar atualizações ao servidor

Quando um usuário faz uma jogada, moveInSquare() é chamado. Esse método verifica se a jogada é válida e envia uma solicitação POST de HTTP ao servidor com a localização do movimento.

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

O servidor recebe o POST de HTTP, e o gerenciador do caminho /move é chamado. Ele identifica o usuário e extrai da solicitação a localização do quadrado. Em seguida, o gerenciador chama o método no objeto Game para realizar o movimento:

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

O método que realiza o movimento conclui uma rodada final de validação na jogada do usuário, verifica se ele ganhou o jogo e grava o estado do jogo no armazenamento de dados. Por fim, ele chama um método para atualizar o estado do jogo no 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;
}

O método de atualização chama o método que você definiu acima para realizar a solicitação HTTP com segurança no 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 fim, ele realiza a solicitação 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();
    }
  }
}

Após essa chamada para o Firebase, os clientes Web recebem imediatamente o estado de jogo atualizado, sem realizar a pesquisa do servidor.

Como remover listeners e excluir dados

Quando um usuário ganha o jogo, o servidor não tem mais informações para enviar. Nesse caso, você precisa remover os detectores e excluir os dados do jogo do Firebase, porque o armazenamento não é necessariamente gratuito. Para fazer isso, chame o método off() na referência do Firebase detectada pelos clientes. Por fim, a mensagem do cliente é enviada ao endpoint /delete do servidor, e os dados armazenados do jogo encerrado são removidos.

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

Acione o gerenciador do ponto de extremidade /delete para excluir dados no Firebase:

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óximas etapas

  • Teste outros recursos do Google Cloud Platform. Veja nossos tutoriais.
Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…