【问题标题】:How to send device to device messages using Firebase Cloud Messaging?如何使用 Firebase Cloud Messaging 向设备发送消息?
【发布时间】:2016-09-22 23:25:33
【问题描述】:

搜索文档后,我找不到任何关于如何在不使用外部服务器的情况下使用 FCM 向设备发送消息的信息。

例如,如果我正在创建一个聊天应用程序,我将需要向用户发送有关未读消息的推送通知,因为他们不会一直在线,而且我无法在后台拥有一个始终如一的持久服务连接到实时数据库,因为那样会占用大量资源。

那么当某个用户“B”向他/她发送聊天消息时,我将如何向用户“A”发送推送通知?我是否需要一个外部服务器来完成这项工作,还是仅使用 Firebase 服务器就可以完成?

【问题讨论】:

  • 我还没用过 FCM,....但是我用过 GCM....假设 FCM 和 GCM 差不多.....设备 A 将消息发送到将推送的服务器向设备 B 发送消息。查看firebase.google.com/support/faq/#messaging-difference
  • @j4rey89 是的,我知道可以使用外部服务器来完成。我在问如果没有它是否可以完成,因为这需要我维护和支付两台服务器而不是一台。
  • @Suyash 必须运行您自己的服务器才能在您的设备之间发送 FCM 消息。如果您担心运行服务器的成本,可以开始部署到具有免费配额的 Openshift Online (PaaS) 或 Google AppEngine(PaaS)。
  • @j4rey89 MrBrightside:听起来像是一个答案。 :-)

标签: firebase firebase-cloud-messaging


【解决方案1】:

更新:现在可以使用 firebase 云功能作为服务器来处理推送通知。查看他们的文档here

============

根据文档,您必须实现一个服务器来处理设备到设备通信中的推送通知。

在您编写使用 Firebase 云消息传递的客户端应用之前,您必须拥有一个满足以下条件的应用服务器:

...

您需要决定要使用哪种 FCM 连接服务器协议来使您的应用服务器能够与 FCM 连接服务器进行交互。请注意,如果要使用来自客户端应用程序的上游消息传递,则必须使用 XMPP。有关此问题的更详细讨论,请参阅Choosing an FCM Connection Server Protocol

如果您只需要从服务器向您的用户发送基本通知。您可以使用他们的无服务器解决方案,Firebase Notifications

在此处查看 FCM 和 Firebase 通知之间的比较: https://firebase.google.com/support/faq/#messaging-difference

【讨论】:

  • 好答案。你知道任何可以解释如何做到这一点的教程或视频吗?谢谢
  • 请您帮我理解一下。据我了解,如果我需要将直接消息从一个 uset 发送到另一个,我必须使用 HTTP 并将此消息发送到我的服务器,下一个服务器将使用 FCM 将通知发送到 recipent,从而 retrive 检索具有发件人 ID 的数据。下一步配方连接到 FCM 并在 ID 的帮助下从 FCM DB 中检索所有数据?这样吗?
  • 完美答案,我已经研究了这两天。关于 FCM 和服务器是否需要的非常完整的信息。谢谢!。
【解决方案2】:

使用链接 https://fcm.googleapis.com/fcm/send 和所需的标头和数据发出 HTTP POST 请求对我有帮助。在下面的代码中 Constants.LEGACY_SERVER_KEY 是一个局部类变量,您可以在 Firebase 项目 Settings->Cloud Messaging->Legacy Server key 中找到它。您需要在下面引用HERE.的代码sn-p中传递设备注册令牌,即regToken

最后你需要 okhttp 库依赖才能让这个 sn-p 工作。

public static final MediaType JSON
        = MediaType.parse("application/json; charset=utf-8");
