使用 Firebase 和 App Engine 柔性环境构建 iOS 应用

本教程演示如何使用 Firebase 构建具有后端数据存储功能、实时同步功能和用户事件日志记录功能的 iOS 应用。在 App Engine 柔性环境中运行的 Java servlet 会侦听存储在 Firebase 中的新用户日志并对其进行处理。

说明展示了如何使用 FirebaseApp Engine 柔性环境完成此操作。

如果您希望应用处理用户数据或编排事件,可以使用 App Engine 柔性环境扩展 Firebase,以执行自动实时数据同步。

示例应用 Playchat 将聊天消息存储在 Firebase 实时数据库中,该数据库会自动在设备间同步这些数据。Playchat 还将用户事件日志写入 Firebase。如需详细了解数据库如何同步数据,请参阅 Firebase 文档中的工作原理

下图展示了 Playchat 客户端架构。

Playchat 客户端架构

在 App Engine 柔性环境中运行的一组 Java servlet 使用 Firebase 注册为侦听器。Servlet 响应新的用户事件日志并处理日志数据。Servlet 使用事务来确保只有一个 servlet 处理每个用户事件日志。

下图展示了 Playchat 服务器架构。

Playchat 服务器架构

应用和 servlet 之间的通信分为三个部分:

  • 当新用户登录 Playchat 时,应用通过在 Firebase 实时数据库中的 /inbox/ 下添加一个条目来请求该用户的日志记录 servlet。

  • 其中一个 servlet 通过将条目的值更新为其 servlet 标识符来接受分配。该 servlet 使用 Firebase 事务来保证它是唯一可以更新值的 servlet。更新值后,所有其他 servlet 将忽略该请求。

  • 当用户登录、退出或转到新频道时,Playchat 会将操作记录在 /inbox/[SERVLET_ID]/[USER_ID]/ 中,其中 [SERVLET_ID] 是 servlet 实例的标识符,[USER_ID] 是表示用户的哈希值。

  • servlet 会侦听收件箱以查找新条目并收集日志数据。

在此示例应用中,servlet 在本地复制日志数据并将其显示在网页上。在此应用的正式版中,servlet 可以处理日志数据或将其复制到 Cloud StorageCloud BigtableBigQuery 中以存储和分析。

目标

本教程演示如何完成以下任务:

  • 构建一个在 Firebase 实时数据库中存储数据的 iOS 应用 Playchat。

  • 在 App Engine 柔性环境中运行一个连接到 Firebase 的 Java servlet,并在 Firebase 中存储的数据发生更改时接收通知。

  • 使用这两个组件构建分布式流式后端服务,以收集和处理日志数据。

费用

Firebase 提供免费使用级别。如果您的服务使用量低于 Firebase 免费计划中指定的限制,则使用 Firebase 无需付费。

App Engine 柔性环境中的实例需要为底层 Compute Engine 虚拟机付费。

准备工作

安装以下软件:

通过在终端窗口中运行以下命令来安装 Cloud SDK 的 App Engine Java 组件。

gcloud components install app-engine-java

克隆示例代码

  1. 克隆客户端应用代码。

    git clone https://github.com/GoogleCloudPlatform/firebase-ios-samples
    
  2. 克隆后端 servlet 代码。

    git clone https://github.com/GoogleCloudPlatform/firebase-appengine-backend
    

创建 Firebase 项目

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

  2. 点击添加项目

  3. 项目名称中,输入:Playchat

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

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

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

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

  8. iOS 软件包 ID 中,输入:com.google.cloud.solutions.flexenv.PlayChat

  9. 点击注册应用

  10. 按照下载配置文件部分中的步骤将 GoogleService-Info.plist 文件添加到项目的 PlayChat 文件夹中。

  11. 点击下载配置文件部分中的下一步

  12. 记下使用 CocoaPods 安装和管理项目依赖项的说明。

  13. 运行以下命令以安装依赖项:

    pod install
    

    如果您的 CocoaPods 安装无法找到 Firebase 依赖项,则可能需要运行 pod repo update

    完成此步骤后,使用新创建的 .xcworkspace 文件而非 .xcodeproj 文件,以在 iOS 应用上进行所有开发。

  14. 添加 Firebase SDK 部分中点击下一步

  15. 记下在项目中初始化 Firebase 所需的代码。

  16. 点击添加初始化代码部分中的下一步

  17. 点击运行您的应用以验证安装部分中的跳过此步骤

