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

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

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

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

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

下图展示 Playchat 客户端架构。

Playchat 客户端架构

在 App Engine 柔性环境中运行的一组 Java servlet 注册为 Firebase 的侦听器。这些 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 实时数据库中存储数据的 Android 应用 Playchat。

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

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

费用

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

如果使用 App Engine 柔性环境中的实例,则需要为底层 Google Compute Engine 虚拟机付费。

准备工作

安装以下软件:

通过从命令行运行以下命令来安装 Cloud SDK 的 App Engine Java 组件。

gcloud components install app-engine-java

克隆示例代码

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

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

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

为应用生成 SHA-1 指纹

如需对您的客户端应用进行 Google 登录身份验证,您需要提供证书的 SHA-1 指纹。在本教程中,我们将使用调试密钥库。如需了解如何创建密钥库指纹的发行版本,请参阅对客户端进行身份验证

  • 构建调试密钥库的 SHA-1。

    keytool -exportcert -list -v \
    -alias androiddebugkey -keystore ~/.android/debug.keystore
    

创建 Firebase 项目

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

  2. 点击添加项目

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

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

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

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

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

  8. Android 软件名称中,输入:com.google.cloud.solutions.flexenv

  9. 调试签名证书 SHA-1 中,输入您在上一部分中生成的 SHA-1 值。

  10. 点击注册应用

  11. 按照下载配置文件部分中的步骤将 google-services.json 文件添加到项目中。

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

  13. 记下建议对项目级层和应用级层 build.gradle 文件所做的更改。

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

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

创建实时数据库

  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. 角色中,选择项目 > Owner

    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>
      

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

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

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

    转到“项目”页面

  2. 确保您的 Google Cloud 项目已启用结算功能。 了解如何确认您的项目已启用结算功能

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

    启用 API

构建和部署后端服务

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

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

设置项目

要使 Maven 正确构建后端 servlet,您必须为其提供要在其中启动 servlet 资源的 Google Cloud Platform (GCP) 项目。请注意,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
    

    如果您已将 gcloud 命令行工具安装到 ~/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 实时数据库中只存储了一个 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 实例以处理传入的客户端请求。

将 Firebase 和 Google Play 服务添加到 Android 应用

客户端应用使用 Firebase 实时数据库来存储和同步消息并记录用户事件日志。客户端应用使用 Google Play 服务通过其 Google 帐号对用户进行身份验证。

  1. 在 Android Studio 中,选择工具 > SDK 管理器

  2. 在右侧窗格顶部,选择 SDK 工具

  3. 选择 Google Play 服务(如果尚未选择)。

  4. 点击确定

  5. 选择文件 > 打开…,然后选择 firebase-android-client 目录。

  6. 等待 Gradle 项目信息完成构建。如果系统提示您使用 Gradle 封装容器,请点击确定

  7. 您在创建 Firebase 项目中记下的对项目级层和应用级层 build.gradle 文件的更改已在示例代码中完成。

运行和测试 Android 应用

  1. 在 Android Studio 中,打开 firebase-android-client 项目,选择运行 > 运行“应用”

  2. 选择运行 Android 6.0 和 Google API 的设备或模拟器作为测试设备。

  3. 将应用加载到设备后,使用您的 Google 帐号登录。

    登录 Playchat

  4. 点击 PlayChat 标题左侧的菜单,然后选择图书频道。

    选择频道

  5. 输入消息。

    发送消息

  6. 进行以上操作时,Playchat 应用会将您的消息存储在 Firebase 实时数据库中。Firebase 跨设备同步数据库中存储的数据。当用户选择图书频道时,运行 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 正在收集日志。/inbox/[SERVLET_IDENTIFIER] 下是应用写入该 servlet 的用户日志。

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

查看代码

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


import com.google.cloud.solutions.flexenv.common.LogEntry;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;

/*
 * FirebaseLogger pushes user event logs to a specified path.
 * A backend servlet instance listens to
 * the same key and keeps track of event logs.
 */
class FirebaseLogger {
    private final DatabaseReference logRef;

    FirebaseLogger(String path) {
        logRef = FirebaseDatabase.getInstance().getReference().child(path);
    }

    public void log(String tag, String message) {
        LogEntry entry = new LogEntry(tag, message);
        logRef.push().setValue(entry);
    }

}

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

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

/*
 * Request that a servlet instance be assigned.
 */
private void requestLogger(final LoggerListener loggerListener) {
    final DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference();
    databaseReference.child(IBX + "/" + inbox).addListenerForSingleValueEvent(new ValueEventListener() {
        public void onDataChange(@NonNull DataSnapshot snapshot) {
            if (snapshot.exists() && snapshot.getValue(String.class) != null) {
                firebaseLoggerPath = IBX + "/" + snapshot.getValue(String.class) + "/logs";
                fbLog = new FirebaseLogger(firebaseLoggerPath);
                databaseReference.child(IBX + "/" + inbox).removeEventListener(this);
                loggerListener.onLoggerAssigned();
            }
        }

        public void onCancelled(@NonNull DatabaseError error) {
            Log.e(TAG, error.getDetails());
        }
    });

    databaseReference.child(REQLOG).push().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 帐号收取费用,请执行以下操作:

删除 Cloud Platform 和 Firebase 项目

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

  1. 在 Cloud Console 中,转到管理资源页面。

    转到“管理资源”页面

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

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

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

  1. 在 Cloud Console 中,转到 App Engine 的版本页面。

    转到“版本”页面

  2. 选中要删除的非默认应用对应的复选框。
  3. 点击删除 以删除应用版本。

后续步骤

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

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

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

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

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

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