一般情况下,IOT设备(针对wifi设备)在智能化过程中需要连接到家庭路由。但在此之前,需要将wifi信息(通常是ssid和password,即名字和密码)发给设备,这一步骤被称为配网。移动设备如Android、iOS等扮演发送wifi信息的角色,简单来说就是移动应用要与IOT设备建立通信,进而交换数据。针对配网这一步骤,市面上一般有两种做法:
- AP连接方式:IOT设备发出AP(Access Point,可理解为路由器,可发出wifi)信息;移动设备STA(Station,可以连接wifi)连接到IOT设备AP,接着就可以发送wifi(家庭路由的wifi)信息给设备了。另外,也可互换角色,及移动设备释放热点,IOT设备进行连接。
- SmartConfig(一键配置)方式:不需要建立连接,移动设备将wifi信息(需提前获取)写入数据包,组播循环发出此数据包;IOT设备处于监听所有网络的模式,接收到UDP包后解析出wifi信息拿去连网。
可以发现,SmartConfig不需建立连接,步骤较少,实现起来也较容易,并且用户也无需进行过多的操作。本文的IOT设备基于ESP32开发板,解释原理及实现如何通过Android APP发出UDP
包实现SmartConfig。知识点:计算机网络、UDP
、组播、DatagramSocket
...
一、网络知识回顾
计算机网络分层结构如下:
- 应用层:体系中的最高层。任务是通过应用程序间的交互来完成特定网络应用。不同的网络应用对应不同的协议:如
HTTP
、DNS
、SMTP
。其交互的数据单元称为报文。 - 运输层:复杂向两台主机中进程直接的通信提供通用的数据传输服务,使用端口作为向上传递的进程标识,主要有TCP和UDP。
- 网络层:负责为分组交换网络上的不同主机提供通信服务,使用IP协议。
- 网络接口层:包括数据链路层和物理层,传输单位分别是帧和比特。
1. IP协议
IP(Internet Protocol)协议是网络层的主要协议。其版本有IPv4(32位)、IPv6(128位)。与IP协议配套使用的还有地址解析协议(ARP)、网际控制报文协议(ICMP,重要应用即常见的PING,测试连通性)、网际组管理协议(IGMP)。
IP数据报格式,由首部和数据部分两部分组成:
IP地址分类如下:
1.1 两级IP地址
IP地址是每一台主机唯一的标识符,由网络号和主机号组成。A、B、C三类均为单播地址(一对一),D类为多播地址(一对多)。
1.2 三级IP地址
在两级中新增了子网号字段,也称为划分子网。其方法是从主机号借用若干位作为子网号。
子网掩码:是一个网络或子网的重要属性,可通过子网掩码计算出目的主机所处于哪一个子网。若没有划分子网,则使用默认子网掩码(A类255.0.0.0、B类255.255.0.0、C类255.255.255.0)
1.3 无分类编址
无分类编址(CIDR)也称为构造超网,使用网络前缀+主机号规则,并使用斜线标明前缀位数,如:
128.15.34.77/20
1.4 IP多播
多播又称为组播,提供一对多的通信,大大节约网络资源。IP数据报中不能写入某一个IP地址,需写入多播组的标识符(需要接收的主机与此标识符关联)。D类地址即为多播组的标识符,所以多播地址(D类)只能作为目的地址。分为本局域网上的硬件多播、互联网多播两种。
多播使用到的协议:
- IGMP(网际组管理协议):让连接在本地局域网上的多播路由器(能够运行多播协议的路由器)知道本局域网上是否有主机(进程)参加或退出了某个多播组。
- 多播路由选择协议:用于多播路由器之间的协同工作,以便让多播数据报以最小的代价传输。
2. UDP协议
运输层向上面的应用层提供通信服务,通信的端点并不是主机而是主机中进程,使用协议端口号标识进程(如HTTP为80)。UDP协议是运输层中重要的两个协议之一。
- UDP是无连接的
- UDP使用尽最大努力交付,不保证可靠交付
- UDP是面向报文的
- UDP没有拥塞控制
- UDP支持一对一,一对多,多对一和多对多
- UDP首部开销小
二、Java中的UDP
1. Socket
socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。简单来说,socket是一种接口,对传输层(TCP/UPD协议)进行了的封装。
socket通信:
- TCP socket:需建立连接,TCP三次握手,基于流的通信(InputStrea和OutputStream)
- UDP socket:无需建立连接,基于报文的通信。可以组播的形式发出报文,适合本场景中的配网步骤。
2. Java中的socket
2.1 类解释
Java为Socket编程封装了几个重要的类(均为客户端-服务端模式):
Socket
类:
实现了一个客户端socket,作为两台机器通信的终端,默认采用TCP。connect()
方法请求socket连接、getXXXStream()
方法获取输入/出流、close()
关闭流。
ServerSocket
类:
实现了一个服务器的socket,等待客户端的连接请求。bind()
方法绑定一个IP
地址和端口、accept()
方法监听并返回一个Socket
对象(会阻塞)、close()
关闭一个socket
。
SocketAddress # InetSocketAddress
类:
前者是一个抽象类,提供了一个socket地址,不关心传输层协议;后者继承自前者,表示带有IP地址和端口号的socket地址。
DatagramSocket
类:
实现了一个发送和接收数据报的socket,使用UDP。send()
方法发送一个数据报(DatagramPacket
)、receive()
方法接收一个数据报(一直阻塞接至收到数据报或超时)、close()
方法关闭一个socket。
DatagramPacket
类:
使用DatagramSocket
时的数据报载体。
2.2 UDP实例
SmartConfig采用UDP实现,所以在前述知识的基础下,先编写一个例子熟悉java udp的使用,首先建立服务端的代码:
public class UDPServer {
/**
* 设置缓冲区的长度
*/
private static final int BUFFER_SIZE = 255;
/**
* 指定端口,客户端需保持一致
*/
private static final int PORT = 8089;
public static void main(String[] args) {
DatagramSocket datagramSocket = null;
try {
datagramSocket = new DatagramSocket(PORT);
DatagramPacket datagramPacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
while (true) {
// 接收数据报,处于阻塞状态
datagramSocket.receive(datagramPacket);
System.out.println("Receive data from client:" + new String(datagramPacket.getData()));
// 服务器端发出响应信息
byte[] responseData = "Server response".getBytes();
datagramPacket.setData(responseData);
datagramSocket.send(datagramPacket);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (datagramSocket != null) {
datagramSocket.close();
}
}
}
}
客户端发出数据报:
public class UDPClient {
/**
* 指定端口,与服务端保持一致
*/
private static final int PORT = 8089;
/**
* 超时重发时间
*/
private static final int TIME_OUT = 2000;
/**
* 最大重试次数
*/
private static final int MAX_RETRY = 3;
public static void main(String[] args) throws IOException {
try {
byte[] sendMsg = "Client msg".getBytes();
// 创建数据报
DatagramSocket socket = new DatagramSocket();
// 设置阻塞超时时间
socket.setSoTimeout(TIME_OUT);
// 创建server主机的ip地址(此处使用了本机地址)
InetAddress inetAddress = InetAddress.getByName("192.168.xxx.xxx");
// 发送和接收的数据报文
DatagramPacket sendPacket = new DatagramPacket(sendMsg, sendMsg.length, inetAddress, PORT);
DatagramPacket receivePacket = new DatagramPacket(new byte[sendMsg.length], sendMsg.length);
// 数据报文可能丢失,设置重试计数器
int tryTimes = 0;
boolean receiveResponse = false;
// 将数据报文发送出去
socket.send(sendPacket);
while (!receiveResponse && (tryTimes < MAX_RETRY)) {
try {
// 阻塞接收数据报文
socket.receive(receivePacket);
// 检查返回的数据报文
if (!receivePacket.getAddress().equals(inetAddress)) {
throw new IOException("Unknown server's data");
}
receiveResponse = true;
} catch (InterruptedIOException e) {
// 重试
tryTimes++;
System.out.println("TimeOut, try " + (MAX_RETRY - tryTimes) + " times");
}
}
if (receiveResponse) {
System.out.println("Receive from server:" + new String(receivePacket.getData()));
} else {
System.out.println("No data!");
}
socket.close();
} catch (SocketException e) {
e.printStackTrace();
}
}
}
运行结果:
* 发现客户端收到的数据被截断了,这是因为没有重置接收包的长度,在服务端datagramPacket.setLength()
可解决。
三、SmartConfig
根据前面的socket相关应用,基本想到如何实现一键配置。在实际应用中,原理一样,只是增加了组播(这一点需要和IOT设备端共同确定,数据的格式也需协定)。在实现中,需要针对不同IP组播地址发出循环的UDP报文,增加设备端接收到的可能性;同时APP也要开启服务端程序监听发出数据报的响应,以此更新UI或进行下一步的数据通信。相关核心代码如下:
// 对每一个组播地址循环发出报文
while (!mIsInterrupt && System.currentTimeMillis() - currentTime < mParameter
.getTimeoutGuideCodeMillisecond()) {
mSocketClient.sendData(gcBytes2,
mParameter.getTargetHostname(),
mParameter.getTargetPort(),
mParameter.getIntervalGuideCodeMillisecond());
// 跳出条件,发出UDP报文达到一定时间
if (System.currentTimeMillis() - startTime > mParameter.getWaitUdpSendingMillisecond()) {
break;
}
}
组播地址设置:
public String getTargetHostname() {
if (mBroadcast) {
return "255.255.255.255";
} else {
int count = __getNextDatagramCount();
return "234." + (count + 1) + "." + count + "." + count;
}
}
完整代码省略(利益相关,代码匿了^_^
),基本思路很简单。最终的实现是IOT设备收到UDP发出的wifi信息,并以此成功连接wifi,连接服务器,进而绑定账号。