如何使用 Firebase 消息传递一对一消息

Posted

技术标签:

【中文标题】如何使用 Firebase 消息传递一对一消息【英文标题】:How to send one to one message using Firebase Messaging 【发布时间】:2016-10-25 16:48:42 【问题描述】:

我一直在尝试阅读有关如何将消息从一台设备发送到另一台设备的官方文档和指南。我已经在实时数据库中保存了两个设备的注册令牌,因此我有另一个设备的注册令牌。 我尝试了以下方式发送消息

RemoteMessage message = new RemoteMessage.Builder(getRegistrationToken())
                    .setMessageId(incrementIdAndGet())
                    .addData("message", "Hello")
                    .build();
FirebaseMessaging.getInstance().send(message);

但是这不起作用。另一台设备没有收到任何消息。我什至不确定是否可以使用上游消息发送来进行设备到设备的通信。

PS:我只想知道是否可以使用 FCM 进行设备到设备的消息传递?如果是,那么我使用的代码是否有问题?如果是,那么正确的方法是什么。

更新: 我的问题是询问是否可以在不使用除 firebase 之外的任何单独服务器的情况下进行设备到设备消息传递,如果是,则不是,因为没有关于它的文档。我不明白这里还有什么要解释的?无论如何,我得到了答案,并会在问题重新打开后将其更新为答案。

【问题讨论】:

@Selvin 如果有人看不懂我添加的代码,那么他们无论如何都没有资格回答。如果您之前使用过 FCM,则此代码直接取自官方指南,注册令牌部分除外。为了理解起见,请阅读更新。 有一个问题getInstance()send ...这甚至无法编译 错字,现在怎么样? @noob.. 请更新您的解决方案.. 我正在寻找它过去 3 天..!! @jankigadhiya 我已经发布了解决方案。 【参考方案1】:

Firebase 具有两个向设备发送消息的功能:

Firebase 控制台中的通知面板允许您向特定设备、用户组或用户订阅的主题发送通知。 通过调用 Firebase Cloud Messaging API,您可以使用您喜欢的任何定位策略发送消息。调用 FCM API 需要访问您的服务器密钥,您不应在客户端设备上公开该密钥。这就是为什么您应该始终在应用服务器上运行此类代码的原因。

Firebase documentation 直观地显示了这一点:

Firebase 云消息传递不支持从一台设备直接向另一台设备发送消息。

更新:我写了一篇博文详细介绍了how to send notifications between android devices using Firebase Database, Cloud Messaging and Node.js。

更新 2:您现在还可以使用 Cloud Functions for Firebase 安全地发送消息,而无需启动服务器。请参阅this sample use-case 以开始使用。如果您不想使用 Cloud Functions,您可以在您已有的任何可信环境(例如您的开发机器或您控制的服务器)上运行相同的逻辑。

【讨论】:

感谢您的回答。我知道服务器密钥不应该暴露给客户端设备,但我只是在制作原型并且不想在这个阶段拥有应用服务器,所以如果我确实暴露了密钥并使用来自客户端的 POST 发送消息设备,能用吗?稍后我会介绍一个应用服务器,但现在不想要了。 所以基本上需要一个应用服务器,主要是为了安全。如果我根本不使用应用服务器只是一个 XMPP 库将节发送到 FCM CCS 并且不将我的服务器密钥存储在我的应用代码中,而是通过托管在在线数据库上的一些 php 脚本来请求它,那该怎么办?需要它。 @frankvanpuffelen 关于更新 2,哪个用例与将 Firebase Cloud Messaging 与 Cloud Functions 结合使用有关?还是您指的是实时数据库聊天限制示例? 为什么 firebase 甚至会强制使用“服务器”,因为它是作为客户端应用程序的后端呈现的,为什么对这些东西和 Admin sdk 有这样的限制?我们可以将这些敏感信息存储在 Firestore 中并刷新它们、获取它们并发送通知和消息,而无需将密钥暴露给客户端应用程序!【参考方案2】:

