在 App Engine 上使用 Firebase 进行实时事件处理

本文介绍如何将 App EngineFirebase 关联,然后使用 Firebase 发送多人互动游戏 tic-tac-toe 的实时更新。您可以在 PythonJava 版 Tic-tac-toe 项目中获取示例代码。

将 App Engine 与 Firebase 实时数据库配合使用即可向浏览器和移动客户端发送即时更新,而无需建立与服务器的永久性流式连接或长轮询。对于需要实时更新用户新信息的应用,此功能非常有用。用例示例包括协作应用、多人游戏和聊天室。

在无法预测更新或无法编写更新脚本的情况下(例如在真人用户之间中继信息时,或者未系统地生成事件时),使用 Firebase 是一种比轮询更好的选择。

本文介绍如何在服务器上完成以下任务:

  • 设置服务器以使用 Firebase 实时数据库。
  • 为连接到您的服务的每个网页客户端创建一个唯一的 Firebase 数据库引用。
  • 通过更改特定数据库引用的数据,将实时更新发送到网页客户端。
  • 通过为每个网页客户端创建唯一令牌并使用 Firebase 数据库安全规则,提高访问消息时的安全性。
  • 通过 HTTP 从网页客户端接收消息。
  • 比赛结束后从数据库中删除游戏数据。

本文还介绍了如何在网络浏览器中完成以下任务:

  • 使用服务器中的唯一令牌连接到 Firebase。
  • 在数据库引用更新时动态更新接口。
  • 将更新消息发送到服务器,以便将它们传递给远程客户端。

Firebase 实时数据库入门

Firebase 实时数据库允许从客户端代码中直接访问数据库,因此您能够构建功能丰富的协作应用。数据会持久地保留在本机,即使处于离线状态,实时事件仍会继续触发,从而为最终用户带来响应灵敏的体验。当设备重新连接到网络时,实时数据库会使用客户端离线期间发生的远程更新同步本地数据更改,以自动合并任何不一致的数据。

创建 Firebase 项目

  1. 创建 Firebase 帐号或登录现有帐号。

  2. 点击添加项目

  3. 项目名称字段中输入一个名称。

  4. 按照其余设置步骤操作,然后点击创建项目

  5. 向导预配项目后,点击继续

  6. 在项目的概览页面中,点击设置齿轮,然后点击项目设置

  7. 点击将 Firebase 添加到您的网页应用

  8. 复制初始化代码段,在将 Firebase SDK 添加到网页部分需要此代码段。

创建实时数据库

  1. Firebase 控制台的左侧菜单中,选择开发组中的数据库

  2. 数据库页面中,转到实时数据库部分,然后点击创建数据库

  3. 实时数据库的安全规则 (Security rules for Realtime Database) 对话框中,选择以测试模式启动 (Start in test mode),然后点击启用

使用 Firebase 中的数据

所有 Firebase 实时数据库数据都会存储为 JSON 对象。您可将该数据库视为托管在云端的 JSON 树。该数据库与 SQL 数据库不同,没有表格或记录的概念。当您将数据添加至 JSON 树时,它会变为现有 JSON 结构中的一个节点,并带有一个关联的键。您可以自行提供键,例如用户 ID 或语义名称。

此结构可使读取数据变得简单,因为客户端只需导航到给定路径,整个对象就会以 JSON 格式返回。无需对数据库字段进行任何特殊处理。如需了解详情,请参阅 Firebase 文档中的如何设计数据库的结构

Java 后端可以使用 REST API 或 Firebase Admin SDK。如果您在 App Engine 上使用 Java 版 Firebase Admin SDK,请确保使用 Java 8 运行时。SDK 6.0.0 版以及更高版本依赖于 App Engine Java 8 运行时中提供的多线程支持。

使用 REST API 时,Firebase 会处理标准 HTTP PUTPATCHPOSTGETDELETE 方法,以执行数据库操作。如需详细了解 Firebase 如何使用这些操作,请参阅 Firebase 文档中的保存数据检索数据

写入数据和发送消息

要执行经过身份验证的请求,应用必须从 App Engine 检索具有所需授权范围的应用默认凭据,并用其创建 HTTP 对象。使用该 HTTP 对象发出的请求会自动包含向 Firebase 发出请求所需的 Authorization 标头。如需详细了解应用默认凭据,请参阅为服务器到服务器的生产应用设置身份验证

