最近一段时间,因为公司需求,需要转向Java Web方向发展,android得放下一段时间(不过还是会利用空余时间坚持写文章~)。
推送功能是app很常用的一个功能,目前能够实现推送的第三方平台也有不少,比如友盟、极光、信鸽等等,总之只要百度一下android推送关键字,就能看到很多的厂家。
这篇博文选择的是极光推送(老板选择的平台…),本文将从android客户端和服务端实现推送,让大家对推送的全流程有一个完整的了解。
我一直认为,无论做那种开发,一定要思路先行,对要实现的功能有一个大致的理解,这样才能以不变应万变。
所以这部分,我个人认为是相比下面技术细节更为重要的内容,只要你理解的推送的大致的工作流程,那么无论你再重新使用哪个推送平台,都能得心应手。
目前提供送服务有非常多平台,而其产品也能囊括消息推送(还有实时聊天的sdk)、短信推送等服务,其平台种类之多……各位小伙伴百度一下就知道了。其实平台的选择在前期并不是很重要(反正XX条推送内都免费),对于初次接触推送的开发者而言,弄清楚推送的工作流程,才是不变的王道。
本篇博文的推送平台采用极光推送。
在正式介绍推送的实现方式之前,先来让我理解一些基本的概念,这样有助于我们后面的理解
推送服务方
提供推送接口及集成SDK,本文中的推送服务方就是极光推送
项目服务器
每个正式的项目,肯定会有一个自己的后台,用于为APP提供接口以及保存APP产生的数据到服务器的数据库中。这里的项目服务器就是指安装并开启了wampServer的本机。
推送请求
通常情况下,我们会把跟推送相关的内容进行封装(Json的方式),里面会包含推送的内容(alert)、标题(title)、平台(platform)等,详细的选项可以参考推送服务方官网的文档,当然,推送请求的内容会因推送平台的不同而有所差异,这里以采用的推送服务方的官方文档为主。
客户端
即发出推送请求或者接受推送消息的一方,
最为简单粗暴的一种方式,一般推送服务方都会给出一个调用推送服务的API,以极光推送为例,其post的调用地址为:
POST https://api.jpush.cn/v3/pus
那么在客户端,直接使用Http,封装推送请求所需要的参数,并向这个地址发送请求
从上图可以看到,请求直接发送给推送方,相关所有参数都需要在客户端操作
优点:直接使用推送方的服务器,所以无需配置,调用方便快捷
缺点:客户端封装大量参数,尤其在android和ios开发同时存在时,微小的变动可能意味着两边大量代码的修改,不利于后期维护
(1)手机客户端封装关键参数(例如推送的type)
(2)项目服务器接受请求,并再次封装一些参数,例如推送消息的titile和alert等
(3)项目服务器作为客户端,向推送方服务器发送Http请求;
首先应该明确的一点是,服务端和客户端是一个相对的概念,可以简单地提交请求的是客户端,处理请求的是服务端。
此时客户端的编程就会很轻松了(就是写一条网络请求就可以了)。麻烦起来的是服务端,因为这二种方法的本质是:把本该由客户端发起的相对复杂的Http请求交给了服务端完成一部分。
那么这种方法和第一种方法的区别是什么呢?
第一种方法,发送推送请求的客户端是移动端设备,在第二种方法中,发送推送请求的客户端,是项目服务器,两种方法接受推送请求的服务端,都是极光推送。客户端不直接与推送服务器打交道。
优点:项目服务器对客户端请求进行了一定封装,当后期需求变动时,利于维护。
缺点:独立开发的话在配置服务器时有点麻烦……其他的看不出什么明显缺点(第二种方式可以和第三中方式做下对比)
第三种实现方式和第二种在大致的工作流程上没有什么太大的区别,主要的不同在于,项目服务器采用了第三方的SDK,在本地集成了之后,只需非常简单的调用一行代码,那就能完成推送的功能(SDK的底层肯定还是网络请求,只不过做了相对完善的封装)。
可以看到极光推送为各种脚本语言编写的服务端都匹配了相应的sdk,还是比较方便的。
第三种方法的工作流程和第二种差不多,这里就不在赘述了。
在实际使用的过程中,推送的对象大致分为如下几种:
(1)无对象差别,全平台推送
(2)无对象差别,特定平台推送
(3)特定对象,全平台推送
(3)特定群组,全平台推送
(4)特定群组,特定平台推送
(5)特定对象,特定台对送
反正就是 对象、群组、平台排列组合,根据项目需求进行设定。
一般比较常见的是特定对象推送,比如QQ消息这种,就是针对某一用户进行推送。
实现这种类型的推送,需要在客户端启动的时候为用户设置一个Alias(相当于唯一标识符),或者tag(表示用户所属群组),来进行特定推送。
关于推送的基本内容就介绍到这里,更多详细的内容,可以参考官方文档~
讲道理,无论是那个第三方平台,都会提供详细的SDK开发文档给开发者,所以说只要开发者有点耐心,一点点按照官方文档上的去实现,遇到不懂的地方度娘一下,基本上就可以实现SDK的集成。
这里我就不完全copy官网的文档,而是在官方文档的基础上,用我自己的语言去描述一下sdk的集成过程,怕我描述有误的小伙伴可以去参考更加权威的官方文档。
当然随着官网集成包的更新,可能我现在在博文里写的方法会过时,所以一切还是以官网的内容为准。
好了,废话到此位置。
同时我们在自己项目的src/main目录下面新建一个文件夹jniLibs,将SDK包libs下面的CPU类型放入我们自己创建的jniLibs里
官方提供了2中导入的方法,一种是jCenter自动导入,一种是手动导入,这里我们采用后者(前者的方法详见官方文档)
(1)将lib目录下的jpush-android-2.1.9.jar包导入到项目的lib目标下
(2)将jar包添加为主工程的依赖。右键jar包,点击add as Library,选择app,确认。
当操作完毕后,我们可以通过FIle->project structure->app->Dependcies 查看主工程添加依赖。
我们要使用极光的推送服务,就必须在其官网创建一个开发者账号(任何第三方推送平台实际上都需要这么做)
创建的过程比较简单,这里就不详细描述了,初学者需要稍微注意一下项目完整的包名
初始化SDK,没什么好说的
MyApplication
package com.example.dell.imooc_jpushdemo;
import cn.jpush.android.api.JPushInterface;
/**
* Created by dell on 2016/9/23.
*/
public class MyApplication extends android.app.Application {
@Override
public void onCreate() {
super.onCreate();
JPushInterface.setDebugMode(true);
JPushInterface.init(this);
}
}
这里是我项目配置的 AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.dell.imooc_jpushdemo">
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23" />
<permission
android:name="com.example.dell.imooc_jpushdemo.permission.JPUSH_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="com.example.dell.imooc_jpushdemo.permission.JPUSH_MESSAGE" />
<uses-permission android:name="android.permission.RECEIVE_USER_PRESENT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:name=".MyApplication"
android:theme="@style/AppTheme">
<meta-data android:name="JPUSH_CHANNEL" android:value="developer-default"/>
<meta-data android:name="JPUSH_APPKEY" android:value="52f7fd72d96df72e2a811d7c"/>
<receiver
android:name="cn.jpush.android.service.PushReceiver"
android:enabled="true" >
<intent-filter android:priority="1000">
<action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED_PROXY" />
<category android:name="com.example.dell.imooc_jpushdemo"/>
intent-filter>
<intent-filter>
<action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
intent-filter>
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
intent-filter>
receiver>
<activity
android:name="cn.jpush.android.ui.PushActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false" >
<intent-filter>
<action android:name="cn.jpush.android.ui.PushActivity" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.example.dell.imooc_jpushdemo" />
intent-filter>
activity>
<service
android:name="cn.jpush.android.service.DownloadService"
android:enabled="true"
android:exported="false" >
service>
<receiver android:name="cn.jpush.android.service.AlarmReceiver" />
<service
android:name="cn.jpush.android.service.PushService"
android:enabled="true"
android:exported="false" >
<intent-filter>
<action android:name="cn.jpush.android.intent.REGISTER" />
<action android:name="cn.jpush.android.intent.REPORT" />
<action android:name="cn.jpush.android.intent.PushService" />
<action android:name="cn.jpush.android.intent.PUSH_TIME" />
intent-filter>
service>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
application>
manifest>
至此androidSDK的配置就完成了,现在我们可以去极光的去推送一下消息,如果应用可以收到推送,这说明配置正确。
服务端这里采用wampServer的方式,当然是处于简单方便的考虑,还可以使用其他的脚本语言来实现,而极光推送的官网也提供了相应的服务端SDK供大家使用
这里的继承方式采用官方提供的sdk(也就是本文介绍的第三种方法),当然,也可以使用PHP向推送方直接发送HTTP请求。
极光官方建议使用composer进行SDK安装,而且给了非常简单的介绍
接下来介绍一下composer的安装。
关于什么是composer,我个人把它理解为android里面的gradle,在android里面,我们可以在gradle中添加一句 “compile XXX”就能轻松引入第三方库,而composer的功能也是如此,composer是 PHP 用来管理依赖(dependency)关系的工具。你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件。
这里简单介绍一下composer的下载,不放心的小伙伴可以去Composer中文网相关查看教程。
传送门:Windows下载
一路next,安装完毕后在cmd里面输入composer
即说明下载成功。
在查阅资料的时候,我看到了composer.phar文件,后来研究了一下,得出了一些结论。
composer.phar 可以理解为composer的一个命令集合,不需要无需下载配置composer环境,只需要在PHP环境下,即可被使用,基本的使用方法是
php composer.phar XXXX
XXX表示composer的命令
而我们在这第一步下载的composer.exe,它实际做的一件事情是下载composer各种命令,并把composer添加到环境变量中(这也是为什么我们安装完毕后,在dos界面输入composer能够有反应的原因),其用法是:
composer XXX
发现没,这里的composer无需使用php,就能够执行composer的命令
综上述所述,如果你不想安装composer.exe,下载一个composer.phar文件,然后在PHP环境下运行就可以了,二者没有太大的差别。
不过有一点需要注意的是,命令的运行目录,一定要是当前的项目目录(存放有composer.json)。
此文件的作用主要用来声明包之间的相互关系和其他的一些元素标签。
接下来,我们来到项目目录(我的是wamp/www/push)
在此目录下新建一个叫 composer.json的文件
在此文件里面输入后保存,退出;
{
"require": {
"jpush/jpush": "v3.5.*"
}
}
(4)下载第三方库
接下来,我们需要在cmd当中不断的cd目录,一直进入到我们当前的push目录,这里教大家一个简单的方法。如果你装了git,那么直接右键push目录,点击git brash here 就能很方便地打开dos界面。
键入下载的命令:
composer install
或者
php composer.phar install
二者的区别请见第二点
在解释上面问题之前,需要讲讲composer.lock这个文件
在安装完所有需要的包之后,composer会生成一张标准的包版本的文件在composer.lock文件中。这将锁定所有包的版本。
使用composer.lock(当然是和composer.json一起)来控制你的项目的版本
这一点非常的重要,我们使用install命令来处理的时候,它首先会判断composer.lock文件是否存在,如果存在,将会下载相对应的版本(不会在于composer.json里面的配置),这意味着任何下载项目的人都将会得到一样的版本。
如果不存在composer.lock,composer将会通过composer.json来读取需要的包和相对的版本,然后创建composer.lock文件
这样子就可以在你的包有新的版本之后,你不会自动更新了,升级到新的版本,使用update命令即可,这样子就能获取最新版本的包并且也更新了你的composer.lock文件。
所以,第一次的话,使用install命令,之后的使用updata命令就好了。
全部下载完成后,我们的当前的项目目录应该是这个样子的。
终于到了编写接口的部分了
为了方便的加载包文件,Composer自动生成了一个文件 vendor/autoload.PHP,我们可以方便只有的使用它在任何你需要使用的地方。
新建一个test.php文件 在里面实现我们的推送逻辑。
php
//composer下载下来的第三方SDK都放在vendor文件夹中
//注意路径
require 'vendor/jpush/jpush/autoload.php';
//接受post来的参数
$params=$_POST;
//创建应用的AppKey和master_secret,可以在极光应用后台查看
$app_key="52f7fd72d96df72e2a811d7c";
$master_secret="847d609885ec219f313b0c12";
/*
* 官方代码
* */
$client = new \JPush\Client($app_key, $master_secret);
$pusher = $client->push();
//设置平台
$pusher->setPlatform('all');
$pusher->addAllAudience();
//唯一标识符,暂不使用
//$pusher->addAlias($params['alias']);
// 简单地给所有平台推送相同的 alert 消息
$pusher->setNotificationAlert('Hello, JPush');
try {
$result=$pusher->send();
var_dump($result);
} catch (\JPush\Exceptions\JPushException $e) {
// try something else here
print $e;
}
官方文档里有更详细的介绍,相信大家结合上面的注释应该是可以看懂的。
这个API的作用是向所有的客户发送一条消息。
现在把wampServer运行起来,在浏览器输入这个接口。
http://localhost/push/test.php
如果调用成功,则说明接口正确~~
到这里,从客户端到服务端的基本推送逻辑已经完成了,接下来我们做的一件事情就是实现定向的推送,即通过这个alias,将消息发送给指定的用户
首先填写一个布局文件MainActivity.xml
比较简陋,一个editText用于设定alias,一个editText用于输入目标alias,还有一个Spinner用于选择推送的类型。
xml代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.dell.imooc_jpushdemo.MainActivity">
<LinearLayout
android:orientation="vertical"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/et_set_alias"
android:gravity="center"
android:hint="设定alias"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/bt_set"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="当前alias:未设置"
android:id="@+id/tv_alias"
android:textSize="18sp"
android:layout_marginTop="22dp" />
<EditText
android:id="@+id/et_alias"
android:gravity="center"
android:layout_marginTop="20dp"
android:hint="输入目标alias"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<Spinner
android:id="@+id/push_type"
android:layout_width="match_parent"
android:layout_height="30dp"
/>
<Button
android:id="@+id/bt_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送"
android:layout_marginTop="23dp" />
<TextView
android:id="@+id/tv_result"
android:text="推送结果"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
/>
LinearLayout>
RelativeLayout>
接下来,我们编写MainActivity当中的逻辑
public class MainActivity extends AppCompatActivity {
//接口地址,根据本机的IP地址改动
private final String url="http://192.168.0.128/push/test.php";
//返回的结果
private TextView tvResult;
private Button btSend; //发送推送请求
private Spinner mSpinner; //选择推送方式
//推送种类
private String pushType;
//spinner的适配器
private ArrayAdapter adapter;
//设置alias的按钮
private Button btSetAlias;
//显示用户设置的alias
private TextView tvAlias;
private String alias;
//更新UI
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
StringBuffer sb = (StringBuffer) msg.obj;
tvResult.setText(sb.toString());
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSpinner= (Spinner) findViewById(R.id.push_type);
tvResult= (TextView) findViewById(R.id.tv_result);
btSend= (Button) findViewById(R.id.bt_send);
btSetAlias= (Button) findViewById(R.id.bt_set);
tvAlias= (TextView) findViewById(R.id.tv_alias);
//spinner内的文字
String[] strings=getResources().getStringArray(R.array.push_type);
List list=new ArrayList<>();
for(String s:strings){
list.add(s);
}
adapter=new ArrayAdapter(MainActivity.this,android.R.layout.simple_spinner_item,list);
//第三步:为适配器设置下拉列表下拉时的菜单样式。
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
//第四步:将适配器添加到下拉列表上
mSpinner.setAdapter(adapter);
mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> adapterView, View view, int i, long l) {
pushType=adapter.getItem(i);
/* 将mySpinner 显示*/
adapterView.setVisibility(View.VISIBLE);
}
@Override
public void onNothingSelected(AdapterView> adapterView) {
adapterView.setVisibility(View.VISIBLE);
}
});
//设置alias
btSetAlias.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
alias=((EditText)findViewById(R.id.et_set_alias)).getText().toString();
//调用SDK接口
JPushInterface.setAlias(getBaseContext(),alias, new TagAliasCallback() {
@Override
public void gotResult(int i, String s, Set set) {
tvAlias.setText("当前alias:"+alias);
Toast.makeText(MainActivity.this, "设置成功", Toast.LENGTH_SHORT).show();
}
});
}
});
btSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
sendRequest();
}
});
}
private void sendRequest() {
new Thread(new Runnable() {
@Override
public void run() {
String alias=((EditText)findViewById(R.id.et_alias)).getText().toString();
try {
// 传递的数据
String data ="&alias="+URLEncoder.encode(alias,"UTF-8")
+"&push_type="+URLEncoder.encode(pushType,"UTF-8");
URL httpUrl = new URL(url);
HttpURLConnection urlConnection = (HttpURLConnection) httpUrl.openConnection();
urlConnection.setRequestMethod("POST");
// 设置请求的超时时间
urlConnection.setReadTimeout(5000);
urlConnection.setConnectTimeout(5000);
//调用conn.setDoOutput()方法以显式开启请求体
urlConnection.setDoOutput(true); // 发送POST请求必须设置允许输出
urlConnection.setDoInput(true); // 发送POST请求必须设置允许输入
//setDoInput的默认值就是true
OutputStream ost = urlConnection.getOutputStream();
PrintWriter pw = new PrintWriter(ost);
pw.print(data);
pw.flush();
pw.close();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
StringBuffer sb = new StringBuffer();
String s;
while ((s = bufferedReader.readLine()) != null) {
sb.append(s);
Log.d("111", "run: "+sb.toString());
}
Message msg = new Message();
msg.obj = sb;
handler.sendMessage(msg);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
R.array.push_type里面的内容:(在res/value/arrays.xml中配置,如果没有这个xml文件,手动创建)
<resources>
<string-array name="push_type">
<item>push Typeitem>
<item>new_taskitem>
<item>send_taskitem>
<item>get_taskitem>
<item>finish_taskitem>
string-array>
resources>
注释都比较清楚,网络操作使用的是调用android原生的API,当然也可以使用一些网络框架来完成。
最后,我们对服务端的代码进行改动
服务端test.php
//composer下载下来的第三方SDK都放在vendor文件夹中
//注意路径
require 'vendor/jpush/jpush/autoload.php';
//接受post来的参数
$params=$_POST;
//创建应用的AppKey和master_secret,可以在极光应用后台查看
$app_key="52f7fd72d96df72e2a811d7c";
$master_secret="847d609885ec219f313b0c12";
$title='';
$alert='';
//接受客户端参数
@$alias=$_POST['alias'];
@$push_type=$_POST['push_type'];
if(empty($alias)){
echo 'alias null';
return;
}
switch ($push_type){
case "new_task":
$title="新订单提醒";
$alert="您有新的订单";
break;
case "send_task":
$title='订单指派提醒';
$alert='您的订单已被指派';
break;
case "get_task":
$title='订单受理提醒';
$alert='您有订单已被受理';
break;
case "finish_task":
$title='订单完成提醒';
$alert='您的订单已完成';
break;
}
/*
* 官方代码
* */
$client = new \JPush\Client($app_key, $master_secret);
$pusher = $client->push();
//设置平台
$pusher->setPlatform('all');
//唯一标识符
$pusher->addAlias($alias);
echo $alert." ".$title;
$pusher->setNotificationAlert($title, $alert);
try {
$result=$pusher->send();
var_dump($result);
} catch (\JPush\Exceptions\JPushException $e) {
// try something else here
print $e;
}
服务端这里新增了对post参数的接收,当然……这里的参数验证逻辑写的还不够严谨,在实际项目中肯定还要有更多的验证及错误信息反馈。
PHP端代码相对比较简单……当然,写的还不够优雅,不过已经可以实现基本的功能了~
至此,代码部分就全部完成了,现在让我们来测试一下效果!
首先我开启了2台虚拟机,并且提前设置好了他们的alias
夜神模拟机的alias设为001,推送的type设为new_task
as 的模拟机alias 设为002,推送的type设为send_task
接下来,我们点击推送
国庆期间花了不少时间折腾这个……现在才整理出来……(不过好在整理出来了)
这个demo从客户端到后台,相对比较全面地介绍了推送的相关内容,当然,这些内容在官网上都能找到相应的资料,而且这篇文章的代码还不够优雅(尤其是PHP部分),实现的功能还比较基础,所以要完成更加复杂功能的小伙伴,还需要去官网深入学习文档,以满足实际的需求。
好了~最后感谢坚持看到了这里的小伙伴(≧▽≦)/
Git 项目地址:
https://github.com/huyifan/Imooc_JpushDemo