private void sendNotification(final String regToken) {
    new AsyncTask<Void,Void,Void>(){
        @Override
        protected Void doInBackground(Void... params) {
            try {
                OkHttpClient client = new OkHttpClient();
                JSONObject json=new JSONObject();
                JSONObject dataJson=new JSONObject();
                dataJson.put("body","Hi this is sent from device to device");
                dataJson.put("title","dummy title");
                json.put("notification",dataJson);
                json.put("to",regToken);
                RequestBody body = RequestBody.create(JSON, json.toString());
                Request request = new Request.Builder()
                        .header("Authorization","key="+Constants.LEGACY_SERVER_KEY)
                        .url("https://fcm.googleapis.com/fcm/send")
                        .post(body)
                        .build();
                Response response = client.newCall(request).execute();
                String finalResponse = response.body().string();
            }catch (Exception e){
                //Log.d(TAG,e+"");
            }
            return null;
        }
    }.execute();

}

如果你想向特定主题发送消息,请像这样替换 json 中的regToken

json.put("to","/topics/foo-bar")

别忘了在你的 AndroidManifest.xml 中添加 INTERNET 权限。

重要提示: - 使用上述代码意味着您的服务器密钥位于客户端应用程序中。这很危险,因为有人可以深入您的应用程序并获取服务器密钥以向您的用户发送恶意通知。

【讨论】:

  • 嗨,有没有可能将消息发送到订阅的特定频道?
  • 缺点是您的服务器密钥驻留在客户端应用程序中。这很危险,因为有人可以深入您的应用程序并获取服务器密钥以向您的用户发送恶意通知。这就是为什么你永远不应该这样做。
  • @kirtan403 对客户端服务器密钥的强加密可以阻止这种情况???
  • @Mr.Popular 也许,但是如果有人能够反编译您的代码(当然,他们可以),那么他们可以获取您用来加密服务器密钥的内容,并获取您的服务器密钥。然后他们可以不受任何限制地向任何人发送通知。所以将服务器密钥放在客户端是一个非常糟糕的主意。一个非常非常糟糕的主意...
  • @Tabish 请使用 remoteMessage.getNotification()。我们不在这里发送数据。
【解决方案3】:

您可以使用 Volly Jsonobject 请求来做到这一点......

首先按照以下步骤操作:

1 复制旧服务器密钥并将其存储为 Legacy_SERVER_KEY

旧版服务器密钥

你可以在图片中看到如何获得

2 你需要 Volley 依赖

编译'com.mcxiaoke.volley:library:1.0.19'

发送推送代码:-

private void sendFCMPush() {

    String Legacy_SERVER_KEY = YOUR_Legacy_SERVER_KEY;
    String msg = "this is test message,.,,.,.";
    String title = "my title";
    String token = FCM_RECEIVER_TOKEN;

    JSONObject obj = null;
    JSONObject objData = null;
    JSONObject dataobjData = null;

    try {
        obj = new JSONObject();
        objData = new JSONObject();

        objData.put("body", msg);
        objData.put("title", title);
        objData.put("sound", "default");
        objData.put("icon", "icon_name"); //   icon_name image must be there in drawable
        objData.put("tag", token);
        objData.put("priority", "high");

        dataobjData = new JSONObject();
        dataobjData.put("text", msg);
        dataobjData.put("title", title);

        obj.put("to", token);
        //obj.put("priority", "high");

        obj.put("notification", objData);
        obj.put("data", dataobjData);
        Log.e("!_@rj@_@@_PASS:>", obj.toString());
    } catch (JSONException e) {
        e.printStackTrace();
    }

    JsonObjectRequest jsObjRequest = new JsonObjectRequest(Request.Method.POST, Constants.FCM_PUSH_URL, obj,
            new Response.Listener<JSONObject>() {
                @Override
                public void onResponse(JSONObject response) {
                    Log.e("!_@@_SUCESS", response + "");
                }
            },
            new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Log.e("!_@@_Errors--", error + "");
                }
            }) {
        @Override
        public Map<String, String> getHeaders() throws AuthFailureError {
            Map<String, String> params = new HashMap<String, String>();
            params.put("Authorization", "key=" + Legacy_SERVER_KEY);
            params.put("Content-Type", "application/json");
            return params;
        }
    };
    RequestQueue requestQueue = Volley.newRequestQueue(this);
    int socketTimeout = 1000 * 60;// 60 seconds
    RetryPolicy policy = new DefaultRetryPolicy(socketTimeout, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT);
    jsObjRequest.setRetryPolicy(policy);
    requestQueue.add(jsObjRequest);
}

