FCM伺服器 admin-SDK接入指引

正半軸發表於2020-10-30

前言

完成了初步測試,基本功能已經可以運用,因此整理一套相對完善的服務端接入指引
部分資料參考自
FCM服務端 - 谷歌雲推送 - “errorCode“: “SENDER_ID_MISMATCH“-故障記錄

Firebase 官方文件

前置操作

由於是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("user2@example.com")
            	    .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本身架構成熟,功能豐富,並且擁有穩定推送的特性,非常適合境外市場。
後續如果我開發時發現什麼有意思的內容也會繼續向下在本文更新。

相關文章