2019-01-16 17:21
Github:
我使用国内的网站服务时经常需要收验证码,所以特意保留了国内的手机和卡。但是这个手机留在家里,我白天在外面需要收验证码时就没办法了。于是考虑做一个app,在我需要的时候将最新的若干条短信发给我。
本质上这是一种“云短信”需求。现有一些服务,如谷歌Message等可以实现类似功能,但我的需求与这些服务略有差别:
首先需要一个Android app运行在手机上。
这个app需要不定时收到消息。需要一个消息传输服务。选用了谷歌的Firebase Cloud Messaging(FCM),也就是以前的GCM。
需要发起请求,传输和阅读短信。决定搭建一个超小型的http服务端。选用Node.js,因为我熟。
服务端需要运行于一个平台上。选用谷歌云。本来AWS更熟悉但我的账号过了免费期了,借这次机会发现谷歌云某一档引擎是长期免费的。
详见代码。代码不长,还有注释。
以下就只是`聊聊开发过程中的感受和一些踩过的坑好了。
没有用别的库,连Express都没用,自己写了一个路由,像这样:
// 手机端:更新设备的FCM Token。
if (method == 'POST' && path == '/token') {
getBody(req, (data) => {
token = data;
console.log("New token: " + data);
});
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Token update success!")
return;
}
// 手机端:发送短信文本,格式为JSONArray。
if (method == 'POST' && path == '/sms') {
getBody(req, (data) => {
var sms = JSON.parse(data);
resBuffer.end(parseSms(sms));
console.log(JSON.stringify(sms, null, 2));
});
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Send sms success!")
return;
}
因为总共就4条API,实在没必要上Express。Do it in hard way了。还能大大减少依赖库体积。
开始还想说要不学着搭一下https服务器。还真的弄出来了,和http的区别是额外需要几个certificate用的文件,privatekey.pem之类的。但是访问不了。浏览器(Chrome)直接报有害网站给屏蔽了(冤枉那包大人!),Android端也联不通。又换回http了。应该是证书没有认证的缘故?
基本思路是网页那边的请求过来时,将response暂存起来,同时发出FCM消息给手机。手机收到消息后会使用另一个API将短信文本发来,这时再返回给刚才暂存的response。暂存机制的代码长这样:
// 暂时地保存请求方的http response,以便在获取短信文本后通过它返回给请求方。
// 您也可以考虑不保存response,而是通过FCM消息的方式发送文本给请求方。
var resBuffer = function() {
var resTemp;
var timer;
// (据说)一些浏览器在超过120秒未获得响应时会终止连接。因此我们限制等待时间为110秒。
var start = (res) => {
clearTimeout(timer);
drop();
resTemp = res;
timer = setTimeout(drop, 110000);
console.log("Waiting for fetching sms result");
};
var drop = () => {
if (resTemp) {
resTemp.writeHead(204, { "Content-Type": "text/html; charset=utf-8" });
resTemp.end("<p>Failed to fetch sms!</p>");
resTemp = null;
console.log("Res buffer dropped due to timeout or new request coming");
}
};
var end = (html) => {
clearTimeout(timer);
if (resTemp) {
resTemp.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
resTemp.end(html);
resTemp = null;
console.log("Sms fetching result sent to requester");
} else {
console.log("Previous res buffer was unexpected dropped!")
}
};
return {
start: start,
end: end
};
}();
说实在话我不知道这是不是处理这个问题(这算啥?“异步响应请求”?)的标准方式。就是说收到请求后需要完成一些高延迟的工作再响应,或者可能不响应。我总感觉request和response是一起来的也应该一起走,不应该把其中一个存起来。但我也说不出为什么不该这样。也许有更好的做法,或者其实应该用其它方式返回消息。
Response只保存110秒,因为我好像在哪看到说浏览器120秒等不到response就不等了。我这边至少要在此之前返回出错信息,加上网络延迟所以限定为110秒。
以及原来JavaScript里的Singleton是这样搞的呀!
返回的数据类型,手机端统一为text/plain
以便log,网页端统一为text/html
以便render。
然后因为只有一个用户所以也没有搭数据库。但是有两样东西就需要另找地方保存了:一是设备的FCM Token,二是一个静态password,以防知道我服务器网址的人都来看我短信。
FCM Token可能会由客户端更新,没办法只好用一个变量存着。每次服务器重启就丢失了。所以其实还是需要数据库的。Firebase好像有数据库服务。
用户口令更新频率低,所以静态保存在文件中。正好FCM要用到一个私钥文件,我就直接在里面夹带私货。这样在公开repo里是看不见的。
(这才是我本行啊,JavaScript是什么鬼!)
这是我第一个用Kotlin写的项目。之前以为会很难,没想到IDE出来接管全场!我懵懵懂懂地写点啥,intellij马上接茬:
“亲object
不是这样用的哟我替您改好了亲”
“亲我们家不用写分号的亲”
“亲您写的是java风格的哟要不要试试我们家新出的when
语句呀亲”
……
真的如果让我声明知识产权我都不好意思说这app是我写的……
构架也很简单。一个Activity;一个Service后台运行:响应消息,读取短信,发送,更新token;一个网络客户端发送http请求,用了谷歌的一个库Volley来实现的。
Volley属于轻量级的网络客户端,小,所需代码短,功能简单。这个项目因为应用场景很少也不复杂所以够用了,再复杂一些还是需要Retrofit。
第一次使用时需要输入服务器IP,之后IP储存在SharedPreferences中。某些时候服务器会更换IP地址,其实新地址可以放在FCM消息中传过来。考虑下一版加上。
FCM消息下可以有notification
和data
等关键字。如果notification
存在,这条消息就被定义为一条可显示的“通知”,否则就只是数据消息。其在处理方式上的区别在于:
onMessageReceived()
方法来处理数据。onMessageReceived()
,只会进入Android的通知栏,显示notification
包含的数据,即使同时存在data
数据,也只会作为extra被放入Intent中;而不含notification
的数据消息则会正常触发onMessageReceived()
。我的服务端发送的消息是数据消息。目前只包含两个有用信息,一是一个collapseKey通知app发送短信,不过目前消息只有这一种用途,只要收到就肯定是请求发送短信的,这个值实际上没用;二是一个数字告诉app读取多少条短信。
读取短信需要用户授权。我在app启动时,在onCreate()
里检查权限,如下:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_SMS), permissionRequestCode)
}
理论上第一次打开app时就应该请求授权,之后就不用重新授权了。不过Android的权限机制发生过变化,实测结果是:在Android 8设备上结果如预期;在Android 4.4设备上,启动app并不会触发授权请求,app会认为已经授权,但在第一次读取短信时还是会弹出请求。这就很麻烦,授权必须预先完成,否则等到用户请求读取短信时多半是远程状态,无法操作设备的。如果找不到更好的办法,hacky的解决方式是onCreate()
时先读取一次短信。
目前的app并没有很完善。很多需要在onResume()
里做的检查都没有做,比如检查Google Play Service的状态,这是FCM SDK要求的。
还是推荐阅读官方文档:
FCM Android SDK 教程
FCM 服务端 SDK 教程
FCM 服务端发送消息
FCM 首页(创建新项目)
Volley 官方教程