FCM 云消息传递(Firebase Cloud Messaging)是一种免费的跨平台消息传递解决方案,可以让您向客户端应用程序发送推送消息。使用FCM,您能够将推送通知发送到单个设备,设备组或订阅特定“主题”的设备。android
使用 FCM,您能够向客户端发送两种类型的消息:golang
应用在后台运行时,通知消息将被传递至通知面板。应用在前台运行时,消息由回调函数处理。web
接收同时包含通知和数据有效负载的消息时的应用行为取决于应用是在后台仍是前台运行 - 特别是在接收时应用是否处于活动状态。算法
如下是包含 notification 键和 data 键的 JSON 格式的消息:编程
{ "message":{ "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...", "notification":{ "title":"Portugal vs. Denmark", "body":"great match!" }, "data" : { "Nick" : "Mario", "Room" : "PortugalVSDenmark" } } }
若是须要执行下列操做,请使用针对具体平台的字段:json
每当您想仅向特定平台发送值时,请不要使用通用字段,而是使用针对具体平台的字段。例如,要仅向 iOS 和网页发送通知而不向 Android 发送通知,您必须针对 iOS 和网页各使用一组字段。后端
示例:包含针对具体平台的递送选项的通知消息api
如下 v1 发送请求会向全部平台发送通用的通知标题和内容,但也会发送一些针对具体平台的覆盖内容。具体而言,该请求会:缓存
{ "message":{ "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...", "notification":{ "title":"Match update", "body":"Arsenal goal in added time, score is now 3-0" }, "android":{ "ttl":"86400s", "notification"{ "click_action":"OPEN_ACTIVITY_1" } }, "apns": { "headers": { "apns-priority": "5", }, "payload": { "aps": { "category": "NEW_MESSAGE_CATEGORY" } } }, "webpush":{ "headers":{ "TTL":"86400" } } } }
咱们的目标是始终提供经过 FCM 发送的每条消息。可是,传递每条消息有时会致使总体用户体验不佳。在其余状况下,咱们须要提供边界以确保 FCM 为全部发送者提供可扩缩的服务。安全
向单一设备发送消息的最大速率
您能够向单一设备发送最多 240 条消息/分钟和 5000 条消息/小时。这个高阈值意味着容许短时间的流量突发,例如当用户经过聊天快速互动时。此限制可防止发送逻辑时发生的错误无心中耗尽设备上的电池电量。
警告:不要按期发送接近此最大速率的消息。这可能会浪费最终用户的资源,而且您的应用可能会被标记为滥用。
上行消息限额
咱们将每一个项目的上行消息限制为 15000 条/分钟,以免上行目标服务器过载。
咱们将每台设备的上行消息限制为 1000 条/分钟,以防止因不良应用行为致使电池电量耗尽。
主题消息限额
主题订阅添加/移除率限制为每一个项目 3000 QPS。
咱们将每一个项目正在进行的主题扇出数量限制为 1000。以后,咱们可能会拒绝其余扇出请求,直到某些扇出完成为止。
实际可实现的主题扇出率受同时请求扇出的项目数量的影响。单个项目的扇出率为 10000 QPS 并不罕见,但这个数字不是一项保证,而是系统总负载的结果。值得注意的是,可用的扇出容量在项目之间划分,而不是在扇出请求之间划分。所以,若是您的项目有两个正在进行的扇出,那么每一个扇出只能确保可用扇出率的一半。最大化扇出速度的推荐方法是一次只有一个进行中的活动扇出。
应用服务器或受信任的服务器环境向 FCM 后端发送消息请求,而后 FCM 后端再将消息发送到用户设备上运行的客户端应用。
受信任的服务器环境的要求
您的应用服务器环境必须符合如下条件:
建议使用 Firebase Admin SDK,由于它支持各类流行的编程语言,并且处理身份验证和受权的方法很是便捷。
Admin FCM API 可处理后端身份验证工做,同时便于发送消息和管理主题订阅。使用 Firebase Admin SDK,您能够执行如下操做:
目前 FCM 提供如下原始服务器协议:
您的应用服务器能够分开使用或同时使用这些协议。由于在向多个平台发送消息方面 FCM HTTP v1 API 是最新、最灵活的协议,所以推荐尽量使用此协议。若是您须要从设备到服务器的上行消息传递功能,则需实现 XMPP 协议。
XMPP 消息传递与 HTTP 消息传递具备如下差别:
上行/下行消息
消息传递(同步或异步)
JSON
纯文本
向多个注册令牌发送多播下行消息。
以上信息均摘自官网,更多信息请见官网。
以下图所示,FCM充当消息发送者和客户端之间的中介。客户端应用程序是在设备上运行的启用FCM的应用程序。应用服务器(由您或您的公司提供)是客户端应用经过FCM与之通讯的启用FCM的服务器。与GCM不一样,FCM使您能够经过Firebase控制台通知GUI直接向客户端应用程序发送消息
客户端应用必须首先向FCM注册,而后才能进行消息传递。客户端应用程序必须完成下图中显示的注册步骤:
应用程序服务器缓存注册令牌,以便与客户端应用程序进行后续通讯。应用服务器能够向客户端应用程序发送确认,以指示已收到注册令牌。在进行此握手以后,客户端应用程序能够从应用服务器接收消息(或向其发送消息)。若是旧令牌被泄露,则客户端应用程序可能会收到新的注册令牌(有关应用程序如何接收注册令牌更新的示例,请参阅使用FCM进行远程通知)。
下图说明了Firebase Cloud Messaging如何存储和转发下游消息:
当应用服务器向客户端应用程序发送下游消息时,它将使用上图中枚举的如下步骤:
主题消息使应用服务器能够向已选择加入特定主题的多个设备发送消息。您还能够经过Firebase控制台通知GUI撰写和发送主题消息。 FCM处理主题消息到订阅客户端的路由和传递。此功能可用于天气警报,股票报价和标题新闻等消息。
主题消息中使用如下步骤(在客户端应用程序获取注册令牌以后,如前所述)
当下游消息从应用服务器发送到客户端应用程序时,应用服务器将消息发送到Google提供的FCM链接服务器;反过来,FCM链接服务器将消息转发到运行客户端应用程序的设备。消息能够经过HTTP或XMPP(可扩展消息传递和在线协议)发送。因为客户端应用程序并不是始终链接或正在运行,所以FCM链接服务器会将消息排入并存储,并在从新链接并可用时将其发送到客户端应用程序。一样,若是应用服务器不可用,FCM会将客户端应用程序的上游消息排入应用服务器。
FCM使用如下凭据来标识应用服务器和客户端应用,并使用这些凭据经过FCM受权消息事务:
Sender ID – 发件人ID是您在建立Firebase项目时分配的惟一数字值。发件人ID用于标识能够向客户端应用程序发送邮件的每一个应用程序服务器。发件人ID也是您的项目编号;注册项目时,您能够从Firebase控制台获取发件人ID。发件人ID的示例是496915549731
API Key – API密钥使应用服务器能够访问Firebase服务; FCM使用此密钥对应用服务器进行身份验证。此凭证也称为服务器密钥或Web API密钥。 API密钥的示例是AJzbSyCTcpfRT1YRqbz-jIwp1h06YdauvewGDzk。
App ID – 您的客户端应用程序的标识(独立于任何给定的设备)注册以接收来自FCM的消息。应用程序ID的示例是1:415712510732:android:0e1eb7a661af2460
Registration Token – 注册令牌(也称为实例ID)是给定设备上客户端应用程序的FCM身份。注册令牌在运行时生成 - 您的应用程序在设备上运行时首次向FCM注册时会收到注册令牌。注册令牌受权客户端应用程序的实例(在该特定设备上运行)以从FCM接收消息。注册令牌的示例是fkBQTHxKKhs:AP91bHuEedxM4xFAUn0z ... JKZS(很是长的字符串)。
在基于FCM开发时最关键的问题是要搞清楚你指望的是啥?有哪些安全问题须要考虑。好比:
现假设有A,B 2个帐号,若是A帐号登陆APP后发表了一篇文章,而后A从APP内退出了。假设有人收藏了该文章,那么A会收到通知吗?你指望A收到通知吗?若是这时候再切换B帐号呢?
好了,这里面有太多种结果了,不作一一分析了,最主要的是想清楚你要的是什么?你指望的是什么?什么是正确且合理的?说下我指望的:
我指望当「A」从「APP内」退出时,再也不收到与「A」帐号有关的私有通知信息。可是像一些可有可无的,通用型的,非隐私的消息能够接收,如:系统公告类的通知等。
若是要达到这个指望应该如何设计呢?指望值明确了,需求明确了就逐个分析呗。
设备、注册Token、用户帐号、状态,这些是关键元素。其中状态的值为:online和offline。
登陆
「帐号A」在登陆后,向「APP服务端」注册Token。
「服务端-注册Token接口」一个设备,只容许存在一条记录,由于一个设备只能同时登陆一个帐号。因此在注册时,把设备做为查询条件,若是存在,更新uid和Token,并设置状态为online;不然,插入用户ID、设备、Token,并设置状态为online,用户ID从Http Header中的LoginToken里解析出来,设备信息也从Http Header中获取。
走Topic发送消息,优势是同一个帐号,多个设备登陆的状况比较方便,缺点是退出后不能收到如何消息
「服务端」建立Topic,以uid做为Topic名称。
「服务端」订阅Topic,以uid对应的Tokens做为订阅者,订阅Topic。
「服务端」发送消息到Topic,Topic名称为uid。
「FCM」会把通知消息Push到Topic的订阅者。
走Token发送消息
「服务端」根据帐号,找到全部online状态的Tokens,发送消息到Tokens。
接收
「客户端」收到通知消息,若是同一个帐号多个设备的状况,那么每一个设备都会收到通知。
退出
「帐号A」在退出时向「服务端」注销Token,退出前获取注册Token,而后向「服务端」发送注销Token请求。
「服务端」根据帐号和设备,修改Token状态为offline。
「服务端」取消订阅Topic。
切换帐号
「帐号B」退出A,登陆B时,因为一个设备只容许容许一个帐号,因此B会覆盖A的注册信息。
以上实现既知足「同一个设备,同一个应用,多个帐号切换的状况」又知足「同一个帐号,多个设备的状况」。
当应用在前台或系统后台运行时,会接收到Firebase推送给该应用的全部通知。不然当应用不可用,而后从新启动时,会且只能收到Firebase推送的最后一条通知。为何呢?在我看来这是合理的,设备通知不一样于站内消息,不可能把应用完全离线的这段时间所产生的设备通知,在应用从新打开时一次所有推送给设备。另外即便没有设备通知还有站内消息呢,错过的通知在站内消息里都能找到。
FCM渠道表「notify_fcm」
表结构以下:
id: {type: 'integer', primaryKey: true, autoIncrement:true} device: {type: 'string', required: true} //设备号,Agent; token: {type: 'string', required: true} //FCM生成的注册Token; userId: {type: 'string', required: true},//用户ID; state: {type: 'integer', required: true} //状态,online|offline; createdAt: {type: 'timestamp', required: true} //建立时间; updateAt: {type: 'timestamp', required: true} //更新时间;
package main import ( "context" "fmt" "log" firebase "firebase.google.com/go" "firebase.google.com/go/messaging" "google.golang.org/api/option" ) func main() { var serviceAccountKey = []byte(`{ "type": "service_account", "project_id": "lvxiaorun-22c97", "private_key_id": "a212612afee74a54e9e30cb2bfbc5b0c118ca3cc", "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCpieVk85eBv9Eh\nGktoEf7rv+lz7PQh6CTa4F2QtljaFM3k3hv4v3F+JYnVhWKYL1yz7D4vNgByv3lD\nsXGRMwe1uK5pP5DLke7X1q0RQJn928QrUKDK71MKqDPTeYMjZEoVLdFRbVzbvpf9\nwks6H+vh9CFGwJ4D/H/7tX//bRR/f6ape5J+VwEjWH0itLXd35klh3oqWCA4/Dwg\nxNg7LzejVdM+GOhyuvLCZfEOcno8wF+YI8Uj7Ut27Wj9fLcO2cW2ybaK8Y87CfKO\nKmQgSt1lqgbrksi+fe725BxbXZp/eF9MrS8CPLOWgv/DRW3aP0ocgxx6TX0z8K1a\ncFdvXwcxAgMBAAECggEABgfe9liY6NdlLceE66qKNiIhQIurAoLCvttw0Js/7WAE\nk/HXrmFG/P0CWmtQfsfehRLwAldqLCrF+kO3XboiOdNcNue5M5iZFanwBZdV8vsM\njxri4V0ih9REZa8inFFutjKnSb15amKs/uyYpvRoPGUmAuGKrWsftVk3OKONcVyP\nA2L8N/keTc8IWKe4GlIdfoU2f/hAxbVYiu0u2HXgdGlFmY1vpOrFgHbJZcXyUlhg\nQvtgCnt/bwKr3GkuWXJff9pm4wf7A2e73jv68zuucCV5P9iExVIAZIkLvhlxgqn1\ncKF7Ib5K/RVzdXgcVNpL0V6ewv+IC1fjHKLJYbXiDQKBgQDSbOXnxzb639NOIlvJ\nSb0wpTJVmWLWsIvJjO74K2ZNOQ38f7LDuvZ1/M2W0QsvVOw43Gpqijza0HV0aCRH\nqrPldgTRBJAqY/VexWt/kuOvrCA1IkXRl8jayLoMrGxCv12fiIp8ryDbikCuVDCE\n9VuenZGbe1++O5aBjMG/3c6sxQKBgQDOQgVAiAiTmaiUv9NPIX5sfpuV0U7QB6TT\n83wS45ahheanWf/G2CvidazK9KR5oEquljVGTPsmK01x17/1pnM+o6p84ZETNQ12\n4SsJpT7NxId/Vkw2Hmn/qzhHZa1wckdEPrHc70Upre/YJ6snp+brCBQNiuhALZde\nc6yrHZKvfQKBgQCNvlE3ydfdMjxyW26crpFEXWMEiigsGgxvngGzJfjpd89WEObo\nNd6jJ8GNIA96uKfOvZrpXWkUtGsKGMSnifNYVCF2cq5x/5dfWXjKHLZGtZmUcRu6\nzZW82o2Iz/S1GZcFScKPrqBhgkWDqK5uQaCPvfBBXd/mktkVNy2kAtOfSQKBgQC+\nx6xp+ynLtOaM6D4BRJ7Wpektk5QNsfRRJDdQlXi/4MXvZ7zBZTR6XJQ+ijkUUyKh\nCEkwxIXN0WHp+kERbCvO9b39kvsIxBq3KiEP4+wKkk0uiFkn+cvb87izuaXKi7nF\nsyP7ksnrenqN+mtC2/goz6kUubaHnmQTtnUxNcJ3VQKBgQDDbVOQx1LCex9HHAV4\n2Rula9E27w0ujYdhf3YvQ66mOG6Ig5+1YhiXrKrElfCCAVc7H8KmAt7iPHcJXKgC\nMJSJK7XWasj+Ld2SdbPT2LgwGglx907N+wyS47+vOh9Ppu3d5Tv9/BIKkvNKXBu+\ndQBw8Xw1P+U5QtIPf7LOKMtNEw==\n-----END PRIVATE KEY-----\n", "client_email": "firebase-adminsdk-k3958@lvxiaorun-22c97.iam.gserviceaccount.com", "client_id": "103134511880014813376", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-k3958%40lvxiaorun-22c97.iam.gserviceaccount.com" }`) // opt := option.WithCredentialsFile("path/to/serviceAccountKey.json") opt := option.WithCredentialsJSON(serviceAccountKey) app, err := firebase.NewApp(context.Background(), nil, opt) if err != nil { log.Fatalf("error initializing app: %v\n", err) } // Obtain a messaging.Client from the App. ctx := context.Background() client, err := app.Messaging(ctx) if err != nil { log.Fatalf("error getting Messaging client: %v\n", err) } uid := "25696773511053390" // This registration token comes from the client FCM SDKs. registrationToken := "fxV6ToLJh3A:APA91bGfcaOl4mmnj_mPY7MTscjzT0aZLvyK5xaLLboWavxFoeqc3hZu_npEtaINebzHAfOrARg4kn9RmWC9ZYKvhqJrPhnNI43qtUruQsvd7Or7w_ZnDG4agOMM_7xB0J4ci9UHPT5S" // These registration tokens come from the client FCM SDKs. registrationTokens := []string{ registrationToken, // ... "cBYSJNhfG_Q:APA91bFzLxiSVynUc2thc6aGfF1ba_6WoJvOctw2_1cIlUEr2r7Pf-n_Qk6uisLpc9Whcf-UU4WwcjnRwLTm_Zok1pH2RGw2_WvLmaT_AdZp84caH29haB4gQFIdrc0wQSr-vVgR0F3o", } subscribe(ctx, client, uid, registrationTokens) // sendMsgToToken(ctx, client, registrationToken) sendMsgToTopic(ctx, client, uid) // createCustomToken(ctx, app) } func sendMsgToTopic(ctx context.Context, client *messaging.Client, topic string) { notification := &messaging.Notification{ Title: "0007 2019-04-23", Body: "fffffffffffffffffffffffffff.", } // See documentation on defining a message payload. message := &messaging.Message{ Data: map[string]string{ "score": "88888", "time": "2:45", }, Notification: notification, Topic: topic, } // Send a message to the devices subscribed to the provided topic. response, err := client.Send(ctx, message) if err != nil { log.Fatalln(err) } // Response is a message ID string. fmt.Println("Successfully sent message:", response) } func sendMsgToToken(ctx context.Context, client *messaging.Client, registrationToken string) { // See documentation on defining a message payload. notification := &messaging.Notification{ Title: "$GOOG up 1.43% on the day", Body: "$GOOG gained 11.80 points to close at 835.67, up 1.43% on the day.", } // timestampMillis := int64(12345) message := &messaging.Message{ // Data: map[string]string{ // "score": "850", // "time": "2:45", // }, Notification: notification, Webpush: &messaging.WebpushConfig{ Notification: &messaging.WebpushNotification{ Title: "title", Body: "body", // Icon: "icon", }, FcmOptions: &messaging.WebpushFcmOptions{ Link: "https://fcm.googleapis.com/", }, }, Token: registrationToken, } // Send a message to the device corresponding to the provided // registration token. response, err := client.Send(ctx, message) if err != nil { log.Fatalln(err) } // Response is a message ID string. fmt.Println("Successfully sent message:", response) } func subscribe(ctx context.Context, client *messaging.Client, topic string, registrationTokens []string) { // Subscribe the devices corresponding to the registration tokens to the // topic. response, err := client.SubscribeToTopic(ctx, registrationTokens, topic) if err != nil { log.Fatalln(err) } // See the TopicManagementResponse reference documentation // for the contents of response. fmt.Println(response.SuccessCount, "tokens were subscribed successfully") } func createCustomToken(ctx context.Context, app *firebase.App) { authClient, err := app.Auth(context.Background()) if err != nil { log.Fatalf("error getting Auth client: %v\n", err) } token, err := authClient.CustomToken(ctx, "25696773511053390") if err != nil { log.Fatalf("error minting custom token: %v\n", err) } log.Printf("Got custom token: %v\n", token) }
Firebase Cloud Messaging
Device Group Management With Firebase Cloud Messaging
Firebase 云消息传送