kpush源码解析---android端

最近看到了一个开源的push项目:kpush,整个项目包含了客户端和服务端的源码,强烈推荐下。决定分析下该项目的源码,学习下作者的解决方案。android端源码比较简单,我们就从简单的入手,本篇详细分析下android端的源码。


1. 目录结构

我们来看下android工程的目录结构

KPushDemo

----src

--------cn.kpush

--------cn.kpush_demo

--------cn.vimer.ferry

--------cn.vimer.netkit

其中cn.kpush是android端push sdk,实现了push的所有功能,cn.vimer.ferry主要是实现了tcp客户端连接/发送/接收数据功能,cn.vimer.netkit主要是数据序列化及反序列化的功能。关于ferry和netkit我们可以先不关注其代码,我们将把精力集中在kpush上。


2. 初始化/数据接收流程

我们先看下MainActivity中的代码,在MainActivity中的onCreate函数中有如下代码:

    KPush.init(this);
    KPush.setDebug(true);
    // KPush.setAliasAndTags("dante", new String[]{"a", "c"});
然后看下KPush.java中的init函数
    public static void init(Context context_) {
        context = context_;

        setDebug(false);

        Intent intent = new Intent(context, PushService.class);
        intent.setAction(Config.INTENT_ACTION_SERVICE_START);
        context_.startService(intent);
    }
可以看到init函数中主要一个功能就是start PushService服务,那么接下来我们分析下PushService的处理流程。

我们首先看下PushService.java文件中的onCreate函数:

    @Override
    public void onCreate() {
        super.onCreate();
        KLog.d("");

        // 初始化的时候
        userAuthed = false;

        // 因为service可能重新进来
        DeviceInfo.init(this);
        handler = new Handler();

        regEventCallback();
        allocServer();
        // 启动心跳
        heartbeat();
    }
DeviceInfo.init(this)主要工作是完成设备信息初始化操作,获取应用包名/版本号/设备id等一些信息。

regEventCallback()顾名思义就是注册一些事件的回调,用于处理相关的事件

allocServer()顾名思义是分配服务器

heartbeat()用于发送心跳包,以保持与服务器的连接

接下来我们分析allocServer函数,allocServer函数代码就一行:

    private void allocServer() {
        // 申请 server
        new AllocServerTask().execute();
    }
AllocServerTask代码如下:

    private class AllocServerTask extends AsyncTask {
        JSONObject jsonData;

        @Override
        protected void onPreExecute() {
        }

        @Override
        protected Integer doInBackground(String... params) {
            try {
                HttpClient httpClient = new DefaultHttpClient();
                // 连接超时
                httpClient.getParams().setParameter(
                        CoreConnectionPNames.CONNECTION_TIMEOUT,
                        Config.HTTP_CONNECT_TIMEOUT * 1000);
                // 读取超时
                httpClient.getParams().setParameter(
                        CoreConnectionPNames.SO_TIMEOUT,
                        Config.HTTP_READ_TIMEOUT * 1000);

                String allocServerUrl = String.format(Config.ALLOC_SERVER_URL,
                        DOMAIN);
                KLog.d("allocServerUrl: " + allocServerUrl);
                HttpPost httpPost = new HttpPost(allocServerUrl);

                JSONObject jsonObject = new JSONObject();
                jsonObject.put("appkey", DeviceInfo.getAppkey());
                jsonObject.put("channel", DeviceInfo.getChannel());
                jsonObject.put("device_id", DeviceInfo.getDeviceId());
                jsonObject.put("device_name", DeviceInfo.getDeviceName());
                jsonObject.put("os_version", DeviceInfo.getOsVersion());
                jsonObject.put("os", Config.OS);
                jsonObject.put("sdk_version", Config.SDK_VERSION);

                String postBody = Utils.packData(SECRET_KEY, jsonObject);
                if (postBody == null) {
                    // 一般不会出现在这
                    KLog.e(String.format("packData fail. jsonData: %s",
                            jsonObject.toString()));
                    return -1;
                }

                KLog.d("postBody: " + postBody);
                httpPost.setEntity(new StringEntity(postBody));

                HttpResponse httpResponse = httpClient.execute(httpPost);
                int code = httpResponse.getStatusLine().getStatusCode();
                if (code == 200) {
                    String recvBody = EntityUtils.toString(httpResponse
                            .getEntity());

                    jsonData = Utils.unpackData(SECRET_KEY, recvBody);
                    KLog.d("jsonData: " + jsonData);

                    if (jsonData == null) {
                        KLog.e(String
                                .format("unpackData invalid: %s", recvBody));
                        return -3;
                    }

                    KLog.d("succ");
                    // 解析成功
                    return 0;
                } else {
                    KLog.e(String.format("status code invalid: %d", code));
                    return -4;
                }
            } catch (Exception e) {
                KLog.e("fail: " + e.toString());

                return -2;
            }
        }

        @Override
        protected void onProgressUpdate(Integer... progresses) {
        }

        @Override
        protected void onPostExecute(Integer result) {
            KLog.d("result: " + result);
            if (result != 0) {
                // 说明失败
                allocServerLater();

                return;
            }

            try {
                serverHost = jsonData.getJSONObject("server").getString("host");
                serverPort = jsonData.getJSONObject("server").getInt("port");
                userId = jsonData.getJSONObject("user").getLong("uid");
                userKey = jsonData.getJSONObject("user").getString("key");
            } catch (Exception e) {
                KLog.e("fail: " + e.toString());

                allocServerLater();
                return;
            }

            // 清零
            failConnectTimes = 0;

            connectToServer();
        }

        @Override
        protected void onCancelled() {
            KLog.d("");
            allocServerLater();
        }
    }