警告 我们没有在任何地方提及这种方法有一个非常重要的原因。这会在 APK 中公开您的服务器密钥 您放在每个客户端设备上。它可以(因此将)取自 那里并可能导致您的项目被滥用。我强烈推荐 反对采取这种方法,除了你只放的应用程序 您自己的设备。 – Frank van Puffelen

好的,所以 Frank 的回答是正确的,Firebase 本身并不支持设备到设备的消息传递。但是,其中有一个漏洞。 Firebase 服务器无法识别您是从实际服务器发送请求还是从您的设备发送请求。

所以您所要做的就是将Post Request 连同服务器密钥一起发送到Firebase 的消息服务器。 请记住,服务器密钥不应该在设备上,但如果您希望使用 Firebase 消息传递进行设备到设备的消息传递,则没有其他选择。

我使用 OkHTTP 而不是调用 Rest API 的默认方式。代码是这样的 -

public static final String FCM_MESSAGE_URL = "https://fcm.googleapis.com/fcm/send";
OkHttpClient mClient = new OkHttpClient();
public void sendMessage(final JSONArray recipients, final String title, final String body, final String icon, final String message) 

        new AsyncTask<String, String, String>() 
            @Override
            protected String doInBackground(String... params) 
                try 
                    JSONObject root = new JSONObject();
                    JSONObject notification = new JSONObject();
                    notification.put("body", body);
                    notification.put("title", title);
                    notification.put("icon", icon);

                    JSONObject data = new JSONObject();
                    data.put("message", message);
                    root.put("notification", notification);
                    root.put("data", data);
                    root.put("registration_ids", recipients);

                    String result = postToFCM(root.toString());
                    Log.d(TAG, "Result: " + result);
                    return result;
                 catch (Exception ex) 
                    ex.printStackTrace();
                
                return null;
            

            @Override
            protected void onPostExecute(String result) 
                try 
                    JSONObject resultJson = new JSONObject(result);
                    int success, failure;
                    success = resultJson.getInt("success");
                    failure = resultJson.getInt("failure");
                    Toast.makeText(getCurrentActivity(), "Message Success: " + success + "Message Failed: " + failure, Toast.LENGTH_LONG).show();
                 catch (JSONException e) 
                    e.printStackTrace();
                    Toast.makeText(getCurrentActivity(), "Message Failed, Unknown error occurred.", Toast.LENGTH_LONG).show();
                
            
        .execute();
    

String postToFCM(String bodyString) throws IOException 
        RequestBody body = RequestBody.create(JSON, bodyString);
        Request request = new Request.Builder()
                .url(FCM_MESSAGE_URL)
                .post(body)
                .addHeader("Authorization", "key=" + SERVER_KEY)
                .build();
        Response response = mClient.newCall(request).execute();
        return response.body().string();
    

我希望 Firebase 将来会提供更好的解决方案。但在那之前,我认为这是唯一的方法。另一种方式是发送主题消息或群组消息。但这不在问题的范围内。

更新: JSONArray 是这样定义的 -

JSONArray regArray = new JSONArray(regIds);

regIds 是一个注册 id 的字符串数组,你想发送这个消息。请记住,注册 ID 必须始终位于数组中,即使您希望将其发送给单个收件人。

【讨论】:

noob 我需要看看你的 recipients JsonArray.. 只需要检查该数组的格式.. 感谢您的解决方案 :) @jankigadhiya 添加了解释 我们没有在任何地方提及这种方法有一个非常重要的原因。这会在您放置在每个客户端设备上的 APK 中公开您的服务器密钥。它可以(因此将)从那里获取,并可能导致您的项目被滥用。我强烈建议不要采用这种方法,除非您只将应用程序放在自己的设备上。 @FrankvanPuffelen 是的,我理解并在答案中提到了它。我将对其进行编辑并使其成为亮点。我只是将它用于原型,因为考虑到时间限制,此时创建服务器对我来说是不可能的。 非常好的答案,但使用 root.put("to","other_user_device_token") 而不是 registration_ids 进行一对一聊天【参考方案3】:

