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

Last reviewed 2018-08-27 UTC

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

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

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

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

下图展示 Playchat 客户端架构。

Playchat 客户端架构

在 App Engine 柔性环境中运行的一组 Java servlet 注册为 Firebase 的监听器。这些 Servlet 不但会响应新的用户事件日志并处理日志数据,而且利用事务来确保仅由一个 servlet 处理每个用户事件日志。

下图展示 Playchat 服务器架构。

Playchat 服务器架构

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

  • 当新用户登录 Playchat 时,应用通过在 Firebase Realtime Database 中的 /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 Realtime Database 中存储数据的 iOS 应用 Playchat。

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

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

费用

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

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

准备工作

安装以下软件:

通过在终端窗口中运行以下命令来安装 gcloud CLI 的 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。记下分配给项目的项目 ID,本教程的多个步骤中会用到。

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

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

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

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

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

  9. 点击注册应用

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

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

  12. 记下使用 CocoaPods 安装和管理项目依赖项的说明。示例代码中已配置 CocoaPods 依赖项管理器。

  13. 运行以下命令以安装依赖项。该命令可能需要很长时间才能完成。

    pod install
    

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

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

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

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

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

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

创建实时数据库

  1. Firebase 控制台中,选择您的项目。

  2. 从控制台的左侧菜单中,选择构建组中的 Realtime Database

  3. 点击 Realtime Database 部分中的创建数据库

  4. 选择一个您附近的位置。

  5. 安全规则对话框中,选择以测试模式开始,然后点击启用

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

  6. 在数据库的规则标签页中,确保您拥有读/写安全规则,该规则指定 30 天后的一个未来日期并以 Unix 纪元时间中定义的时间单位来表示。 例如,如果您在 8 月 4 日创建规则,则该规则应在 9 月 4 日之后到期:

    {
      "rules": {
        ".read": "now < 1659639249000", //2022-08-04
        ".write": "now < 1659639249000" //2022-08-04
      }
    }
    
  7. 记下项目的 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. 点击完成

  5. 点击您刚刚创建的服务帐号,在密钥标签页下,点击添加密钥,然后点击创建新密钥

  6. 点击密钥类型旁边的 JSON,然后点击创建以下载密钥。

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

  8. 按如下所示修改 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 项目启用结算功能和 API

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

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

    前往“项目”页面

  2. Make sure that billing is enabled for your Google Cloud project.

  3. Enable the App Engine Admin and Compute Engine APIs.

    Enable the APIs

构建和部署后端服务

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

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

设置项目

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

  1. 提供 gcloud CLI 访问 GCP 所用的凭据。

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

    gcloud config set project [FIREBASE_PROJECT_ID]
    
  3. 列出配置,验证项目已经设置。

    gcloud config list
    
  4. 如果是第一次使用 App Engine,请初始化您的 App Engine 应用

    gcloud app create
    

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

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

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

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

    mvn clean package appengine:run
    

    如果您已将 Google Cloud CLI 安装到 ~/google-cloud-sdk 以外的目录中,请按如下所示将安装路径添加到命令中,并将 [PATH_TO_TOOL] 替换为您的自定义路径。

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

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

刷新页面时,此标识符不会更改,相反,您的本地服务器会启动一个 servlet 实例。这对测试很有用,因为 Firebase Realtime Database 中只存储了一个 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 并导航到关键网址类型 (key URL types) > 项目 0(编辑者) (Item 0 (Editor)) > 网址架构 (URL Schemes) > 项目 0 (Item 0)。

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

运行和测试 iOS 应用

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

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

    登录 Playchat

  3. 选择图书频道。

  4. 输入消息。

    发送消息

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

发送消息

验证数据

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

为您的应用打开 Firebase Realtime Database,其中 [FIREBASE_PROJECT_ID]创建 Firebase 项目时的标识符。

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

在 Firebase Realtime Database 的底部,在 /inbox/ 数据位置下有一组节点,这些节点以 client- 为前缀,后跟一个代表用户帐号登录名的随机生成密钥。此示例中的最后一个条目 client-1240563753 后跟当前侦听该用户日志事件的 servlet 的 16 位标识符(在此示例中为 0035806813827987)。

存储在 Firebase Realtime Database 中的数据

就在这组节点上面,/inbox/ 数据位置下,是所有当前已经分配的 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 Realtime Database。

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 Realtime Database 中的 /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 Realtime Database,并向 /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 Realtime Database 中检索新的日志数据来进行响应。

/*
 * 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 控制台中创建了项目,但您也可以在 Google Cloud 控制台中将其删除,因为 Firebase 项目和 Cloud 项目为同一个项目。

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

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

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

  1. In the Google Cloud console, go to the Versions page for App Engine.

    Go to Versions

  2. Select the checkbox for the non-default app version that you want to delete.
  3. 如需删除应用版本,请点击删除

后续步骤

  • 分析和归档数据 - 在此示例中,servlet 仅将日志数据存储在内存中。如需扩展此示例,您可以使用 Cloud StorageCloud BigtableGoogle Cloud DataflowBigQuery 之类的服务让 servlet 对数据进行归档、转换和分析。

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

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

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

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

  • 使用 Cloud AI 产品实现其他功能 - 探索如何使用 Cloud AI 产品和服务提供基于机器学习的功能。例如,您可以扩展此示例实现,以结合使用 Speech-to-TextTranslationText-to-Text Speech API 来提供语音翻译功能。如需了解详情,请参阅向 Android 应用添加语音翻译