FCM服务器 admin-SDK接入指引
前言
完成了初步测试,基本功能已经可以运用,因此整理一套相对完善的服务端接入指引
部分资料参考自
FCM服务端 - 谷歌云推送 - “errorCode“: “SENDER_ID_MISMATCH“-故障记录
前置操作
由于是Google服务因此当你在国内本地搭建环境时需要翻墙,需要为代码配置代理的服务的端口。
FCM集成
官网提供了使用起来非常方便的sdk,https://firebase.google.com/docs/admin/setup
并且有详细的接入流程。
admin-sdk导入
我采用了方便的maven进行sdk集成
<dependency> <!-- 谷歌推送依赖 -->
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
<version>6.10.0</version>
</dependency>
初始化sdk
为了能在非Google环境下进行凭据确认,我采用设置 GOOGLE_APPLICATION_CREDENTIALS
环境变量的ADC确认方法。
将 GOOGLE_APPLICATION_CREDENTIALS
环境变量设置为包含服务帐号密钥的 JSON 文件的文件路径。
可以手动设置环境变量或者控制台:
set GOOGLE_APPLICATION_CREDENTIALS="C:\Users\username\Downloads\service-account-file.json"
环境变量配置后需重启使得配置生效(也许有些情况下不用,但是我测试时需要重启否则读不到这个值)
//sdk的初始化语句;
FirebaseOptions options = new FirebaseOptions.Builder()
.setCredentials(GoogleCredentials.getApplicationDefault())
.setDatabaseUrl("https://<DATABASE_NAME>.firebaseio.com/")
.build();
FirebaseApp.initializeApp(options);
测试方案
集成后可以先测试一下是否集成成功,直接运行初始化函数,如果不报错表示json文件的路径和里面的几个关键字段是正确的(服务帐号密钥的 JSON 文件不要自己去编辑,理论上直接下载的就一定是正确的)。
然后可以进行进一步测试,查看是否能够沟通FCM服务器,检验sdk运行情况,这里我提供一个思路:
//设置环境
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.getApplicationDefault())
.setServiceAccountId(client_email)
.build();
FirebaseApp.initializeApp(options);
CreateRequest request = new CreateRequest()
.setEmail("[email protected]")
.setEmailVerified(false)
.setPassword("secretPassword")
.setPhoneNumber("+11234567892")
.setDisplayName("John Doe")
.setPhotoUrl("http://www.example.com/12345678/photo.png")
.setDisabled(false);
UserRecord userRecord = FirebaseAuth.getInstance().createUser(request);
System.out.println("Successfully created new user: " + userRecord.getUid());
这段代码的功能为创建用户,用这种方式可以验证拥有的权限文件是否可用,如果成功将获得成功打印,如果失败请自行排查bug
参考思路:https://firebase.google.com/docs/auth/admin/manage-users
初始化单个或多个应用
单个应用初始很简单,如果在需要情况下也可以根据需求初始多个应用
// Initialize the default app
FirebaseApp defaultApp = FirebaseApp.initializeApp(defaultOptions);
// Initialize another app with a different config
FirebaseApp otherApp = FirebaseApp.initializeApp(otherAppConfig, "other");
System.out.println(defaultApp.getName()); // "[DEFAULT]"
System.out.println(otherApp.getName()); // "other"
// Use the shorthand notation to retrieve the default app's services
FirebaseAuth defaultAuth = FirebaseAuth.getInstance();
FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance();
// Use the otherApp variable to retrieve the other app's services
FirebaseAuth otherAuth = FirebaseAuth.getInstance(otherApp);
FirebaseDatabase otherDatabase = FirebaseDatabase.getInstance(otherApp);
注意:每个应用实例都有自己的配置选项和身份验证状态。
构建发送请求
方便构建请求的工具类可以去我上一篇里去找,那部分也是别人创作的,或者根据自己的需求进行改写
参考内容1:FCM服务端 - 谷歌云推送 - “errorCode“: “SENDER_ID_MISMATCH“-故障记录
参考内容2:构建发送请求:https://firebase.google.com/docs/cloud-messaging/send-message
参考内容3:消息类型:https://firebase.google.com/docs/cloud-messaging/concept-options#notifications_and_data_messages
发送请求的类别
总体上有2种发送请求模式,定向发送和订阅主题的群体发送,然后在定向发送时可以进行群发或者单发,都需要提供每一个目标的注册令牌(token)。
另外官方还提供了群体组合消息发送的API,可以批量的发送大量的发送请求
定向发送(token)
单体发送
// This registration token comes from the client FCM SDKs.
String registrationToken = "YOUR_REGISTRATION_TOKEN";
// See documentation on defining a message payload.
Message message = Message.builder()
.putData("score", "850")
.putData("time", "2:45")
.setToken(registrationToken)
.build();
// Send a message to the device corresponding to the provided
// registration token.
String response = FirebaseMessaging.getInstance().send(message);
// Response is a message ID string.
System.out.println("Successfully sent message: " + response);
运行成功后,每个发送方法都将返回一个消息 ID。Firebase Admin SDK 会返回 projects/{project_id}/messages/{message_id} 格式的消息 ID 字符串。HTTP 协议响应为单个 JSON 密钥:
{
"name":"projects/myproject-b5ae1/messages/0:1500415314455276%31bd1c9631bd1c96"
}
群体发送
借助 REST API 与 Admin FCM API,可以将消息多播到一系列设备注册令牌。每次调用时,最多可以指定 100 个设备注册令牌(对于 Java 和 Node.js,最多可以指定 500 个)
并且对于对于 Firebase Admin SDK,返回时响应列表和输入顺序相对应,方便查看错误单元
// Create a list containing up to 100 registration tokens.
// These registration tokens come from the client FCM SDKs.
List<String> registrationTokens = Arrays.asList(
"YOUR_REGISTRATION_TOKEN_1",
// ...
"YOUR_REGISTRATION_TOKEN_n"
);
MulticastMessage message = MulticastMessage.builder()
.putData("score", "850")
.putData("time", "2:45")
.addAllTokens(registrationTokens)
.build();
BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message);
// See the BatchResponse reference documentation
// for the contents of response.
System.out.println(response.getSuccessCount() + " messages were sent successfully");
if (response.getFailureCount() > 0) {
List<SendResponse> responses = response.getResponses();
List<String> failedTokens = new ArrayList<>();
for (int i = 0; i < responses.size(); i++) {
if (!responses.get(i).isSuccessful()) {
// The order of responses corresponds to the order of the registration tokens.
failedTokens.add(registrationTokens.get(i));
}
}
System.out.println("List of tokens that caused failures: " + failedTokens);
主题推送(topic)
主题推送可以为订阅主题的用户群进行推送,并且可以进行复合主题推送,让订阅了特殊主题组合的用户收到推送。
一般主题推送
// The topic name can be optionally prefixed with "/topics/".
String topic = "highScores";
// See documentation on defining a message payload.
Message message = Message.builder()
.putData("score", "850")
.putData("time", "2:45")
.setTopic(topic)
.build();
// Send a message to the devices subscribed to the provided topic.
String response = FirebaseMessaging.getInstance().send(message);
// Response is a message ID string.
System.out.println("Successfully sent message: " + response);
组合主题推送
如需向主题组合发送消息,请指定一个条件,该条件是一个指定目标主题的布尔表达式。例如,如需向已订阅 TopicA 和 TopicB 或 TopicC 的设备发送消息,请设置如下条件:
"'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"
FCM 首先对括号中的所有条件求值,然后从左至右对表达式求值。在上述表达式中,只订阅某个单一主题的用户将不会接收到消息。同样地,未订阅 TopicA 的用户也不会接收到消息。下列组合将会接收到消息:
- TopicA 和 TopicB
- TopicA 和 TopicC
最多可以在条件表达式中添加五个主题。
// Define a condition which will send to devices which are subscribed
// to either the Google stock or the tech industry topics.
String condition = "'stock-GOOG' in topics || 'industry-tech' in topics";
// See documentation on defining a message payload.
Message message = Message.builder()
.setNotification(Notification.builder()
.setTitle("$GOOG up 1.43% on the day")
.setBody("$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.")
.build())
.setCondition(condition)
.build();
// Send a message to devices subscribed to the combination of topics
// specified by the provided condition.
String response = FirebaseMessaging.getInstance().send(message);
// Response is a message ID string.
System.out.println("Successfully sent message: " + response);
批量发送
官方文档如下说明:
REST API 与 Admin SDK 支持批量发送消息。您可以将多达 500 条消息组合到一个批处理中,并在单个 API 调用中将它们全部发送出去。与为每条消息单独发送 HTTP 请求相比,这种方法可以显著提高性能。
此功能可用于构建一组自定义消息(包括主题或特定设备注册令牌)并将其发送给不同的接收者。 例如,当您需要同时向不同受众发送消息正文中具有略微不同细节的消息时,请使用此功能
// Create a list containing up to 500 messages.
List<Message> messages = Arrays.asList(
Message.builder()
.setNotification(Notification.builder()
.setTitle("Price drop")
.setBody("5% off all electronics")
.build())
.setToken(registrationToken)
.build(),
// ...
Message.builder()
.setNotification(Notification.builder()
.setTitle("Price drop")
.setBody("2% off all books")
.build())
.setTopic("readers-club")
.build()
);
BatchResponse response = FirebaseMessaging.getInstance().sendAll(messages);
// See the BatchResponse reference documentation
// for the contents of response.
System.out.println(response.getSuccessCount() + " messages were sent successfully");
返回模型和定向推送里的群体推送相同,这是因为都是调用了底层的sendALL,返回的消息顺序和发送的请求顺序相同,方便查找失败单元。
失败类型
FCM发生故障时的错误代码来自:
https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode
枚举 | 错误代码 | 建议措施 |
---|---|---|
UNSPECIFIED_ERROR | 没有更多有关此错误的信息。 | 没有。 |
INVALID_ARGUMENT | (HTTP错误代码= 400) | 请求参数无效。返回类型为google.rpc.BadRequest的扩展名,以指定哪个字段无效。 |
UNREGISTERED | (HTTP错误代码= 404) | 应用实例已从FCM中取消注册。这通常意味着使用的令牌不再有效,必须使用新的令牌。 该错误可能是由于缺少注册令牌或未注册令牌引起的。 |
SENDER_ID_MISMATCH | (HTTP错误代码= 403) | 经过身份验证的发件人ID与注册令牌的发件人ID不同。 注册令牌绑定到特定的一组发送者。客户端应用注册FCM时,必须指定允许哪些发件人发送消息。将消息发送到客户端应用程序时,应使用这些发件人ID之一。如果您切换到其他发件人,则现有的注册令牌将不起作用。 |
QUOTA_EXCEEDED | (HTTP错误代码= 429) | 超出了邮件目标的发送限制。返回类型为google.rpc.QuotaFailure的扩展名,以指定超出了哪个配额。 此错误可能是由于超出了邮件速率配额,超出了设备邮件速率配额或超出了主题邮件速率配额引起的。 |
UNAVAILABLE | (HTTP错误代码= 503) | 服务器超载。 服务器无法及时处理请求。重试相同的请求,但您必须:-如果FCM Connection Server的响应中包含Retry-After标头,请遵循该标头。-在重试机制中实现指数补偿。 (例如,如果您在第一次重试之前等待了一秒钟,则在下一次重试之前等待至少两秒钟,然后等待4秒钟,依此类推)。如果您要发送多封邮件,请为每封邮件单独延迟一个额外的随机量,以避免同时发出针对所有邮件的新请求。导致问题的发件人可能会被列入黑名单。 |
INTERNAL | (HTTP错误代码= 500) | 发生未知的内部错误。 服务器在尝试处理请求时遇到错误。您可以按照“超时”中列出的要求重试相同的请求(请参见上一行)。如果错误仍然存在,请联系Firebase支持。 |
THIRD_PARTY_AUTH_ERROR | (HTTP错误代码= 401) | APNS证书或Web推送身份验证密钥无效或丢失。 无法发送针对iOS设备或Web推送注册的消息。检查您的开发和生产凭证的有效性。 |
调试心得
调试时如果发现error,需要按次序依次排查,通常比较容易出现的是400、403、404这3种错误,其中404最先发生,表示token作废,token值完全不可用,403其次,表示token不匹配,403这个错误类型有多种诱发因素,核心是token不匹配,但是原因可能会很复杂,调试和思考路径可以参考我的:
400表示字段内容错误,这个时候token已经对了,错误核心是其他参数,需要仔细的一一排查。比如某些字符串是否是填写错误了,某些网址是否不可用。
主题管理
官方资料:https://firebase.google.com/docs/cloud-messaging/manage-topics
主题功能我还未进行详细测试,或许以后有空了会研究一下,
订阅功能:
// These registration tokens come from the client FCM SDKs.
List<String> registrationTokens = Arrays.asList(
"YOUR_REGISTRATION_TOKEN_1",
// ...
"YOUR_REGISTRATION_TOKEN_n"
);
// Subscribe the devices corresponding to the registration tokens to the
// topic.
TopicManagementResponse response = FirebaseMessaging.getInstance().subscribeToTopic(
registrationTokens, topic);
// See the TopicManagementResponse reference documentation
// for the contents of response.
System.out.println(response.getSuccessCount() + " tokens were subscribed successfully");
退订功能
// These registration tokens come from the client FCM SDKs.
List<String> registrationTokens = Arrays.asList(
"YOUR_REGISTRATION_TOKEN_1",
// ...
"YOUR_REGISTRATION_TOKEN_n"
);
// Unsubscribe the devices corresponding to the registration tokens from
// the topic.
TopicManagementResponse response = FirebaseMessaging.getInstance().unsubscribeFromTopic(
registrationTokens, topic);
// See the TopicManagementResponse reference documentation
// for the contents of response.
System.out.println(response.getSuccessCount() + " tokens were unsubscribed successfully");
推送铃声设置
这方面也需要客户端配合,而服务端的推送函数配置可以如下:
public static void testPush( String token,String title,String body
,String ring ,String ChannelID) throws IOException, FirebaseMessagingException {
// See documentation on defining a message payload.
//获取实例
FirebaseApp firebaseApp = FirebaseApp.getInstance();
//实例为空的情况
if (firebaseApp == null) {
return;
}
// 这2个内容先不要写需要和其他人协调后再做决定
// androidNotifiBuilder.setIcon("https://www.shiku.co/images/favicon.png");// 设置消息图标
// androidConfigBuilder.setRestrictedPackageName("io.telecomm.telecomm");
androidNotifiBuilder.setColor("#55BEB7");// 设置消息通知颜色
androidNotifiBuilder.setTitle(title);// 设置消息标题
androidNotifiBuilder.setBody(body);// 设置消息内容
androidNotifiBuilder.setSound(ring); //设置提示音
androidNotifiBuilder.setChannelId(ChannelID); //设置通知通道
AndroidNotification androidNotification = androidNotifiBuilder.build();
androidConfigBuilder.setNotification(androidNotification);
AndroidConfig androidConfig = androidConfigBuilder.build();
//构建消息
Message message = Message.builder()
.setAndroidConfig(androidConfig)
.setToken(token)
.build();
String response = FirebaseMessaging.getInstance().send(message);
// Response is a message ID string.
System.out.println("Successfully sent message: " + response);
}
如果推送成功android端将按着前端开发的设定,将会播放定义的提示音。
关于通知提示音问题,网上有个不错的方案,如下:
可以通过两种方式处理fcm后台铃声问题。一种是把fcm的sdk版本号设置为10.0.1及以下,在zzm()方法中处理通知铃声(这种方式不推荐,因为fcm版本号迟早都得升级);另外一种就是,8.0以下版本在res/raw内置一套铃声音频,设置完成后把铃声名告诉服务端(ter.mp3只需要设置ter,后缀不需要),服务端通过setSound来实现。在8.0及以上版本,当修改一个铃声或者振动,在本地创建一个新的渠道id,设置设置的铃声与振动,把渠道id告诉服务端,服务端通过setAndroidChannelId()来实现;
对于这个方案,我分析了一下应该是这个意思由于版本问题服务端需要向下方传递2个信息一个是音频的名字,不需要后缀,另一个是渠道id,但是服务端怎么知道下方的版本呢,我的设想是,客户端自身配置好铃声后,都上报,然后服务端下发时都下发,又客户端自己处理版本问题。
结语
大部分资料都是来自官方的操作指引,FCM本身架构成熟,功能丰富,并且拥有稳定推送的特性,非常适合境外市场。
后续如果我开发时发现什么有意思的内容也会继续向下在本文更新。