参考资料:http://blog.csdn.net/qq_17250009/article/details/52774472
MQTT官网:http://mqtt.org/
MQTT介绍:http://www.ibm.com
MQTT Android github:https://github.com/eclipse/paho.mqtt.android
MQTT API:http://www.eclipse.org/paho/files/javadoc/index.html
MQTT Android API: http://www.eclipse.org/paho/files/android-javadoc/index.html
前言
这几天因一些需求要搞个云端通信,本来想是直接找个比较完善的物联网平台调用他们的SDK就行了
然而显然不可能这么顺利,直接下阿里云和腾讯云的物联网平台Android连接例程,都不能直接用。。。
而我目前只懂一点点Android,关于联网部分是完全空白。。。所以想改也改不来。
不过我发现这些物联网云平台基本都用了MQTT协议,那既然直接上啃不动,我们就慢慢来,哪里的基础知识不会就去补哪里。
因此我决定先来了解MQTT协议,实现MQTT协议的通信。
网上关于这个的教程不少,但是个人碰到的问题都可能不同,不一定都会在网上提到,特此把这几天做的仔细记录下来,说不定能帮到和我一样要搞这方面的小白。
首先是安装服务器,版本挺多的,http://activemq.apache.org/apollo/download.html,这是我下载的Apollo
我目前只安了windows的,所以也只说这个安装,以后有机会补充Linux安装
windows-MQTT服务器安装
1、去上面的网站下载解压。
2、命令行进入解压文件夹下bin
3、运行apollo create mybroker命令,其中mybroker是服务器的名字,可以自己改,此时bin下应该有个mybroker文件夹了
4、根据提示运行服务器即可,我这边就是输入命令:"D:\Download\apache-apollo-1.7.1-windows-distro\apache-apollo-1.7.1\bin\mybroker\bin\apollo-broker" run
5、启动成功,显示如下
图1
注意最后七八行的内容,分别写出了相应协议访问服务器的端口(后面会用到),最后两行写了服务器管理页面的地址,在浏览器里输入地址即可查看服务器的一些信息(网页需要输入账户密码登录,默认的话就是admin,password)
注:安装目录\etc\apollo.xml文件下是配置服务器信息的文件。etc\users.properties文件包含连接MQTT服务器时用到的用户名和密码,默认为admin=password,即账号为admin,密码为password,可自行更改。
至此 windows端安装mqtt服务器就已经完成了,这一步比较容易,网上相关教程很多,关键我们还得实际建立客户端通信以下,看到效果才知道服务器真的有用。下面才是关键。
Android客户端MQTT通信Demo
首先我们需要找到MQTT通信的java包,导入Android Studio
下载链接:https://pan.baidu.com/s/1nw39tYp
jar导入Android Studio教程参考:http://www.cnblogs.com/otaganyuki/p/8495125.html
接下来就可以调用此包与服务器建立连接,尝试发送消息了
建立连接主要用到MqttClient这个类,需要设置的参数可以具体看下面代码,这里先特别说以下传入的localhost,
我原先并没有什么网络方面的知识,只是知道这个要传个ip地址,便百度搜ip地址,百度就会告诉你你的ip地址
我就把它填上去了,然而怎么也连不上。。。后来我想会不会是要从自己电脑里“网络和internet设置”那里 查看自己的ip
结果一看,这两个ip确实不一样,填后者在模拟器上就可以连接成功,这里我基本就有点头绪了。
ip地址分为内网ip和外网ip,外网ip是我们接入因特网的地址,但是这个地址不一定就是一台设备独有的,外网ip会分给某些服务器和局域网
在局域网下,所有连着局域网的计算机都共有一个外网ip,通过从某个共同的出口访问因特网。
而内网ip是在一个局域网下用于区分局域网下的计算机所设置的地址。
我的电脑wifi连在路由器作为服务器,开的虚拟机也是通过这里访问网络,我填的内网ip它就能访问到我的电脑的服务器。
但是填的外网ip地址就不行,这是为什么呢
我猜测可能是因为外网ip只能定位到我所连接的局域网的ip地址,无法定位到局域网下的某一台机器,自然就没法访问我的服务器了
但是如果只能在局域网下通信就未免有点没意思了,通过因特网络连接方法有以下几种
1、租个有独立ip的服务器,通过其ip访问
2、内网穿透/端口映射
由于这些我当时只是通过猜测和百度得出的推测,所以我想先尽快试试可不可以连接验证推测,而不是花钱买服务器这么麻烦的事。
我试了内网穿透,确实是可以的。内网穿透具体百度,大概就是将本计算机某个端口和某个具有独立ip的服务器的某个端口进行映射,
这样客户端访问具有独立ip的服务器,独立ip的服务器又把请求发到自己的电脑上,实现了连接。
提供内网穿透服务的东西蛮多的,我用的是花生壳(花了六块钱一个月,最省力,其它ngrok之类的比较麻烦)
图2
稍微说一下那个花生壳吧,安装后点内网穿透,在弹出的网页里 点击添加映射
填的ip是自己作服务器的计算机的内网ip,端口随机吧,固定的要钱。之后就会出现如上内容(我第一天搞得时候上面的东西出不来,第二天就好了。。。。)
之后就根据前面的网址 查到其外网ip地址,用这个ip填入客户端连接,端口也要用图2里的端口(如果是内网里面连接就填图1里的后两行上的端口)
这个解决了就容易了,以下为MainActivity代码,复制进去再在xml文件里加两个按钮布局,别忘了在AndroidManifest里加上权限(加在package下面)
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
就能运行了,如果没加权限,会很快弹出fail 异常内容
java.net.SocketException: socket failed
package com.example.a86275.mqtt_test;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.util.concurrent.ScheduledExecutorService;
public class MainActivity extends AppCompatActivity {
public String TAG="MyTAG";
//private String host = "tcp://内网ip:服务器端口号";
private String host = "tcp://外网代理ip:端口号";//代理ip 花生壳
private String userName = "admin";
private String passWord = "password";
private String clientId="AndroidClient1";
private int i = 1;
private MqttClient client;
private String myTopic = "AndroidTopic";
private MqttConnectOptions options;
//private ScheduledExecutorService scheduler;
public Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 1) {
Toast.makeText(MainActivity.this,"Success",Toast.LENGTH_SHORT).show();
try {
client.subscribe(myTopic, 1);
} catch (Exception e) {
e.printStackTrace();
}
}else if(msg.what==2){
Toast.makeText(MainActivity.this,"fail",Toast.LENGTH_SHORT).show();
}else if(msg.what==3){
Toast.makeText(MainActivity.this,(String)msg.obj,Toast.LENGTH_SHORT).show();
Log.d(TAG, "handleMessage");
}
}
};
Button button1=(Button)findViewById(R.id.button1);
Button button2=(Button)findViewById(R.id.button2);
button1.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
class MyThread extends Thread{
@Override
public void run(){
init();
try {
client.connect(options);
Message msg = new Message();
msg.what = 1;
handler.sendMessage(msg);//连接成功
} catch (Exception e) {
e.printStackTrace();
Message msg = new Message();
msg.what = 2;
handler.sendMessage(msg);
//连接失败
}
}
}
new MyThread().start();
}
});
button2.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
publish("MessageReceived");
}
});
}
private void init() {
try {
//host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
client = new MqttClient(host, clientId,
new MemoryPersistence());
//MQTT的连接设置
options = new MqttConnectOptions();
//设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(true);
//设置连接的用户名
options.setUserName(userName);
//设置连接的密码
options.setPassword(passWord.toCharArray());
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
//设置回调
client.setCallback(mqttCallback);
} catch (Exception e) {
e.printStackTrace();
}
}
private MqttCallback mqttCallback = new MqttCallback() {
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {//This method is called when a message arrives from the server
String str1 = new String(message.getPayload());
Log.d(TAG, "messageArrived: "+str1);
Message msg=new Message();
msg.what=3;
msg.obj=str1;
handler.sendMessage(msg);
}
@Override
public void deliveryComplete(IMqttDeliveryToken arg0) {
//Called when delivery for a message has been completed
}
@Override
public void connectionLost(Throwable arg0) {
// This method is called when the connection to the server is lost.
Log.d(TAG, "connectionLost: ");
}
};
public void publish(String msg){
String topic = myTopic;
Integer qos = 0;
Boolean retained = false;
try {
client.publish(topic, msg.getBytes(), qos.intValue(), retained.booleanValue());
} catch (MqttException e) {
e.printStackTrace();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
try {
scheduler.shutdown();
client.disconnect();
} catch (MqttException e) {
e.printStackTrace();
}
}
}
布局代码
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="connect"
android:id="@+id/button1"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="send"
android:id="@+id/button2"/>
运行效果就是点击connect,连接成功会弹出success(很快),同时能在管理服务器的网页上看到连接的设备,否则会弹出fail(过好一会儿才弹出),点击send就会发送消息到某个topic 所有订阅了此topic的客户端都会接受到此消息
我在模拟器和手机都订阅同一个topic,其中一个点send两个都会弹出“message received”
效果如下
以上就实现了安卓客户端和服务器的连接和通信。后面会讲树莓派和服务器建立连接和通信。
后来补充的,我自己碰到的一点问题
权限之前讲过了,不过容易忘 ,再提一下
刚调用建立连接的线程后不能马上调用pulish方法发送消息,此时连接并没有建好,可能会出现空指针异常导致活动停止
主题并不需要创建,直接使用就是了。
主题可能是管理员在服务端预先定义好的,也可能是服务端收到第一个订阅或使用那个主题名的应用消息时动态添加的。服务端也可以使用一个安全组件有选择地授权客户端使用某个主题资源。