AllocServerTask其实就是一个AsyncTask,执行一些后台操作,那这些操作到底是什么呢,接下来我们就一一分析下到底这个task中都干了些什么?首先我们到doInBackground函数中,该函数中主要就做了三件事情:

1. 打开一个http client

2. 把DeviceInfo中的设备信息组织成json串,然后http post到http://demo.kpush.cn/server/alloc

3. 解析post的response消息到jsonData变量中

然后我们进入到onPostExecute函数中,可以看到在该函数中,主要从jsonData中取出host/port/uid/key四个数据,然后调用connectToServer函数,看到connectToServer函数名称及上面取出的四个数据,我们可以猜测出来是要连接到服务器上,可以看下connectToServer具体代码:

    private void connectToServer() {
        Ferry.getInstance().init(serverHost, serverPort);
        if (Ferry.getInstance().isRunning()) {
            if (!Ferry.getInstance().isConnected()) {
                Ferry.getInstance().connect();
            }
        } else {
            Ferry.getInstance().start();
        }
    }
通过上述代码可以验证我们的猜测,主要就是通过ferry,连接到serverHost:serverPort TCP服务器上。到目前位置,感觉流程已经走完了,难道只是连接上服务器就结束了吗?答案是否定的,还记得onCreate函数中的regEventCallback吗?接下来我们就看看这个函数,流程如何从connectToServer流转到了regEventCallback函数中的呢?regEventCallback函数如下:

    private void regEventCallback() {
        Ferry.getInstance().addEventCallback(new Ferry.CallbackListener() {
            @Override
            public void onOpen() {

                userLogin();
            }

            @Override
            public void onRecv(IBox ibox) {
                Box box = (Box) ibox;

                KLog.d("box: " + box);
                JSONObject jsonData = Utils.unpackData(SECRET_KEY, box.body);
                KLog.d("box.body: " + box.body);
                KLog.d("jsonData: " + jsonData);

                if (box.cmd == Proto.EVT_NOTIFICATION) {
                    if (jsonData != null) {
                        try {
                            int notificationID = jsonData.getInt("id");

                            recvNotification(notificationID);

                            boolean silent = false;
                            if (jsonData.has("silent")) {
                                silent = jsonData.getBoolean("silent");
                            }

                            if (!silent) {
                                showNotification(notificationID,
                                        jsonData.getString("title"),
                                        jsonData.getString("content"));
                            }
                        } catch (Exception e) {
                            KLog.e(String.format("exc occur. e: %s, box: %s",
                                    e, box));
                        }
                    }
                }
            }

            @Override
            public void onClose() {
                userAuthed = false;
                KLog.e("");
                // Ferry.getInstance().connect();
                // 从获取IP开始
                allocServer();
            }

            @Override
            public void onError(int code, IBox ibox) {
                KLog.e(String.format("code: %d", code));
            }

        }, this, "main");
    }
看到该函数中的onOpen回调函数了没,当我们在connectToServer函数中调用Ferry.getInstance().start()去连接tcp服务器,如果连接上就会调用onOpen函数,这是整个流程就流转到了regEventCallback。我们看到onOpen回调函数中就一行代码:userLogin(); 可以知道这个函数是实现用户登录的,该函数具体代码如下:

    private void userLogin() {

        Box box = new Box();
        box.cmd = Proto.CMD_LOGIN;
        JSONObject jsonObject = new JSONObject();
        try {
            jsonObject.put("uid", userId);
            jsonObject.put("key", userKey);
        } catch (Exception e) {
            KLog.e("exc occur. e: " + e);
        }

        String body = Utils.packData(SECRET_KEY, jsonObject);
        KLog.d("body: " + body);

        box.body = body == null ? null : body.getBytes();

        Ferry.getInstance().send(box, new Ferry.CallbackListener() {
            @Override
            public void onSend(IBox ibox) {
            }

            @Override
            public void onRecv(IBox ibox) {
                Box box = (Box) ibox;

                if (box.ret != 0) {
                    // 几秒后再重试
                    userLoginLater();
                } else {
                    // 登录成功
                    userAuthed = true;
                    failConnectTimes = 0;

                    sendPendingMsgs();
                }
            }

            @Override
            public void onError(int code, IBox ibox) {
                KLog.e(String.format("code: %d", code));
                userLoginLater();
            }

            @Override
            public void onTimeout() {
                KLog.e("");

                userLoginLater();
            }
        }, 5, this);
    }
可以看到该函数中,把uid和key封装到box,然后通过ferry发送到tcp server,然后等待服务器的响应消息,判断是否登录成功,如果失败则进行重试操作。

