IOT設備SmartConfig實現
一般情況下,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,連接伺服器,進而綁定帳號。