最近看到了一个开源的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则为推送的消息内容。
到此客户端源码分析完毕,基本流程如图所示