创建实时数据库

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

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

  3. 实时数据库的安全规则对话框中,选择以测试模式启动,然后点击启用

    此步骤显示您存储在 Firebase 中的数据。在本教程的后续步骤中,您可以重新访问此网页以查看客户端应用和后端 servlet 添加和更新的数据。

  4. 记下项目的 Firebase 网址,其格式为 https://[FIREBASE_PROJECT_ID].firebaseio.com/ 并显示在链接图标旁边。

为 Firebase 项目启用 Google 身份验证

您可以配置各种登录方式以连接到 Firebase 项目。本教程将指导您设置身份验证,以便用户使用 Google 帐号登录。

  1. Firebase 控制台的左侧菜单中,点击开发组中的身份验证

  2. 点击设置登录方法

  3. 选择 Google,切换到启用,然后点击保存

将服务帐号添加到 Firebase 项目

后端 servlet 不使用 Google 帐号登录。相反,它使用服务帐号连接到 Firebase。以下步骤将指导您创建可以连接到 Firebase 并将服务帐号凭据添加到 servlet 代码的服务帐号。

  1. Firebase 控制台的左侧菜单中,在 Playchat 项目主页旁边,选择设置齿轮,然后选择项目设置

  2. 选择服务帐号,然后选择管理所有服务帐号

  3. 点击创建服务帐号

  4. 请配置以下设置:

    1. 服务帐号名称中,输入 playchat-servlet
    2. 角色中,选择项目 > 所有者

    3. 勾选提供新的私钥

    4. 密钥类型中选择 JSON
  5. 点击创建

  6. 下载服务帐号的 JSON 密钥文件,并保存到 src/main/webapp/WEB-INF/ 目录中的后端服务项目 firebase-appengine-backend。文件名采用 Playchat-[UNIQUE_ID].json 格式。

  7. 修改 src/main/webapp/WEB-INF/web.xml 并修改初始化参数,如下所示:

    • JSON_FILE_NAME 替换为您下载的 JSON 密钥文件的名称。

    • FIREBASE_URL 替换为您之前记录的 Firebase 网址。

      <init-param>
        <param-name>credential</param-name>
        <param-value>/WEB-INF/JSON_FILE_NAME</param-value>
      </init-param>
      <init-param>
        <param-name>databaseUrl</param-name>
        <param-value>FIREBASE_URL</param-value>
      </init-param>
      

为 Google Cloud Platform 项目启用结算功能和 API

要在 GCP 上运行后端服务,您需要为项目启用结算功能和 API。GCP 项目与您在创建 Firebase 项目中创建的为同一项目,具有相同的项目标识符。

  1. 在 Google Cloud Platform Console 中,选择 Playchat 项目。

    转到“项目”页面

  2. 确保您的 Google Cloud Platform 项目已启用结算功能。

    了解如何启用结算功能

  3. 启用 App Engine Admin 和 Compute Engine API。

    启用 API

构建和部署后端服务

此示例中的后端服务使用 Docker 配置来指定其托管环境。自定义托管环境意味着您必须使用 App Engine 柔性环境而不是 App Engine 标准环境。

要构建后端 servlet 并将其部署在 App Engine 柔性环境中,您可以使用 Google App Engine Maven 插件。此插件已在此示例附带的 Maven 构建文件中指定。

设置项目

