1、建立连接的原理
在实现软件与硬件交互的时候,首先需要了解该硬件的构造,运行流程等相关操作,熟读相关的文档,知道硬件开发商那边给出了哪些接口,分别对应我们项目中的那些模块等等
就拿我这个项目的硬件来说,供应商给出的连接流程如下图:
我们可以看到,整体流程并不复杂。执行操作的前提是App连接上硬件的WiFi,之后就是通过 UDP协议
来发送指令。这里只大概介绍一下UDP协议
,具体了解可以自行百度。
UDP协议
是OSI 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠
信息传送服务
举个例子,就比如送信,你写的一封信给你的朋友,你只需要将你朋友的名字、地址等信息填好,然后寄出去就可以了。而你的朋友在它的邮箱里可以收到它。你不需要一直等朋友收到信你才能走,朋友也不需要收到信才能离开。换句话来说,就是不需要发送方和接收方一直保持在线。
为什么称之为不可靠
信息传输服务?因为你寄出去了之后你就无法监控它了,这封信有可能半路丢了啊,不小心被烧掉了啊之类的。你只负责发送,至于朋友能不能收到你就管不了了。
2、 “信件”和“邮递员”
UDP的底层也是使用了Socket
套接字,所以我们这里用到了DatagramSocket
类,你可以把它看成邮递员
//创建 “邮递员” 13047为我们本机开放的端口,可以理解为寄件时填写的本人地址
DatagramSocket sendSocket = new DatagramSocket(13047);
既然邮递员有了,接下来就是写信,DatagramPacket
类就可以理解为信件
//msgs 是需要发送的数据包,通过EncodingUtil.hexToByte转换为二进制数组
byte[] data = EncodingUtil.hexToByte(msgs);
//创建DatagramPacket对象,用来存放发送的数据,端口和ip
DatagramPacket packet = new DatagramPacket(data,data.length);
//指定广播的范围和端口(收件地址)
//58121 硬件指定的接收端口
packet.setPort(58121);
//255.255.255.255 进行全网段广播,所有在本网域中的58121端口都会收到我发出去的信息
packet.setAddress(InetAddress.getByName("255.255.255.255"));
//使用DatagramSocket的send方法,发送数据包
sendSocket.send(packet);
//使用完流后应该关闭,这是一个好习惯
sendSocket.close();
既然信发出去了,那我们就等待对方回应吧。但是UDP是个不可靠的“邮递员”,对方会不会有没有收到信件的可能?这肯定是会有的,那该怎么处理这种情况?
3、对方可能未收到如何处理
首先我去向硬件方咨询了一下,如果硬件收到了UDP广播,在1~2秒内就会有回应的。所以我们可以设定一个等待时间,如果超时了,就重新发送一次
//计时器是否生效标志位
private boolean isAuto = false;
//创建CountDownTimer 对象,设置计时器
private static CountDownTimer timerAuto;
//创建一个计时器 CountDownTimer(总时长,每次执行间隔时长)
//这里给了15s的计时,如果15s内未收到硬件的回应,就执行对应的操作
timerAuto=new CountDownTimer(1000*15,1000) {
@Override
public void onTick(long millisUntilFinished) {
}
//当计时器走完总时长时执行
@Override
public void onFinish() {
cancel();//取消计时器
//关闭接收回应的监听
//关闭监听
receiverListener.exit();
//创建线程,再次发送
new Thread(new Runnable() {
@Override
public void run() {
try {
//执行发送UDP广播
sendBroadcast();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
};
//如果未重新发送过,就启动计时器
//如果已经重发过了,就不再次重发了,发了两次都没有收到,那应该是其他地方的问题了
if(!isAuto){
isAuto=true;//标志位状态变更
timerAuto.start();//启动计时器
}else{
// 如果多次重发都未回应,那么就可能有以下几种情况:
//1. “信件”的问题,检查一下发送的端口号是否正确,本地开启的端口是不是被占用,发送的数据包格式是否正确等
//2. “道路”的问题,App与硬件通信的前提是需要App连接硬件的Wifi,检查一下连接过程中设备与硬件WiFi是否一直保持正常连接,毕竟路都不通,邮递员也走不了。
//3. “接收方”的问题,如果我们这边都没问题,那么就要考虑一下是不是硬件的问题,这个就需要和供应商那边去调解
//提示:等待超时,请检查App是否与硬件WiFi保持连接...
}
如果多次重发都未收到回应,那就要变更一下策略了,毕竟在某些硬性条件不满足的情况下,无论你发送多少次都是无效的。
4、接收“回信”
当硬件收到我们发送过去的指令后,就会进入第二步,硬件向App发送硬件备信息。那么该如何接收回应呢
使用Runnable
开启一个线程服务用来监听硬件的回应
/**
* 开启监听
*/
private static class SearchListener implements Runnable{
MyApplication myApplication;//存放全局变量
Activity activity;//页面activity
DatagramSocket receiverSocket;//接收socket
Integer index=0;//用来判断是第几次的数据交互,因为每次发送的数据都不一样
String ssid;//App设置的SSID
String pass;//App设置的热点密码
}
//实现Runnable内的方法
@Override
public void run() {
}
接下来创建一个构造器给外部调用,并且将所需的参数传递进来
//构造函数,开放给外部调用,同时传入需要的参数
SearchListener(Activity activity,Integer index,String ssid,String pass){
this.index = index;
this.ssid = ssid;
this.pass = pass;
this.myApplication= (MyApplication) activity.getApplication();
};
上面的操作都是为了给下一次发送的指令作准备工作,但是接收其实用不到这些。
接收这里同样使用的是DatagramSocket
,写在实现的run方法中
按照硬件文档说明,硬件会向发送方的原端口回应信息,在上面我们发送的时候开启的本地端口是13047,所以这里同样使用13047端口来监听回应的指令
@Override
public void run() {
Log.e("监听提示:","监听已启动...");
try {
//建立 13047 端口的DatagramSocket对象
receiverSocket = new DatagramSocket(13047);
//接收数据缓冲区大小
byte[] receiverData = new byte[1024];
//使用 DatagramPacket 对象设置接收对象参数
DatagramPacket receiverPacket = new DatagramPacket(receiverData,receiverData.length);
//使用DatagramSocket对象接收数据包
//该方法会阻塞,直到接收到数据报文
try {
receiverSocket.receive(receiverPacket);
}catch(SocketTimeoutException e){
e.printStackTrace();
}
//使用receive方法来接收之后,线程会进入阻塞状态,所以在接收到回应之前,代码都不会往下执行
//------------------------------------------------------------------------------------
//拆解接收到的数据包
//获得接收到的IP地址
//数据缓冲区
byte[] buffer = receiverPacket.getData();
//将接受到的二进制数据转换为16进制字符串
String datas = EncodingUtil.toHexString(buffer)
//接收到回应后,就将计时器关闭
timerAuto.cancel();
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
到这里,我们就已经实现了接收硬件的“回信”,而且在之前我们已经写好了计时器,如果超时都未收到回应,那就关闭监听,再次发送指令。若多次发送都未收到回应,那就给用户相应的提示。
5、多次数据交互
既然目前App发送指令给硬件,硬件也能够收到并且回应,说明我们已经完成了一个完整的流程。按照最开始的那张流程图,可以将整个连接配置过程分为3次发送和2次接收。既然有多次类似的操作,那么代码就可以封装起来,方便复用
首先是发送的方法,需要传递SSID和密码等参数,所以封装成sendBroadcast()
方法如下:
/**
* 发送指令
* @param msg 初始指令
* @param index 判断是第几次发送
* @param ssid 热点的SSID
* @param pass 热点的密码
* @throws IOException
*/
private static void sendBroadcast(String msg, int index, final String ssid, final String pass) throws IOException {
DatagramSocket sendSocket = new DatagramSocket(13047);
String MSG_STR = msg.replace(" ","");//先空格
//生成校验位
String CRC8_ITU = EncodingUtil.CRC8(EncodingUtil.hexToByte(MSG_STR));
//组合成完整的指令
MSG_STR = MSG_STR + CRC8_ITU.toUpperCase();
byte[] data = EncodingUtil.hexToByte(msgs);
//创建DatagramPacket对象,用来存放发送的数据,端口和ip
DatagramPacket packet = new DatagramPacket(data,data.length);
packet.setPort(58121);
packet.setAddress(InetAddress.getByName("255.255.255.255"));
//使用DatagramSocket的send方法,发送数据包
sendSocket.send(packet);
sendSocket.close();
//如果不是第三次发送的指令,就开启监听,接收硬件的回应
if(index!=3){
//创建接收监听对象
receiverListener = new SearchListener(myActivity,index,ssid,pass);
//开启监听线程
new Thread(receiverListener).start();
}else{
//如果是第三次发送,发送完后就可以不用接受回应了
//直接提示用户配置完成,打开手机热点
}
}
接收监听在上面就已经实现了,接下来我们就需要根据传递进来的index
完成不同的操作
//使用receive方法来接收之后,线程会进入阻塞状态,所以在接收到回应之前,代码都不会往下执行
//------------------------------------------------------------------------------------
//拆解接收到的数据包
//获得接收到的IP地址
//数据缓冲区
byte[] buffer = receiverPacket.getData();
//将接受到的二进制数据转换为16进制字符串
String datas = EncodingUtil.toHexString(buffer)
//接收到回应后,就将计时器关闭
timerAuto.cancel();
if(index==1){
//如果是第一次发送指令,硬件应该会回应硬件有关的一些信息
//那么我们可以通过这些信息来生成第二次发送的指令
//对接收到的数据进行处理,得到硬件版本、IP、端口等所需要的参数
// ----- 数据处理 略 ---------
String Msg = "";//生成的第二次初始指令
//调用发送指令的方法
exit();//关闭监听的socket,以免出现端口号被占用的问题
//调用发送指令的方法,这次的index为2
sendBroadcast(Msg,2,ssid,pass);
}else if(index==2){
// ----- 数据处理 略 ---------
exit();//关闭监听的socket,以免出现端口号被占用的问题
//生成第三次发送指令
String Msg = "";//生成的第二次初始指令
//调用发送指令的方法,这次的index为3
sendBroadcast(Msg ,3,ssid,pass);
}
我们来看看整体的代码执行流程
6. 小结
在与硬件的对接开发时,很多没有接触过这方面的同学可能会感到害怕和不知所措,毕竟我们学习的是软件技术专业,很少跟硬件打交道。但是其实这一路流程走下来,用到的技术点大多都是我们所熟知的,比如socket、UDP广播、计时器等等。
DatagramSocket
Java使用DatagramSocket代表UDP协议的Socket,DatagramSocket本身只是码头,不维护状态,不能产生IO流,它的唯一作用就是接收和发送数据报,Java使用DatagramPacket来代表数据报,DatagramSocket接收和发送的数据都是通过DatagramPacket对象完成的。
最重要的是在开发之前,一定要熟读相关的硬件文档,多多了解硬件的操作流程、开放接口等相关的知识。将硬件当做是一个工具,发送指令是为了告诉它我们需要什么,但是它不能听懂我们日常这种对话,所以处理硬件和使用者关系,就是我们开发者的职责所在。