应用描述: 向应用发送通知的案例有很多:微信发表朋友圈后,有人回复你,微信会有通知。通知中能带一些数据,让你知道微信中有哪些和你有关的事情发生。要实现通知首先需要了解通知的整个过程(这里介绍手机的通知)。
- 用户在移动端的某些操作会在服务端触发一些事情,服务端通过一定的方式给对应的移动端发送信号;
- 移动端的应用程序实现了手机接收通知的接口的,能收到这个信号,并获取到数据;
- 移动端应用程序直接解析数据或是去服务端拉取对应的详情数据后再解析数据;
- 移动端应用程序根据数据设定锚点,并展示通知。这样点击通知获取详情后,能跳转到应用程序的对应页面;
这个过程中,移动端接收通知是被动的(如果是主动,移动端需要以心跳检测的方式一直向服务器发送请求,这对服务器的压力太大);要达到被动接受效果需要借助三方平台(firebase)来完成这个事情。 所以在整个通知流程中,服务端需要做的是:
- 在业务中激发对应的信号,并将信号需要的数据准备好
为了让django应用程序对消息推送'无感知',可以采用django信号来完成,即:应用程序保存某些数据的时候,框架会将
save
信号发送到对应的处理程序,这个处理程序中进行消息推送处理
- 调用三方平台消息推送接口,使之将数据发送到对应的移动端
将三方平台的调用继集成到自己的程序中
- 提供移动端程序调用接口,使之能拿到详细的数据
服务端提供消息详情获取接口,供移动端调用
相关组件介绍
1. firebase 通知功能介绍
Firebase 云信息传递 FCM 是一种跨平台消息传递解决方案,可供您免费、可靠地传递消息 使用 FCM,您可以通知客户端应用存在可同步的新电子邮件或其他数据。您可以发送通知消息以再次吸引用户并留住他们。在即时通讯等使用情形中,一条消息可将最多 4KB 的有效负载传送至客户端应用。
服务器要发送消息有两种方式:
- 直接调用官方提供 Admin SDK
- 另一种是使用原生的协议,发送指定格式的数据,这种方式需要自己处理发送异常,实现重试机制。 使用官方SDK的,官方提供了对应语言的源码和调用示例,可以直接拿过来使用。
2. django 信号机制
Django Signals的机制是一种观察者模式,又叫发布-订阅(Publish/Subscribe) 。当发生一些动作的时候,发出信号,然后监听了这个信号的函数就会执行。 通俗来讲,就是一些动作发生的时候,信号允许特定的发送者去提醒一些接受者。用于在框架执行操作时解耦。 特别是使用django内置信号的时候,你执行完某个操作,不需要去手动调用信号接收处理函数,这在一定的程度上实现了程序的'无感知'。发送通知时,用这个方式就很合适:业务层做完对应的业务,保存数据到通知表,保存后触发信号,框架去调用对应的处理函数(将通知发送到移动端的函数),业务逻辑和移动端处理函数不用耦合在一起。
3. django 异步
发送信号因要调用三方平台,会比较耗时,如果让业务程序执行后,使用处理业务程序的线程继续处理通知的话,会大大的延长业务程序处理的时间,这会降低服务器的处理效率。这里可以新开线程来单独处理通知。让业务程序直接返回(业务处理程序不需要管通知处理的结果)。在django中要用额外的线程来处理这些,最好是使用celery,也可以自己手动新建线程来处理。
import threading
def post_save_callback(sender, **kwargs):
target = threading.Thread(
target=notice_handler,
kwargs=kwargs,
)
target.start()
复制代码
设计思路和代码示例
1. 集成 FCM Admin SDK
这里介绍下python集成的示例[官方文档]:
- 添加SDK
sudo pip install firebase-admin
复制代码
- 下载 key 在google的firebase控制台添加应用
找到对应应用的服务账号设置
找到对应的语言,生成私钥(这个必要暴露出来!):yourPrivateKey.json
- 初始化SDK
class MessageHandler(object):
_instance_lock = threading.Lock()
def __init__(self):
if not hasattr(self, 'app'):
with MessageHandler._instance_lock:
if not hasattr(self, 'app'):
self.cred = credentials.Certificate('yourPath/yourPrivateKey.json')
self.default_app = firebase_admin.initialize_app(self.cred)
self.app = self.default_app
def __new__(cls, *args, **kwargs):
if not hasattr(MessageHandler, "_instance"):
with MessageHandler._instance_lock:
if not hasattr(MessageHandler, "_instance"):
MessageHandler._instance = object.__new__(cls)
return MessageHandler._instance
def get_app(self):
return self.app
_message_handler = MessageHandler()
if '__name__' == 'main':
print('register success!')
复制代码
直接执行这个文件,能执行成功,证明配置正确 配置正确后可以在项目启动的时候就完成对象的注册:在 admin 模块的类中添加ready,并将对象 import 过来(后续的信号注册也需要这样做)
from django.apps import AppConfig
class TestConfig(AppConfig):
name = 'test'
verbose_name = 'Test'
def ready(self):
import your MessageHandler file name
复制代码
2. 编写消息发送函数来调用 Admin SDK API
官方有对应示例, 可以在此基础上调整后直接使用
# Copyright 2018 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import print_function
import datetime
from firebase_admin import messaging
def send_to_token():
# [START send_to_token]
# This registration token comes from the client FCM SDKs.
registration_token = 'YOUR_REGISTRATION_TOKEN'
# See documentation on defining a message payload.
message = messaging.Message(
data={
'score': '850',
'time': '2:45',
},
token=registration_token,
)
# Send a message to the device corresponding to the provided
# registration token.
response = messaging.send(message)
# Response is a message ID string.
print('Successfully sent message:', response)
# [END send_to_token]
def send_to_topic():
# [START send_to_topic]
# The topic name can be optionally prefixed with "/topics/".
topic = 'highScores'
# See documentation on defining a message payload.
message = messaging.Message(
data={
'score': '850',
'time': '2:45',
},
topic=topic,
)
# Send a message to the devices subscribed to the provided topic.
response = messaging.send(message)
# Response is a message ID string.
print('Successfully sent message:', response)
# [END send_to_topic]
def send_to_condition():
# [START send_to_condition]
# Define a condition which will send to devices which are subscribed
# to either the Google stock or the tech industry topics.
condition = "'stock-GOOG' in topics || 'industry-tech' in topics"
# See documentation on defining a message payload.
message = messaging.Message(
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.',
),
condition=condition,
)
# Send a message to devices subscribed to the combination of topics
# specified by the provided condition.
response = messaging.send(message)
# Response is a message ID string.
print('Successfully sent message:', response)
# [END send_to_condition]
def send_dry_run():
message = messaging.Message(
data={
'score': '850',
'time': '2:45',
},
token='token',
)
# [START send_dry_run]
# Send a message in the dry run mode.
response = messaging.send(message, dry_run=True)
# Response is a message ID string.
print('Dry run successful:', response)
# [END send_dry_run]
def android_message():
# [START android_message]
message = messaging.Message(
android=messaging.AndroidConfig(
ttl=datetime.timedelta(seconds=3600),
priority='normal',
notification=messaging.AndroidNotification(
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.',
icon='stock_ticker_update',
color='#f45342'
),
),
topic='industry-tech',
)
# [END android_message]
return message
def apns_message():
# [START apns_message]
message = messaging.Message(
apns=messaging.APNSConfig(
headers={'apns-priority': '10'},
payload=messaging.APNSPayload(
aps=messaging.Aps(
alert=messaging.ApsAlert(
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.',
),
badge=42,
),
),
),
topic='industry-tech',
)
# [END apns_message]
return message
def webpush_message():
# [START webpush_message]
message = messaging.Message(
webpush=messaging.WebpushConfig(
notification=messaging.WebpushNotification(
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.',
icon='https://my-server/icon.png',
),
),
topic='industry-tech',
)
# [END webpush_message]
return message
def all_platforms_message():
# [START multi_platforms_message]
message = messaging.Message(
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.',
),
android=messaging.AndroidConfig(
ttl=datetime.timedelta(seconds=3600),
priority='normal',
notification=messaging.AndroidNotification(
icon='stock_ticker_update',
color='#f45342'
),
),
apns=messaging.APNSConfig(
payload=messaging.APNSPayload(
aps=messaging.Aps(badge=42),
),
),
topic='industry-tech',
)
# [END multi_platforms_message]
return message
def subscribe_to_topic():
topic = 'highScores'
# [START subscribe]
# These registration tokens come from the client FCM SDKs.
registration_tokens = [
'YOUR_REGISTRATION_TOKEN_1',
# ...
'YOUR_REGISTRATION_TOKEN_n',
]
# Subscribe the devices corresponding to the registration tokens to the
# topic.
response = messaging.subscribe_to_topic(registration_tokens, topic)
# See the TopicManagementResponse reference documentation
# for the contents of response.
print(response.success_count, 'tokens were subscribed successfully')
# [END subscribe]
def unsubscribe_from_topic():
topic = 'highScores'
# [START unsubscribe]
# These registration tokens come from the client FCM SDKs.
registration_tokens = [
'YOUR_REGISTRATION_TOKEN_1',
# ...
'YOUR_REGISTRATION_TOKEN_n',
]
# Unubscribe the devices corresponding to the registration tokens from the
# topic.
response = messaging.unsubscribe_from_topic(registration_tokens, topic)
# See the TopicManagementResponse reference documentation
# for the contents of response.
print(response.success_count, 'tokens were unsubscribed successfully')
# [END unsubscribe]
复制代码
拿过来后就可以直接测试发送消息了,测试通过再往后面走(如何让移动端接收到firebase的信息的请参看对应文档)
3. 编写信号处理函数来调用信号发送函数
def send_to_token(notice):
data = None # your message data
token = None # 移动端 token
return msg_handler.send_to_token(
token,
data,
)
def notice_handler(**kwargs):
"""notificaton demo"""
notice = kwargs.get('instance', None)
send_to_token(notice)
复制代码
4. 用多线程方式注册信号处理函数,使之能'感知'到对应的业务操作
from django.db.models.signals import post_save
def post_save_callback(sender, **kwargs):
target = threading.Thread(
target=notice_handler,
kwargs=kwargs,
)
target.start()
post_save.connect(
post_save_callback, # 指定回调函数
dispatch_uid='xxx', # 限制消息只发送一次
sender=NoticeModelClassName, # 限制只接收和通知有关的消息
)
复制代码