要使 Maven 正确构建后端 servlet,您必须为其提供 Google Cloud Platform (GCP) 项目以启动 servlet 资源。GCP 项目标识符和 Firebase 项目标识符相同。

  1. gcloud 工具提供访问 GCP 的凭据。

    gcloud auth login
    
  2. 使用以下命令将项目设置为您的 Firebase 项目,将 [FIREBASE_PROJECT_ID] 替换为您之前记下的 Firebase 项目 ID 的名称。

    gcloud config set project [FIREBASE_PROJECT_ID]
    
  3. 通过列出配置来验证是否已设置项目。

    gcloud config list
    

(可选)在本地服务器上运行服务

开发新的后端服务时,在将该服务部署到 App Engine 之前请在本地运行该服务,以快速迭代更改,省去完全部署到 App Engine 的开销。

在本地运行服务器时,它不使用 Docker 配置或在 App Engine 环境中运行。相反,Maven 保证所有依赖库都在本地安装,并且应用在 Jetty 网络服务器上运行。

  1. firebase-appengine-backend 目录中,使用以下命令在本地构建并运行后端模块:

    mvn clean package appengine:run
    
    mvn clean package appengine:run -Dgcloud.gcloud_directory=[PATH_TO_TOOL]
    
  2. 如果系统提示您是否希望应用“Python.app”接受传入的网络连接?,请选择允许

完成部署后,打开 http://localhost:8080/printLogs 验证后端服务是否正在运行。网页显示“Inbox :”后跟一个 16 位的标识符。这是在本地计算机上运行的 servlet 的收件箱标识符。

刷新页面时,此标识符不会更改;您的本地服务器会启动一个 servlet 实例。这对测试很有用,因为 Firebase 实时数据库中只存储了一个 servlet 标识符。

要关闭本地服务器,请按下 Ctrl+C

将服务部署到 App Engine 柔性环境

在 App Engine 柔性环境中运行后端服务时,App Engine 使用 /firebase-appengine-backend/src/main/webapp/Dockerfiles 中的配置来构建服务运行的托管环境。柔性环境会启动几个 servlet 实例并对其进行扩缩以满足需求。

  • firebase-appengine-backend 目录中,使用以下命令在本地构建并运行后端模块:

    mvn clean package appengine:deploy
    
    mvn clean package appengine:deploy -Dgcloud.gcloud_directory=[PATH_TO_GCLOUD]
    

在构建运行时,您会看到“将构建上下文发送到 Docker 守护进程…”的行。上一个命令会上传您的 Docker 配置并在 App Engine 柔性环境中对其进行设置。

完成部署后,打开 https://[FIREBASE_PROJECT_ID].appspot.com/printLogs,其中 [FIREBASE_PROJECT_ID]创建 Firebase 项目时的标识符。网页显示“Inbox :”后跟一个 16 位的标识符。这是在 App Engine 柔性环境中运行的 servlet 的收件箱标识符。

刷新页面时,此标识符会定期更改,因为 App Engine 会启动多个 servlet 实例以处理传入的客户端请求。

更新 iOS 示例中的网址架构

  1. 在 Xcode 中,打开 PlayChat 工作区,然后打开 PlayChat 文件夹。

  2. 打开 GoogleService-Info.plist 并复制 REVERSED_CLIENT_ID 的值。

  3. 打开 Info.plist 并导航到关键网址类型 > 项目 0(编辑者) > 网址架构 > 项目 0

  4. 将占位符值 [REVERSED_CLIENT_ID] 替换为您从 GoogleService-Info.plist 复制的值。

运行和测试 iOS 应用

  1. 在 Xcode 中,打开 PlayChat 工作区,然后选择产品 > 运行

  2. 将应用加载到模拟器后,使用您的 Google 帐号登录。

    登录 Playchat

  3. 选择图书频道。

  4. 输入消息。

    发送消息

进行以上操作时,Playchat 应用会将您的消息存储在 Firebase 实时数据库中。Firebase 跨设备同步数据库中存储的数据。当用户选择图书频道时,运行 Playchat 的设备会显示新消息。

发送消息

验证数据