我也一直在我的原型中使用直接设备到设备 gcm 消息传递。它一直运作良好。我们没有任何服务器。我们使用短信/文本交换 GCM reg id,然后使用 GCM 进行通信。我在这里放了与 GCM 处理相关的代码

******************发送 GCM 消息******************

//Sends gcm message Asynchronously
public class GCM_Sender extends IntentService
    final String API_KEY = "****************************************";

    //Empty constructor
    public GCM_Sender() 
        super("GCM_Sender");
    

    //Processes gcm send messages
    @Override
    protected void onHandleIntent(Intent intent)   

        Log.d("Action Service", "GCM_Sender Service Started");
        //Get message from intent
        String msg = intent.getStringExtra("msg");
        msg =  "\"" + msg + "\"";
        try
            String ControllerRegistrationId = null;                 
            //Check registration id in db       
            if(RegistrationIdAdapter.getInstance(getApplicationContext()).getRegIds().size() > 0 ) 
                String controllerRegIdArray[] = RegistrationIdAdapter.getInstance(getApplicationContext()).getRegIds().get(1);
                if(controllerRegIdArray.length>0)
                    ControllerRegistrationId = controllerRegIdArray[controllerRegIdArray.length-1];

                if(!ControllerRegistrationId.equalsIgnoreCase("NULL"))
                    // 1. URL
                    URL url = new URL("https://android.googleapis.com/gcm/send");
                    // 2. Open connection
                    HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
                    // 3. Specify POST method
                    urlConnection.setRequestMethod("POST");
                    // 4. Set the headers
                    urlConnection.setRequestProperty("Content-Type", "application/json");
                    urlConnection.setRequestProperty("Authorization", "key=" + API_KEY);
                    urlConnection.setDoOutput(true);
                    // 5. Add JSON data into POST request body
                    JSONObject obj = new JSONObject("\"time_to_live\": 0,\"delay_while_idle\": true,\"data\":\"message\":" + msg + ",\"registration_ids\":[" + ControllerRegistrationId + "]");
                    // 6. Get connection output stream
                    OutputStreamWriter out = new OutputStreamWriter(urlConnection.getOutputStream());
                    out.write(obj.toString());
                    out.close();
                    // 6. Get the response
                    int responseCode = urlConnection.getResponseCode();

                    BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
                    String inputLine;
                    StringBuffer response = new StringBuffer();
                    while ((inputLine = in.readLine()) != null)
                        response.append(inputLine);
                    
                    in.close();
                    Log.d("GCM getResponseCode:", new Integer(responseCode).toString());
                else
                    Log.d("GCM_Sender:","Field REGISTRATION_TABLE is null");
                
            else 
                Log.d("GCM_Sender:","There is no Registration ID in DB ,please sync devices");
            
         catch (Exception e) 
            e.printStackTrace();
            //MessageSender.getInstance().sendMessage(msg, Commands.SMS_MESSAGE);
         
    

    //Called when service is no longer alive
    @Override
    public void onDestroy() 
        super.onDestroy();
        //Do a log that GCM_Sender service has been destroyed
        Log.d("Action Service", "GCM_Sender Service Destroyed");
    

******************接收GCM消息**************