您可以使用 PUT HTTP 方法在特定 Firebase 路径写入或替换数据,如以下示例所示:

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

要在 GitHub 上查看 PATCHPOST 的实现示例,请点击上一代码示例中的在 GitHub 上查看按钮。

要读取数据,请发出 HTTP GET 请求以获得特定路径。响应中包含所请求的位置的 JSON 对象。

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

如需查看使用 PATCHDELETE 的示例,请参阅在示例应用中进行综合应用

从网络浏览器侦听实时事件

要侦听实时事件,您需要执行以下操作:

  • 将 Firebase SDK 添加到您的网页。
  • 添加对存储数据的 Firebase 路径的引用。
  • 添加侦听器。
  • 实现回调函数。

将 Firebase SDK 添加到您的网页

要将 Firebase SDK 添加到您的网页,请执行以下操作:

侦听数据更改

通过将异步侦听器挂接到 firebase.database.Reference 来检索 Firebase 数据。该侦听器会针对数据的初始状态触发一次,以后只要数据有更改就会再次触发。客户端可以侦听以下两种主要的更改类型:

  • 值事件:读取并侦听对路径中所有内容的更改。值事件最常用于监控单个对象的更改。
  • 子事件:读取并侦听对特定路径下子元素的更改。子事件最常用于监控对象列表中的更改。可以侦听以下子事件:child_addedchild_changedchild_movedchild_removed

此示例应用可将单个游戏状态对象在服务器和客户端之间保持同步。因此,本文重点介绍侦听值事件。在网络上检索数据中介绍了子事件侦听器以及更高级的数据检索技术。

以下代码展示了如何侦听值更改事件并添加回调函数,以在触发事件时执行任务。代码首先会创建一个数据库引用,作为应用侦听的对象的路径。接下来,代码会使用 reference.on() 方法将侦听器添加到该引用中,其中回调函数作为参数传递。

在此示例中,实时 tic-tac-toe 应用会向侦听用户游戏对应的特定通道的客户端发送游戏信息,并在此信息在数据库中发生更改时更新 UI。在将 Firebase 添加到您的网页这一步中添加的 JavaScript 代码段会自动创建变量 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());
});

本文后面部分将介绍回调函数 onMessage() 的实现。

分离侦听器

可以通过调用 Firebase 数据库引用的 off() 方法来移除回调。

将事件发布回 App Engine

App Engine 目前不支持双向流式 HTTP 连接。如果客户端需要更新服务器,则必须发送显式 HTTP 请求。

限制对数据的访问权限

借助 Firebase,您可以使用授权令牌将对数据的访问权限限制为特定用户。我们建议仅向您的服务器授予写入权限,同时向网络客户端授予只读权限。下方代码演示了如何:

  • 为 App Engine 服务器生成凭据。
  • 为每个客户端生成令牌。
  • 将令牌传递给可用于读取数据的网络客户端。

在 App Engine 与 Firebase 之间建立通信

您可以使用 App Engine 的内置应用默认凭据从 App Engine 服务器对 Firebase 数据库进行经过身份验证的 REST 调用。

Python

使用适当的范围实例化 oauth2client 软件包提供的 GoogleCredentials 对象,以授予对 Firebase 的访问权限。然后,使用该对象向 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

该对象自动包含正确的身份验证令牌,您可以直接使用 httplib2.Http 对象进行 REST 调用。以下函数使用从 _get_http() 返回的对象发送 PATCHDELETE 请求。请注意,此示例在返回语句中调用 _get_http(),以检索已获授权的 HTTP 对象:

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

由于 App Engine 限制了创建后台线程的能力,因此您需要使用 Firebase API 而不是直接使用 Firebase Server SDK

首先,检索应用默认凭据并初始化 UrlFetchTransport 对象。

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

然后,使用该凭据创建已获授权的 HttpRequestFactory 对象,从而使用 App Engine 所需的网址提取服务发出出站 HTTP 请求。要调用 Firebase 数据库,请构建路径并执行请求:

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

以上示例使用 Python 版 httplib2 库Java 版 Google HTTP 客户端库处理 REST 请求。但您可以使用您的首选方法执行 REST 请求。

生成客户端身份验证令牌

Firebase 使用 JSON 网络令牌 (JWT) 授权用户。为创建 JWT,应用利用 App Engine 的内置 App Identity 服务,通过应用默认凭据对声明进行签名。

