第四次作业:Push Technology的实现

这次作业主要是针对服务端进行完善,让app可以接受推送,接收到推送以后点击通知能够跳转到正确的activity。

首先要知道一个叫Firebase Cloud Messaging (FCM)的工具,我们是利用这个工具向装有app的每个手机广播推送的。

我们要做的主要工作有三:

Step1

一个谷歌账号,创立一个项目,设置好Firebase Cloud Messaging API。
这一步主要是前期的准备工作,具体操作流程见<a href="http://iems5722.albertauyeung.com/files/assignments/iems5722-assignment-04.pdf">传送门</a>
ps: 可以在AS里的tools中找到 firebase,通过这个方法来链接回节省一些添加依赖的步骤。

Step2

在AS上改写app,让它支持FCM。这部分又分为三个小工作:

2.1

得到一个本机token。
这一步主要是编写 MyFirebaseInstanceIDService.java里的onTokenRefresh()方法。

2.2

上传这个token给服务器,服务器存到数据库。
这部分要分为两步,一个是client端,一个是服务器端。app上要继续编写MyFirebaseInstanceIDService.java,与上一小步不同的是写的是sendRegistrationToServer(String token)方法

直接贴代码吧:

import android.app.Service;
import android.util.Log;

import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.FirebaseInstanceIdService;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;

import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

/**
 * Created by huangkai on 2017/3/28.
 */
public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {
    // @Override
    private static final String TAG = "MyFirebaseIIDService";
    private final String STUDENT_ID = "1155084531";

    // This function will be invoked when Android assigns a token to the app
    @Override
    public void onTokenRefresh() {
        String refreshedToken = FirebaseInstanceId.getInstance().getToken();
        Log.d(TAG, "Refreshed token: " + refreshedToken);
        sendRegistrationToServer(refreshedToken);
    }

    private void sendRegistrationToServer(String token) { // Submit Token to your server (e.g. using HTTP) // (Implement your own logic ...)


        RequestBody reqBody = new FormBody.Builder()
                .add("token", token)
                .add("user_id", STUDENT_ID)
                .build();
        String url="http://www.therealhk.top/api/assgn4/submit_push_token";
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(url)
                .post(reqBody)
                .build();
        Log.d(TAG, url);
        try {
            Response res = client.newCall(request).execute();
            String resData = res.body().string();
            JSONObject json = new JSONObject(resData);
            Log.d("push token status:", String.valueOf(json.get("status")));
        } catch (IOException e) {
            e.printStackTrace();
            Log.d(TAG, "sendRegistrationToServer: error");
        } catch (JSONException e) {
            e.printStackTrace();
            Log.d(TAG, "sendRegistrationToServer: error");
        }

    }
}

服务器端要做什么呢?
服务器端要处理接收到的server,要在server 上新建一个table 用来存放user_id 和token。