public class GCM_Receiver extends WakefulBroadcastReceiver 
    public static final String RETRY_ACTION ="com.google.android.c2dm.intent.RETRY";
    public static final String REGISTRATION ="com.google.android.c2dm.intent.REGISTRATION";
    public SharedPreferences preferences;

    //Processes Gcm message .
    @Override
    public void onReceive(Context context, Intent intent) 
        ComponentName comp = new ComponentName(context.getPackageName(),
                GCMNotificationIntentService.class.getName());
        //Start GCMNotificationIntentService to handle gcm message asynchronously
        startWakefulService(context, (intent.setComponent(comp)));
        setResultCode(Activity.RESULT_OK);

        /*//Check if DatabaseService is running .
        if(!DatabaseService.isServiceRunning) 
            Intent dbService = new Intent(context,DatabaseService.class);
            context.startService(dbService);
        */
        //Check if action is RETRY_ACTION ,if it is then do gcm registration again .
        if(intent.getAction().equals(RETRY_ACTION)) 
            String registrationId = intent.getStringExtra("registration_id");

            if(TextUtils.isEmpty(registrationId))
                DeviceRegistrar.getInstance().register(context);
            else 
                //Save registration id to prefs .
                preferences = PreferenceManager.getDefaultSharedPreferences(context);
                SharedPreferences.Editor editor = preferences.edit();
                editor.putString("BLACKBOX_REG_ID",registrationId);
                editor.commit();
            
         else if (intent.getAction().equals(REGISTRATION)) 
        

    


//Processes gcm messages asynchronously .
public class GCMNotificationIntentService extends IntentService
    public static final int NOTIFICATION_ID = 1;
    private NotificationManager mNotificationManager;
    String gcmData;
    private final String TAG = "GCMNotificationIntentService";

    //Constructor with super().
    public GCMNotificationIntentService() 
        super("GcmIntentService");
    

    //Called when startService() is called by its Client .
    //Processes gcm messages .
    @Override
    protected void onHandleIntent(Intent intent) 

        Log.d("GCMNotificationIntentService", "GCMNotificationIntentService Started");
        Bundle extras = intent.getExtras();
        //Get instance of GoogleCloudMessaging .
        GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this);
        //Get gcm message type .
        String messageType = gcm.getMessageType(intent);

        if (!extras.isEmpty()) 
            if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR
                    .equals(messageType)) 
                sendNotification("Send error: " + extras.toString());
             else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED
                    .equals(messageType)) 
                sendNotification("Deleted messages on server: "
                        + extras.toString());
             else if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE
                    .equals(messageType)) 
                Log.i(TAG, "Completed work @ " + SystemClock.elapsedRealtime());

                gcmData = extras.getString("message");
                Intent actionService = new Intent(getApplicationContext(),Action.class);    
                actionService.putExtra("data", gcmData);
                //start Action service .
                startService(actionService);

                //Show push notification .
                sendNotification("Action: " + gcmData);
                //Process received gcmData.

                Log.d(TAG,"Received Gcm Message from Controller : " + extras.getString("message"));
            
        
        GCM_Receiver.completeWakefulIntent(intent);
    

    //Shows notification on device notification bar .
    private void sendNotification(String msg) 
        mNotificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
        Intent notificationIntent = new Intent(this, BlackboxStarter.class);
        //Clicking on GCM notification add new layer of app.
        notificationIntent.setFlags( Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0,notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
                this).setSmallIcon(R.drawable.gcm_cloud)
                .setContentTitle("Notification from Controller")
                .setStyle(new NotificationCompat.BigTextStyle().bigText(msg))
                .setContentText(msg);
        mBuilder.setContentIntent(contentIntent);
        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
        //Play default notification
        try 
            Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
            Ringtone r = RingtoneManager.getRingtone(getApplicationContext(), notification);
            r.play();
         catch (Exception e) 
            e.printStackTrace();
        
    

    //Called when service is no longer be available .
    @Override
    public void onDestroy() 
        // TODO Auto-generated method stub
        super.onDestroy();
        Log.d("GCMNotificationIntentService", "GCMNotificationIntentService Destroyed");
    


【讨论】:

欢迎来到***。您需要将实际的解决方案和最好的代码放在您的答案中,以便将其视为一个。否则,您将获得投票和/或完全删除您的答案。如果你想分享你的意见,你可以评论(达到声望50后)。查看帮助部分了解更多详情【参考方案4】:

根据October 2, 2018 上更新的新文档,您必须按以下方式发送帖子请求