使用 Playchat 应用生成一些使用 Playchat 应用的用户事件后,您可以验证 servlet 是否正在注册为侦听器并收集用户事件日志。

为您的应用打开 Firebase 实时数据库,其中 [FIREBASE_PROJECT_ID]创建 Firebase 项目时的标识符。

https://console.firebase.google.com/project/[FIREBASE_PROJECT_ID]/database/data

在 Firebase 实时数据库的底部,在 /inbox/ 数据位置下,有一组以 client- 为前缀,后跟代表用户帐号登录信息的随机生成密钥的节点。在此示例中的最后一个条目 client-1240563753 之后是当前正在侦听来自该用户的日志事件的 servlet 的 16 位标识符,在此示例中为 0035806813827987

存储在 Firebase 实时数据库中的数据

/inbox/ 数据位置上方,是所有当前分配的 servlet 的 servlet 标识符。在此示例中,只有一个 servlet 正在收集日志。/inbox/[SERVLET_IDENTIFIER] 下是应用写入该 servlet 的用户日志。

打开后端服务的 App Engine 页面 https://[FIREBASE_PROJECT_ID].appspot.com/printLogs,其中 [FIREBASE_PROJECT_ID]创建 Firebase 项目时的标识符。该页面显示记录您生成的用户事件的 servlet 的标识符。您还可以在 servlet 的收件箱标识符下方查看这些事件的日志条目。

查看代码

Playchat iOS 应用定义了一个 FirebaseLogger 类,用于将用户事件日志写入 Firebase 实时数据库。

import Firebase

class FirebaseLogger {
  var logRef: DatabaseReference!

  init(ref: DatabaseReference!, path: String!) {
    logRef = ref.child(path)
  }

  func log(_ tag: String!, message: String!) {
    let entry: LogEntry = LogEntry(tag: tag, log: message)
    logRef.childByAutoId().setValue(entry.toDictionary())
  }
}

当新用户登录时,Playchat 调用 requestLogger 函数将新条目添加到 Firebase 实时数据库中的 /requestLogger/ 位置并设置一个侦听器,这样当 servlet 更新该条目的值(即接受分配)时,Playchat 能够进行响应。

当 servlet 更新该值时,Playchat 将移除侦听器并将“已登录”日志写入 servlet 的收件箱。

func requestLogger() {
  ref.child(IBX + "/" + inbox!).removeValue()
  ref.child(IBX + "/" + inbox!)
    .observe(.value, with: { snapshot in
      print(self.inbox!)
      if snapshot.exists() {
        self.fbLog = FirebaseLogger(ref: self.ref, path: self.IBX + "/"
          + String(describing: snapshot.value!) + "/logs")
        self.ref.child(self.IBX + "/" + self.inbox!).removeAllObservers()
        self.msgViewController!.fbLog = self.fbLog
        self.fbLog!.log(self.inbox, message: "Signed in")
      }
    })
  ref.child(REQLOG).childByAutoId().setValue(inbox)
}

在后端服务端,当 servlet 实例启动时,MessageProcessorServlet.java 中的 init(ServletConfig config) 函数连接到 Firebase 实时数据库,并向 /inbox/ 数据位置添加一个侦听器。

当一个新条目被添加到 /inbox/ 数据位置时,servlet 会使用其标识符更新该值,这是一个发送到 Playchat 应用的信号,表示该 servlet 接受为该用户处理日志的分配。Servlet 使用 Firebase 事务来确保只有一个 servlet 可以更新该值并接受分配。

/*
 * Receive a request from a client and reply back its inbox ID.
 * Using a transaction ensures that only a single servlet instance replies
 * to the client. This lets the client know to which servlet instance
 * send consecutive user event logs.
 */
firebase.child(REQLOG).addChildEventListener(new ChildEventListener() {
  public void onChildAdded(DataSnapshot snapshot, String prevKey) {
    firebase.child(IBX + "/" + snapshot.getValue()).runTransaction(new Transaction.Handler() {
      public Transaction.Result doTransaction(MutableData currentData) {
        // Only the first servlet instance writes its ID to the client inbox.
        if (currentData.getValue() == null) {
          currentData.setValue(inbox);
        }
        return Transaction.success(currentData);
      }

      public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) {}
    });
    firebase.child(REQLOG).removeValue();
  }
  // ...
});