CREATE TABLE push_tokens (
`id` int(11) NOT NULL AUTO_INCREMENT, 
`user_id` VARCHAR(11) NOT NULL,
`token` VARCHAR(256) NOT NULL, 
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;

然后我们在上次的api的基础上增加一个部分:submit_push_token()

@app.route('/api/assgn4/submit_push_token', methods=['POST'])

def submit_push_token():
    my_db = md.MyDatabase()
    user_id =request.form.get("user_id")
    token = request.form.get("token")

    if token == None or user_id ==None:
        return jsonify(status="ERROR", message="missing parameters")

    query = "INSERT INTO push_tokens (user_id,token) values(%s,%s)"
    parameters = (user_id,token)
    my_db.cursor.execute(query,parameters)
    my_db.db.commit()

    return jsonify(status="OK")
2.3

app正确接收FCM的message
这部分主要是写MyFirebaseMessagingService.java
代码如下:

package a1_1155084531.iems5722.ie.cuhk.edu.hk.a1_1155084531;

import com.google.firebase.messaging.FirebaseMessagingService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.google.firebase.messaging.RemoteMessage;

/**
 * Created by huangkai on 2017/3/28.
 */
public class MyFirebaseMessagingService extends FirebaseMessagingService {
    private static final String TAG = "MyFirebaseMsgService";
    private static int count =0;
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // [START_EXCLUDE]
        // There are two types of messages data messages and notification messages. Data messages are handled
        // here in onMessageReceived whether the app is in the foreground or background. Data messages are the type
        // traditionally used with GCM. Notification messages are only received here in onMessageReceived when the app
        // is in the foreground. When the app is in the background an automatically generated notification is displayed.
        // When the user taps on the notification they are returned to the app. Messages containing both notification
        // and data payloads are treated as notification messages. The Firebase console always sends notification
        // messages. For more see: https://firebase.google.com/docs/cloud-messaging/concept-options
        // [END_EXCLUDE]

        // TODO(developer): Handle FCM messages here.
        // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
        Log.d(TAG, "From: " + remoteMessage.getFrom());

        // Check if message contains a notification payload.
        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
            sendNotification(remoteMessage.getNotification().getTitle(),
                    remoteMessage.getNotification().getTag(),
                    remoteMessage.getNotification().getBody());
            count = count +1;
        }

        // Also if you intend on generating your own notifications as a result of a received FCM
        // message, here is where that should be initiated. See sendNotification method below.
    }
    // [END receive_message]


    /**
     * Create and show a simple notification containing the received FCM message.
     *
     * @param messageBody FCM message body received.
     */
    private void sendNotification(String chatroom_name,String chatroom_id,String messageBody) {
       //以下是保证点击通知跳转正确到activity的代码
        Intent intent = new Intent(this, Chat_Activity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        intent.putExtra("chatroom_id",chatroom_id);
        intent.putExtra("chatroom_name",chatroom_name);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, count /* Request code */, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.ic_stat_ic_notification)
                .setContentTitle(chatroom_name)
                .setContentText(messageBody)
                .setAutoCancel(true)
                .setSound(defaultSoundUri)
                .setContentIntent(pendingIntent);

        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        notificationManager.notify(count/* ID of notification */, notificationBuilder.build());
    }
}

这一部分和server端有关,取决于你server端Title,Tag,Body里取出来的是什么。下一部分会有介绍。

Step3

扩展server的功能使之能够实现某用户发送一个消息了以后其他所有人都收到FCM消息。

这里要首先介绍一个叫做celery的东西,它是一个消息队列的管理器,我们利用它来创建异步的任务,把消息转发给FCM,系统结构如下。

屏幕快照 2017-03-29 下午8.27.51.png

server端与上次不同,还需要写一个task.py,写之前安装RabbitMQ 和 Celery 以及 task.py 中需要的 requests。

$ sudo apt-get install rabbitmq-server
$ sudo pip install celery
$ sudo pip install requests

task.py 代码如下

from celery import Celery
from flask import Flask
import requests

def make_celery(app):
    celery = Celery(app.import_name, broker=app.config['CELERY_BROKER_URL'])
    celery.conf.update(app.config)
    TaskBase = celery.Task

    class ContextTask(TaskBase):
        abstract = True

        def __call__(self, *args, **kwargs):
                return TaskBase.__call__(self, *args, **kwargs)
    celery.Task = ContextTask
    return celery

app = Flask(__name__)
app.config.update(
    CELERY_BROKER_URL='amqp://guest@localhost'
)
celery = make_celery(app)


@celery.task()
def NoififyEveryOne(chatroom_name,chatroom_id,name,msg,token):
    api_key = 'AAAAmQpTpkU:APA91bGwonrhhB1lPoqIlkAwTnV9dDNF100o5aKo3J13gc3ZesxnkSTrShDNCnE8CZbXTVXOlM1yoqtas_GiSRIej1cX52z8thv6F9o-p6ShDy0dWR-9i-w7t0shWVZOe1cZ1ETEa1nX'
    url = 'https://fcm.googleapis.com/fcm/send'

    headers = {
        'Authorization': 'key=' + api_key,
        'Content-Type': 'application/json'
    }

    device_token = token
    
    payload = {'to': device_token,'notification':{
    "title":chatroom_name,
    "tag":chatroom_id,
    "body":name +": "+msg
    }}

    response = requests.post(url,headers =headers,json=payload)
    if response.status_code == 200:
        print "Send to FCM sucessfully!"

可以看到
title里放的是chatroom_name
tag放的是chatroom_id,
body放的是name +": "+msg
出来的通知大概长这样:

