一般情況下,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,連線伺服器,進而繫結賬號。