只需调用 sendFCMPush();

【讨论】:

  • 嗨,有没有可能将消息发送到订阅的特定频道?
  • 是的,您可能必须为其添加标志,这取决于您可以向订阅用户发送推送
  • @RjzSatvara 如果接收方手机上没有运行应用程序怎么办?它会收到消息吗?提前致谢
  • @Jaco ,这是不可能的。你必须通过其他方式来管理它。
【解决方案4】:

1) 订阅相同的主题名称,例如:

  • ClientA.subcribe("to/topic_users_channel")
  • ClientB.subcribe("to/topic_users_channel")

2) 在应用程序内部发送消息

GoogleFirebase : How-to send topic messages

【讨论】:

  • 这是否还需要在客户端使用授权密钥?这使它不安全。另外我什至不知道为每个用户创建一个单独的主题是否是个好主意。
  • 好主意,但是:主题消息针对吞吐量而不是延迟进行了优化。为了快速、安全地向单个设备或小组设备传递信息,请将消息定位到注册令牌,而不是主题。
  • @Maxim Firsoff-如何从 FCM 控制台或任何其他方式创建主题?
  • @AjaySharma 我记得,FMC 控制台没有工具,您可以通过编程方式创建主题(见上面我的伪代码)。
【解决方案5】:

是的,可以在没有任何服务器的情况下执行此操作。您可以创建设备组客户端,然后在组中交换消息。但是有一些限制:

  1. 您必须在设备上使用同一个 Google 帐户
  2. 您无法发送高优先级消息

参考:Firebase doc 请参阅“在 Android 客户端应用程序上管理设备组”部分

【讨论】:

  • 你还需要一个服务器来发送群消息
  • 没办法。群组中的任何设备都可以发送消息
  • 来自文档:授权:key=API_KEY 您仍然需要服务器密钥。所以这个解决方案不适合生产
  • API 密钥是 Google 帐户,通信仅限于单个用户帐户。尝试之前发表评论。
【解决方案6】:

Google Cloud Functions 现在可以在没有应用服务器的情况下从设备到设备发送推送通知。 我已经制作了云功能,当新消息添加到数据库中时触发

node.js代码

'use strict';

const functions = require('firebase-functions');
const admin = require('firebase-admin'); admin.initializeApp();

exports.sendNotification = functions.database.ref('/conversations/{chatLocation}/{messageLocation}')
  .onCreate((snapshot, context) => {
      // Grab the current value of what was written to the Realtime Database.
      const original = snapshot.val();

       const toIDUser = original.toID;
       const isGroupChat = original.isGroupChat;

       if (isGroupChat) {
       const tokenss =  admin.database().ref(`/users/${toIDUser}/tokens`).once('value').then(function(snapshot) {

// Handle Promise
       const tokenOfGroup = snapshot.val()

      // get tokens from the database  at particular location get values 
       const valuess = Object.keys(tokenOfGroup).map(k => tokenOfGroup[k]);

     //console.log(' ____________ddd((999999ddd_________________ ' +  valuess );
    const payload = {
       notification: {
                 title:   original.senderName + " :- ",
                 body:    original.content
    }
  };

  return admin.messaging().sendToDevice(valuess, payload);



}, function(error) {

  console.error(error);
});

       return ;
          } else {
          // get token from the database  at particular location
                const tokenss =  admin.database().ref(`/users/${toIDUser}/credentials`).once('value').then(function(snapshot) {
                // Handle Promise
  // The Promise was "fulfilled" (it succeeded).

     const credentials = snapshot.val()



    // console.log('snapshot ......snapshot.val().name****^^^^^^^^^^^^kensPromise****** :- ', credentials.name);
     //console.log('snapshot.....****snapshot.val().token****^^^^^^^^^^^^kensPromise****** :- ', credentials.token);


     const deviceToken = credentials.token;

    const payload = {
       notification: {
                 title:   original.senderName + " :- ",
                 body:    original.content
    }
  };

  return admin.messaging().sendToDevice(deviceToken, payload);


}, function(error) {

  console.error(error);
});


          }





  return ;


    });