通知样式.png

我们要实现的功能是某用户发送一个消息了以后其他所有人都收到FCM消息,那肯定要在原本发送信息的api里调用上面写的NoififyEveryOne.delay()方法,故对API的发送部分做如下修改:

@app.route('/api/assgn3/send_message',methods=['POST'])

def send_message():
    my_db = md.MyDatabase()
    msg = request.form.get("message")
    name = request.form.get("name")
    chatroom_id = request.form.get("chatroom_id")
    user_id = request.form.get("user_id")

    # Get chatroom name
    select_chatroomname_query = "SELECT name from CHATROOMS where id = %s" % chatroom_id
    my_db.cursor.execute(select_chatroomname_query)
    chatroom_name_json = my_db.cursor.fetchone()
    chatroom_name = chatroom_name_json['name']

    if msg == None or chatroom_id == None or name == None or user_id == None :
        return jsonify(status="ERROR", message="missing parameters")

    query = "INSERT INTO messages (chatroom_id,user_id,name,timestamp,message) values(%s,%s,%s,%s,%s)"
    parameters = (chatroom_id,user_id,name,time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()+8*3600)),msg)
    my_db.cursor.execute(query,parameters)
    my_db.db.commit()

    select_token_query = "SELECT token FROM push_tokens"
    my_db.cursor.execute(select_token_query)
    token_json = my_db.cursor.fetchone()
    while token_json != None:                 #发送给每一台机子
        token = token_json['token']
        NoififyEveryOne.delay(chatroom_name,chatroom_id,name,msg,token)
        token_json = my_db.cursor.fetchone()
    return jsonify(status="OK")

在做下一步以前,可以开两个终端并更改app里写的API地址。分别运行

$ python api.py
$ celery -A task.celery worker --loglevel=DEBUG

进行调试,一切OK后进入下一步。

Step4 配置服务器

4.1 Supervisor的配置

在上次的.conf文件(etc目录下的),添加新的program配置信息,command 一行即运行 celery worker的命令

[program:iems5722_2]
command = celery -A task.celery worker
directory = /home/ubuntu/api
user = ubuntu
autostart = true
autorestart = true
stdout_logfile = /home/ubuntu/api/task.log 
redirect_stderr = true

然后shutdown并重载supervisor。具体指令如下:

$ supervisord -c /etc/supervisord.conf  # 启动supervisor
$ supervisorctl reload      # 重新加载配置文件
$ supervisorctl update
4.2 Nginx的配置

配置文件在 /etc/nginx/sites-available/ 下面,配置完了之后软链接一份到 /etc/nginx/sites-enabled/ghost.conf 下面(原本的软链接删除)。
配置文件需要修改:

server {
    listen 80;
    listen [::]:80;

    root /home/ubuntu/api;
    index index.php index.html index.htm;

    server_name 0.0.0.0;
    location /api/ {
        proxy_pass http://0.0.0.0:8000;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host; 
        proxy_http_version 1.1; 
        proxy_redirect off; 
        proxy_buffering off;
    } 
}

做软链接:

$ sudo ln -s /etc/nginx/sites-available/iems5722.conf /etc/nginx/sites-enabled/iems5722.conf

最后重启nginx服务:

$sudo netstat -nlp | grep :80
$sudo kill xxxx
$ sudo service nginx restart

References

<a href="https://github.com/leoymr/Android-self-learning/blob/master/android-server%E9%85%8D%E7%BD%AE.md">Android server 端配置问题汇总</a>
<a href="https://github.com/leoymr/Android-Instant-MSG">安卓即时通信软件</a>

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 84,112评论 14 122
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 135,493评论 17 574
  • 第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的“同行”Apache吧!Ngi...
    JokerW阅读 27,547评论 24 981
  • 旅途中,最让你难忘的应该是各种有趣或独特的人和事。从一个地方,到另一个地方,不停歇的去经历,我们期待着每一个自己尚...
    日月星辰JC阅读 37评论 0 1
  • 个个摩拳擦掌等待比赛!没有参加的在教室看《弟子规》。恭喜国际象棋小王子取得优异成绩!
    可爱的河南妹阅读 62评论 0 0