到此整个初始化流程已经完成了,接下来就等待接收推送的消息并进行响应了。regEventCallback函数中的onRecv函数就是用于接收push消息,然后进行相应的处理。

3. 数据结构

上面已经分析了基本流程,接下来本节将分析数据结构定义,打开apk,并然后通过管理后台推送一条消息,我们可以看到logcat中有如下的log输出:

05-13 16:20:34.790   435   679 I ActivityManager: Start proc cn.kpush_demo for activity cn.kpush_demo/.MainActivity: pid=23280 uid=10057 gids={50057, 3003, 1028, 1015}
05-13 16:20:34.880 23280 23280 D kpush   : [main(1) PushService.java:58 onCreate]: 
05-13 16:20:34.890 23280 23280 D kpush   : [main(1) DeviceInfo.java:37 init]: packageName: cn.kpush_demo, appVersion: 1, deviceId: b1ff4b0a-2020-33e4-9fb4-e5c3fb76d0c5, osVersion: 19, deviceName: J1
05-13 16:20:34.890 23280 23280 D kpush   : [main(1) PushService.java:76 onStartCommand]: action: cn.kpush.intent.SERVICE_START
05-13 16:20:34.890 23280 23294 D kpush   : [AsyncTask #1(225) PushService.java:458 doInBackground]: allocServerUrl: http://demo.kpush.cn/server/alloc
05-13 16:20:34.930 23280 23294 D kpush   : [AsyncTask #1(225) PushService.java:478 doInBackground]: postBody: {"sign":"036fe07a2b1a72dc1c6ef6e467f5121b","data":"{\"appkey\":\"7d357c9b4ce1414fb27f077b54fb5a8f\",\"sdk_version\":5,\"os\":\"android\",\"device_id\":\"b1ff4b0a-2020-33e4-9fb4-e5c3fb76d0c5\",\"device_name\":\"J1\",\"channel\":\"MAIN\",\"os_version\":19}"}
05-13 16:20:34.970   435   451 I ActivityManager: Displayed cn.kpush_demo/.MainActivity: +200ms
05-13 16:20:35.130 23280 23294 D kpush   : [AsyncTask #1(225) PushService.java:488 doInBackground]: jsonData: {"ret":0,"user":{"uid":211,"key":"41675b5ab24d461c9111f5c6c197f74b"},"server":{"port":29100,"host":"115.28.224.64"}}
05-13 16:20:35.130 23280 23294 D kpush   : [AsyncTask #1(225) PushService.java:496 doInBackground]: succ
05-13 16:20:35.140 23280 23280 D kpush   : [main(1) PushService.java:516 onPostExecute]: result: 0
05-13 16:20:35.170 23280 23280 D kpush   : [main(1) PushService.java:191 userLogin]: body: {"sign":"75195f6043da7cbd17f74b90fbbac6f2","data":"{\"uid\":211,\"key\":\"41675b5ab24d461c9111f5c6c197f74b\"}"}

05-13 16:20:39.870   435   702 I ActivityManager: START u0 {cmp=cn.kpush_demo/.NextActivity} from pid 23280
05-13 16:20:39.940   435   451 I ActivityManager: Displayed cn.kpush_demo/.NextActivity: +65ms
05-13 16:21:09.350 23280 23280 D kpush   : [main(1) PushService.java:131 onRecv]: box: magic: 2037952207, version: 0, flag: 0, packetLen: 170, cmd: 10001, ret: 0, sn: 0, bodyLen: 146
05-13 16:21:09.350 23280 23280 D kpush   : [main(1) PushService.java:133 onRecv]: box.body: [B@4207f618
05-13 16:21:09.360 23280 23280 D kpush   : [main(1) PushService.java:134 onRecv]: jsonData: {"silent":false,"id":592,"content":"test_content","title":"test_title"}
由log可以看到allocServerUrl: http://demo.kpush.cn/server/alloc,这个http server是用于分配uid/key/tcp server等资源,客户端通过post请求,post的内容为

{"sign":"036fe07a2b1a72dc1c6ef6e467f5121b","data":"{\"appkey\":\"7d357c9b4ce1414fb27f077b54fb5a8f\",\"sdk_version\":5,\"os\":\"android\",\"device_id\":\"b1ff4b0a-2020-33e4-9fb4-e5c3fb76d0c5\",\"device_name\":\"J1\",\"channel\":\"MAIN\",\"os_version\":19}"}
post的response数据为

{"ret":0,"user":{"uid":211,"key":"41675b5ab24d461c9111f5c6c197f74b"},"server":{"port":29100,"host":"115.28.224.64"}}
其中user为用户信息,在userLogin中使用,server则是tcp server的地址,用于客户端连接到该tcp server上。

当我们通过管理后台推送一条消息是,可以看到接收到的消息格式为:

{"silent":false,"id":592,"content":"test_content","title":"test_title"}
其中title为推送消息的标题,content则为推送的消息内容。

到此客户端源码分析完毕,基本流程如图所示

kpush源码解析---android端_第1张图片



你可能感兴趣的:(Web开发)