【讨论】:

    【解决方案7】:

    如果您有要向其发送通知的设备的 fcm(gcm) 令牌。这只是发送通知的发布请求。

    https://github.com/prashanthd/google-services/blob/master/android/gcm/gcmsender/src/main/java/gcm/play/android/samples/com/gcmsender/GcmSender.java

    【讨论】:

    • 是的,但这仍然需要我们自己的外部服务器,对吧?因为我们不应该将 API_KEY 直接嵌入到我们的客户端中。我的问题是,如果没有外部服务器,这是否可行,目前其他回复并未建议这样做。
    【解决方案8】:

    Google Cloud Functions 现在可以在没有应用服务器的情况下从设备到设备发送推送通知。

    From the relevant page 在 Google Cloud Functions 上:

    开发者可以使用 Cloud Functions 来保持用户的参与度并保持最新状态 日期与有关应用程序的相关信息。例如,考虑一个 允许用户在应用程序中关注彼此的活动的应用程序。 在这样的应用程序中,由实时数据库触发的函数写入 存储新的关注者可以创建 Firebase 云消息传递 (FCM) 通知让适当的用户知道他们已经获得 新的追随者。

    例子:

    1. 该函数在写入存储关注者的实时数据库路径时触发。

    2. 该函数编写一条消息以通过 FCM 发送。

    3. FCM 将通知消息发送到用户的设备。

    Here is a demo project 用于使用 Firebase 和 Google Cloud Functions 发送设备到设备的推送通知。

    【讨论】:

      【解决方案9】:

      在我的例子中,我将retrofit 与此类消息一起使用:

      public class Message {
      
          private String to;
          private String collapseKey;
          private Notification notification;
          private Data data;
      
          public Message(String to, String collapseKey, Notification notification, Data data) {
              this.to = to;
              this.collapseKey = collapseKey;
              this.notification = notification;
              this.data = data;
          }
      }
      

      数据

      public class Data {
      
          private String body;
          private String title;
          private String key1;
          private String key2;
      
          public Data(String body, String title, String key1, String key2) {
              this.body = body;
              this.title = title;
              this.key1 = key1;
              this.key2 = key2;
          }
      }
      

      通知

      public class Notification {
      
          private String body;
          private String title;
      
          public Notification(String body, String title) {
              this.body = body;
              this.title = title;
          }
      }
      

      这个电话

      private void sentToNotification() {
          String to = "YOUR_TOKEN";
          String collapseKey = "";
          Notification notification = new Notification("Hello bro", "title23");
          Data data = new Data("Hello2", "title2", "key1", "key2");
          Message notificationTask = new Message(to, collapseKey, notification, data);
      
          Retrofit retrofit = new Retrofit.Builder()
                  .baseUrl("https://fcm.googleapis.com/")//url of FCM message server
                  .addConverterFactory(GsonConverterFactory.create())//use for convert JSON file into object
                  .build();
      
          ServiceAPI api = new retrofit.create(ServiceAPI.class);
      
          Call<Message> call = api .sendMessage("key=YOUR_KEY", notificationTask);
      
          call.enqueue(new Callback<Message>() {
              @Override
              public void onResponse(Call<Message> call, retrofit2.Response<Message> response) {
                  Log.d("TAG", response.body().toString());
              }
      
              @Override
              public void onFailure(Call<Message> call, Throwable t) {
      
                  Log.e("TAG", t.getMessage());
              }
          });
      }
      

      我们的 ServiceAPI

      public interface ServiceAPI {
          @POST("/fcm/send")
          Call<Message> sendMessage(@Header("Authorization") String token, @Body Message message);
      }
      

      【讨论】:

        【解决方案10】:

        您可以使用改造。为设备订阅主题新闻。从一台设备向另一台设备发送通知。

        public void onClick(View view) {
        
            HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
            logging.setLevel(HttpLoggingInterceptor.Level.BODY);
        
            OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
            httpClient.addInterceptor(new Interceptor() {
                @Override
                public okhttp3.Response intercept(Chain chain) throws IOException {
                    Request original = chain.request();
        
                    // Request customization: add request headers
                    Request.Builder requestBuilder = original.newBuilder()
                            .header("Authorization", "key=legacy server key from FB console"); // <-- this is the important line
                    Request request = requestBuilder.build();
                    return chain.proceed(request);
                }
            });
        
            httpClient.addInterceptor(logging);
            OkHttpClient client = httpClient.build();
        
            Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl("https://fcm.googleapis.com")//url of FCM message server
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create())//use for convert JSON file into object
                    .build();
        
            // prepare call in Retrofit 2.0
            FirebaseAPI firebaseAPI = retrofit.create(FirebaseAPI.class);
        
            //for messaging server
            NotifyData notifydata = new NotifyData("Notification title","Notification body");
        
            Call<Message> call2 = firebaseAPI.sendMessage(new Message("topic or deviceID", notifydata));
        
            call2.enqueue(new Callback<Message>() {
                @Override
                public void onResponse(Call<Message> call, Response<Message> response) {
        
                    Log.d("Response ", "onResponse");
                    t1.setText("Notification sent");
        
                }
        
                @Override
                public void onFailure(Call<Message> call, Throwable t) {
                    Log.d("Response ", "onFailure");
                    t1.setText("Notification failure");
                }
            });
        }
        

        POJO

        public class Message {
            String to;
            NotifyData notification;
        
            public Message(String to, NotifyData notification) {
                this.to = to;
                this.notification = notification;
            }
        
        }
        

        public class NotifyData {
            String title;
            String body;
        
            public NotifyData(String title, String body ) {
        
                this.title = title;
                this.body = body;
            }
        
        }
        

        和 FirebaseAPI

        public interface FirebaseAPI {
        
            @POST("/fcm/send")
            Call<Message> sendMessage(@Body Message message);
        
        }
        

        【讨论】:

          【解决方案11】:

          您可以使用 firebase 实时数据库来执行此操作。您可以创建用于存储聊天的数据结构,并为两个用户的对话线程添加观察者。它仍然是设备 - 服务器 - 设备架构,但在这种情况下,开发人员没有额外的服务器。这使用了 firebase 服务器。您可以在此处查看教程(忽略 UI 部分,不过,这也是聊天 UI 框架的一个很好的起点)。

          Firebase Realtime Chat

          【讨论】:

          • 用户不会一直使用应用程序,我们也不能在后台使用firebase实时数据库,因为它与服务器保持持久的套接字连接,这对电池来说太重了设备。
          • 我可以使用 Smack 库在设备和通知之间发送 Firebase 消息。我没有在我的 android 代码中实现任何外部服务器。 Smack 使用 XMPP 协议管理消息节的连接和传入/传出。
          【解决方案12】:

          这里介绍了如何在没有第二台服务器(除了 Firebase 的服务器)的情况下获取通知。所以我们只使用 Firebase,没有额外的服务器。

          1. 在移动应用代码中,我们通过 here 等 Android 库创建自己的通知功能,不使用 Firebase 库 like here,不使用 Firebase Cloud 消息传递。 以下是 Kotlin 的示例:

            私人娱乐通知(){ createNotificationChannel()

             val intent = Intent(this, LoginActivity::class.java).apply {
                 flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
             }
             val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
            
             val notificationBuilder = NotificationCompat.Builder(this, "yuh_channel_id")
                 .setSmallIcon(R.drawable.ic_send)
                 .setContentText("yuh")
                 .setContentText("yuh")
                 .setAutoCancel(true)
                 .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                 .setContentIntent(pendingIntent)
             val notificationManager =
                 getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
             notificationManager.notify(0, notificationBuilder.build())
            
             with(NotificationManagerCompat.from(this)) {
                 // notificationId is a unique int for each notification that you must define
                 notify(0, notificationBuilder.build())
             }
            

            }

             private fun createNotificationChannel() {
             // Create the NotificationChannel, but only on API 26+ because
             // the NotificationChannel class is new and not in the support library
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                 val name = "yuh_channel"
                 val descriptionText = "yuh_description"
                 val importance = NotificationManager.IMPORTANCE_DEFAULT
                 val CHANNEL_ID = "yuh_channel_id"
                 val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
                     description = descriptionText
                 }
                 // Register the channel with the system
                 val notificationManager: NotificationManager =
                     getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
                 notificationManager.createNotificationChannel(channel)
             }
            
          1. 在 Firebase 数据库中,创建“待处理通知”集合。文档应包含用户名(将通知发送到)和源名称(用户在点击通知时应该去哪里)。

          2. 在应用程序代码中,实现将新记录添加到 Pending Notifications 集合的选项。例如。如果用户 A 向用户 B 发送消息,则在集合中创建 ID 为用户 B(将被通知)的文档。

          3. 在应用代码中,设置后台(当应用对用户不可见时)服务。喜欢here。在后台服务中,为“Notifications Pending”集合中的更改设置侦听器。当带有用户 id 的新记录进入集合时,调用第 1 段中创建的通知函数supra,并从集合中删除后续记录。

          【讨论】:

            【解决方案13】:

            所以我在这里有了一个想法。请参阅:如果 FCM 和 GCM 具有指向 http 请求的端点,我们可以在其中发送带有我们的消息数据的 post json,包括我们希望传递此消息的设备的令牌。

            那么,为什么不向 Firebase 服务器发送一个帖子,同时将此通知发送给用户 B?你明白 ?

            因此,如果用户在后台使用您的应用,您可以发送消息并与通话帖子聊天,以确保发送通知。我也很快需要它,我稍后会测试。你说什么?

            【讨论】:

            • FCM 已经有一个端点,请参阅here。但由于它需要服务器 api 密钥,因此无法直接在我们的客户端中使用它。即使它可以公开访问,也会导致安全问题,因为任何用户都可以向任何人发送任何 FCM 消息。
            【解决方案14】:

            最简单的方法:

            void sendFCMPush(String msg,String token) {
                HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
                logging.setLevel(HttpLoggingInterceptor.Level.BODY);
            
                OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
                httpClient.addInterceptor(new Interceptor() {
                    @Override
                    public okhttp3.Response intercept(Chain chain) throws IOException {
                        Request original = chain.request();
            
                        // Request customization: add request headers
                        Request.Builder requestBuilder = original.newBuilder()
                                .header("Authorization", "key="+Const.FIREBASE_LEGACY_SERVER_KEY); // <-- this is the important line
                        Request request = requestBuilder.build();
                        return chain.proceed(request);
                    }
                });
            
                httpClient.addInterceptor(logging);
                OkHttpClient client = httpClient.build();
            
                Retrofit retrofit = new Retrofit.Builder()
                        .baseUrl("https://fcm.googleapis.com/")//url of FCM message server
                        .client(client)
                        .addConverterFactory(GsonConverterFactory.create())//use for convert JSON file into object
                        .build();
            
                // prepare call in Retrofit 2.0
                FirebaseAPI firebaseAPI = retrofit.create(FirebaseAPI.class);
            
                //for messaging server
                NotifyData notifydata = new NotifyData("Chatting", msg);
            
                Call<Message> call2 = firebaseAPI.sendMessage(new Message(token, notifydata));
            
                call2.enqueue(new Callback<Message>() {
                    @Override
                    public void onResponse(Call<Message> call, retrofit2.Response<Message> response) {
                        Log.e("#@ SUCCES #E$#", response.body().toString());
                    }
            
                    @Override
                    public void onFailure(Call<Message> call, Throwable t) {
            
                        Log.e("E$ FAILURE E$#", t.getMessage());
                    }
                });
            }
            

            创建类来制作对象:

            public class Message {
            String to;
            NotifyData data;
            
            public Message(String to, NotifyData data) {
                this.to = to;
                this.data = data;
            }
            }
            

            创建类来制作对象:

            public class Notification {
            String title;
            String message;
            enter code here`enter code here`
            public Notification(String title, String message) {
                this.title = title;
                this.message = message;
            }
            }
            

            【讨论】:

            • Const.FIREBASE_LEGACY_SERVER_KEY 在客户端代码中使用是不安全的。请在发帖前至少阅读其他回复和 cmets。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-08-07
            • 2014-02-21
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多