以下方法演示了如何创建自定义 JWT:

  1. 使用 App Identity 服务自动检索服务帐号电子邮件。
  2. 构造令牌的标头。
  3. 根据 Firebase 身份验证文档,将声明添加到令牌负载。这些声明中最重要的是 uid,它是将访问权限限制为单个用户的唯一渠道标识符。
  4. 最后,再次使用 App Identity 服务对令牌进行签名。

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

服务器执行前面的函数并将令牌传递到要在客户端上呈现的模板中。在此示例 tic-tac-toe 应用中,玩家的用户 ID 和游戏密钥包含用于 Firebase 授权的 uid

对 JavaScript 客户端进行身份验证

在服务器上生成令牌并将其传递到客户端 HTML 之后,这些令牌可用于授权客户端的读取和写入命令。只需对客户端进行一次身份验证,即可对使用 firebase 对象的所有后续调用进行身份验证。以下示例代码演示了具体方法:

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

更新 Firebase 数据库安全规则

在 Firebase 控制台的数据库部分,选择规则 (Rules) 标签。现在,更新 Firebase 的安全规则,将访问权限仅限于已获授权的连接。具体来说,限制写入服务器,并且仅当身份验证负载中的用户唯一 ID uid 与数据库路径中的唯一 ID 匹配时才允许读取。

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

在示例应用中进行综合应用

为了更好地演示如何使用 Firebase 构建实时应用,请查看示例 tic-tac-toe 游戏应用。如需查看 Python 应用的完整源代码,请访问 Fire Tac Toe GitHub 页面。如需查看 Java 应用的完整源代码,请访问 Fire Tac Toe GitHub 页面。

该游戏允许用户创建游戏、通过发送网址邀请其他玩家,以及一起实时玩游戏。在另一玩家走棋后,该应用会实时更新这两个玩家的棋盘视图。

本文的其余部分将回顾说明关键功能的要点。

连接到 Firebase 路径

当用户首次访问游戏时,会发生两件事情:

  • 游戏服务器会将令牌注入到发送给客户端的 HTML 页面中。客户端使用此令牌打开与 Firebase 的连接并侦听更新。用户侦听的唯一路径是 /channels/[USER_ID + GAME_KEY]
  • 游戏服务器会向用户提供一个网址,他们可将该网址与好友分享,以便邀请他们加入游戏。

以下服务器端代码创建了游戏应用的 Firebase 路径,并将该路径注入到客户端的 HTML 中:

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

现在,网络客户端使用令牌来侦听路径 /channels/[USER_ID + GAME_KEY]。此情况发生在 openChannel() 函数中。添加侦听器以跟踪路径值的更改。在本例中,使用 Firebase 中的 <reference>.on() 方法设置在发生值更改时执行的回调。当数据库路径的值发生更改时,调用 onMessage() 方法,以更新用户的游戏状态和 UI。

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

将更新发送到服务器

在用户走棋时,将调用方法 moveInSquare(),该方法会检查此走棋是否有效,然后向包含走棋位置的服务器发送 HTTP POST 请求。

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

服务器接收 HTTP POST,并且会调用 /move 路径的处理程序。此处理程序标识用户并从请求中提取方格的位置。然后,它通过对 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();
    }
  }
}

执行走棋的方法会在用户走棋后完成最后一轮验证,检查用户是否赢得了游戏,并将游戏状态写入数据存储区中。最后,它会调用一种方法来更新 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;
}

然后,更新方法会调用上面定义的方法,以执行对 Firebase 的 HTTP 请求:

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

最后,它将发出 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();
    }
  }
}

在对 Firebase 进行此调用之后,网络客户端会立即收到已更新的游戏状态,而无需轮询服务器。

移除侦听器并删除数据

在某个玩家赢得游戏后,服务器不再发送额外信息。在此情况下,您需要移除侦听器并从 Firebase 中删除游戏数据,因为存储可能会收费。为此,请对客户端一直侦听的 Firebase 引用调用 off() 方法。最后,客户端会向服务器的 /delete 端点发送一条消息,移除为已结束的游戏存储的数据。

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

通过调用 /delete 端点的处理程序删除 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);
  }
}

后续步骤

  • 亲自试用其他 Google Cloud Platform 功能。查阅我们的教程
此页内容是否有用?请给出您的反馈和评价:

发送以下问题的反馈:

此网页
解决方案