捋一捋关于APP 常用促活的一个手段:push。先来看一张图,撇开平台相关,基本流程大体和下图一致:
- 手机是如何显示“Push Notification”的?
- 服务器怎么下发“Push Notification”到用户手机上?
之前我也对这俩问题蛮好奇的,后来对接了一个相关的需求,就摸索了下相关知识,现整理如下。
手机 APP(这里指商用APP,单机版 APP 不作考虑) 一般来说,都会添加一些手机服务商的 SDK(尤其是安卓手机,OV 华米,小米推送等,iOS 自己有另外的 APNS 服务),其目的就是让手机和服务商做成**“长链接”**,方便一些业务场景的实现。
下面一起看下 APP 如何与业务服务器配合,实现用户push notification 的。
- APP 向操作系统注册 push 通知
- 操作系统向服务商申请 device token
- APP 将拿到的 device token 与用户相关信息(如 userid)发送给业务服务器,由业务服务器进行记录
- 业务服务器将 userid 对应的 device token 拿到,配合业务文案,发给服务商
- 服务商通过长链接,将信息推给 APP,来实现“push notification”
整个流程还算比较清晰,但是在一个公司内部,如何把这套系统做的更简洁高效,还是需要花点时间的。
- 第三步中,用户将device token 以及自己的信息传给业务服务器,服务器需要进行记录,这个 device tokenn 通常来说每个 APP 独一份。业务服务器对每次 push 业务,都需要拿到每个人的 device token,然后加上业务文案,告诉服务商:“把这个文案推给device token”是 xxx 的设备上。
- 第四步中业务服务器向服务商请求的过程一般来说都不会很快,所以 qps 肯定高不到哪里去。这就导致运营的某次 push 通知需求可能持续很久很久。所以一般会借助消息队列来达到“异步”、“削峰”处理。
- 和公司双端的客户端确认了下,这个 device token 上报的时机是不一样的,安卓是会记录上报状态,遍历所有推送渠道如 OV 华米,小米等渠道是否上报过或者本地账号切换都会触发重新上传;iOS 是每次冷启动,用户登录登出会进行解绑,然后重新上报。
- 发送量(业务服务器管理)、到达率(对接服务商)、点击率(用户上报到业务服务器)等十个很冗杂的活儿,一般来说 push 本身是一个较强的促活手段,对很多 APP 来说是标配组件,还是值得花时间来完整做一套出来的。
业务服务器向服务商请求 push, 以OPPO 的 push 为例:
public function sendToOne($title, $desc, $data, $userToken, $expireTime=3600, $retry=true) {
$ret = 1;
if($this->accessToken == "" || $this->expire < time()) {
$this->getAccessToken(true);
}
if($this->isenabled == 1) {
$messages = array(
'target_type' => 2,
'registration_id' => $userToken,
'target_value' => $userToken,
'notification' => array(
//'app_message_id' => intval($data['id']),// 这个id不能重复,不然就不发了
'title' => $title,
'subtitle' => '',
'content' => $desc,
'click_action_type' => 1,
'click_action_activity' => 'com.xiaochang.easylive.OppoPushMessage',
'action_parameters' => array(
'uri' => json_encode(array(
'id' => intval($data['id']),
'title' => strval($title),
'url' => strval($data['url']),
'iscb' => strval($data['iscb']),)),
'source' => 'oppo',
),
//'call_back_url' => '',
//'call_back_parameter' => strval($data['id']),
)
);
if(!empty($expireTime)) {
$message['off_line_ttl'] = $expireTime;
}
$postData = array('auth_token' => $this->accessToken, 'message' => json_encode($messages),);
$pushUrl = self::$pushUrl.'/server/v1/message/notification/unicast';
$curl['url'] = $pushUrl;
$curl['post'] = http_build_query($postData);
list($code, $response) = $this->curlContent($curl);
LogError("response code for {$userToken} : $code, response ".json_encode($response), "/tmp/oppopush.log");
if($code == 0) {
$response = json_decode($response, true);
if($response["code"] == 29) {
$this->getAccessToken(true);
if($retry) {//取token然后再尝试发一次。
$ret = $this->sendToOne($title, $desc, $data, $userToken, $expireTime, false);
}
} else if($response["code"] == 0) {
$ret = 0;
} else if($response["code"] == 33) {//超限了,今天不走oppo推送了
global $user_redis;
$num = isset($response["data"]["permits"]) ? intval($response["data"]["permits"]) : 1;
$user_redis->init()->hset(self::IS_OPPO_OUT_OF_LIMIT, date("YmdH"), $num);
$this->isenabled = 0;
}
}
}
return $ret;
}