https://fcm.googleapis.com/fcm/send
Content-Type:application/json
Authorization:key=AIzaSyZ-1u...0GBYzPu7Udno5aA //Server key


    "to": "sent device's registration token",
    "data": 
       "hello": "message from someone",
    

要获取设备的注册令牌,请扩展 FirebaseMessagingService 并覆盖 onNewToken(String token) 更多信息请参考文档https://firebase.google.com/docs/cloud-messaging/android/device-group

【讨论】:

【参考方案5】:

我迟到了,但上面的解决方案帮助我写下了这个简单的答案,您可以从 android 应用程序直接将您的消息发送到 android 设备,这是我所做的简单实现,它对我很有用。

    编译android volley库

    compile 'com.android.volley:volley:1.0.0'
    

    只需复制粘贴这个简单的功能;)你的生活就会变得像黄油里的刀一样顺利。 :D

    public static void sendPushToSingleInstance(final Context activity, final HashMap dataValue /*your data from the activity*/, final String instanceIdToken /*firebase instance token you will find in documentation that how to get this*/ ) 
    
    
    final String url = "https://fcm.googleapis.com/fcm/send";
    StringRequest myReq = new StringRequest(Request.Method.POST,url,
            new Response.Listener<String>() 
                @Override
                public void onResponse(String response) 
                    Toast.makeText(activity, "Bingo Success", Toast.LENGTH_SHORT).show();
                
            ,
            new Response.ErrorListener() 
                @Override
                public void onErrorResponse(VolleyError error) 
                    Toast.makeText(activity, "Oops error", Toast.LENGTH_SHORT).show();
                
            ) 
    
        @Override
        public byte[] getBody() throws com.android.volley.AuthFailureError 
            Map<String, Object> rawParameters = new Hashtable();
            rawParameters.put("data", new JSONObject(dataValue));
            rawParameters.put("to", instanceIdToken);
            return new JSONObject(rawParameters).toString().getBytes();
        ;
    
        public String getBodyContentType()
        
            return "application/json; charset=utf-8";
        
        @Override
        public Map<String, String> getHeaders() throws AuthFailureError 
            HashMap<String, String> headers = new HashMap<String, String>();
            headers.put("Authorization", "key="+YOUR_LEGACY_SERVER_KEY_FROM_FIREBASE_CONSOLE);
            headers.put("Content-Type","application/json");
            return headers;
        
    
    ;
    
    Volley.newRequestQueue(activity).add(myReq);
    
    

注意 如果您想向主题发送消息,则可以将参数 instanceIdToken 更改为 /topics/topicName 之类的内容。 groups 的实现是相同的,但您只需要注意参数。 checkout Firebase documentation and you can pass those parameters. 如果您遇到任何问题,请告诉我。

【讨论】:

在 json 中传递错误格式得到错误号 400 请用简单的“datavalue”测试一下。 data="title":"Testing", to=diduAXILZtY:APA91bHcIBllVz_liOnF-RQa4ei 这是我要发送的内容,对值进行了修剪 请在您的 getBody() 函数中显示此行的最终结果。 "新的 JSONObject(rawParameters).toString()" . @Override public byte[] getBody() throws com.android.volley.AuthFailureError Map rawParameters = new Hashtable(); rawParameters.put("data", new JSONObject(dataValue).toString()); rawParameters.put("to", instanceIdToken); System.out.println(rawParameters.toString());返回新的 JSONObject(rawParameters).toString().getBytes(); ;

以上是关于如何使用 Firebase 消息传递一对一消息的主要内容,如果未能解决你的问题,请参考以下文章

用于一对一消息传递的 Firebase 数据库结构?

如何使用 Firebase 规则从一侧删除消息?

如何使用 Nuxt.js 实现 Firebase 云消息传递 (FCM)

如何使用 firebase 消息传递 9.0 版本 最新版本

如何使用 Firebase 云消息传递和 Liferay 推送通知

如何使用 FCM(Firebase 云消息传递)制作紧凑通知?