在 servlet 接受了处理用户事件日志的分配后,它会添加一个侦听器,用于检测 Playchat 应用何时将新日志文件写入 servlet 的收件箱。Servlet 通过从 Firebase 实时数据库中检索新的日志数据来进行响应。

/*
 * Initialize user event logger. This is just a sample implementation to
 * demonstrate receiving updates. A production version of this app should
 * transform, filter, or load to another data store such as Google BigQuery.
 */
private void initLogger() {
  String loggerKey = IBX + "/" + inbox + "/logs";
  purger.registerBranch(loggerKey);
  firebase.child(loggerKey).addChildEventListener(new ChildEventListener() {
    public void onChildAdded(DataSnapshot snapshot, String prevKey) {
      if (snapshot.exists()) {
        LogEntry entry = snapshot.getValue(LogEntry.class);
        logs.add(entry);
      }
    }

    public void onCancelled(DatabaseError error) {
      localLog.warning(error.getDetails());
    }

    public void onChildChanged(DataSnapshot arg0, String arg1) {}

    public void onChildMoved(DataSnapshot arg0, String arg1) {}

    public void onChildRemoved(DataSnapshot arg0) {}
  });
}

清理

为避免因本教程中使用的资源而导致系统向您的 Google Cloud Platform 帐号收取费用,请执行以下操作:

删除 Google Cloud Platform 和 Firebase 项目

停止计费的最简单方法是删除您为本教程创建的项目。虽然您在 Firebase 控制台中创建了项目,但您也可以在 GCP Console 中将其删除,因为 Firebase 项目和 GCP 项目为同一个项目。

  1. 在 GCP Console 中,转到“项目”页面。

    转到“项目”页面

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关闭以删除项目。

删除 App Engine 应用的非默认版本

如果您不想删除 GCP 和 Firebase 项目,可以通过删除 App Engine 柔性环境应用的非默认版本来降低费用。

  1. 在 GCP Console 中,转到“App Engine 版本”页面。

    转到“版本”页面

  2. 点击要删除的非默认应用版本旁边的复选框。
  3. 点击页面顶部的删除按钮以删除应用版本。

后续步骤

  • 分析和归档数据 - 在此示例中,servlet 仅将日志数据存储在内存中。要扩展此示例,您可以借助诸如 Cloud StorageCloud BigtableGoogle Cloud DataflowBigQuery 等服务,使用 servlet 归档、转换和分析数据。

  • 在 servlet 之间均匀分布工作负载 - App Engine 提供自动和手动扩缩。通过自动扩缩,柔性环境可以检测工作负载的变化,并通过在集群中添加或移除虚拟机实例来做出响应。通过手动扩缩,您可以指定固定数量的实例来处理流量。如需详细了解如何配置扩缩,请参阅 App Engine 文档中的服务扩缩设置

    由于将用户活动日志分配给 servlet 是通过访问 Firebase 实时数据库完成的,因此工作负载可能无法均匀分布。例如,一个 servlet 可能会比其他 servlet 处理更多的用户事件日志。

    您可以实现一个工作负载管理器,针对每个虚拟机独立控制工作负载,从而提高效率。这种工作负载平衡可以以每秒日志记录请求数量或并发客户端数量等指标为基础。

  • 恢复未处理的用户事件日志 - 在此示例实现中,如果一个 servlet 实例崩溃,则与该实例关联的客户端应用会继续将日志事件发送到 Firebase 实时数据库中的 servlet 收件箱。在此应用的正式版中,后端服务必须检测这种情况以恢复未处理的用户事件日志。

此页内容是否有用?请给出您的反馈和评价:

发送以下问题的反馈:

此网页
Solutions