socket 是什麼?socket 是在 TCP/IP 傳輸重要的環節,透過這樣的介面 API,讓我們不用去處理 TCP/UDP 通訊協議的底層,進而執行網路通信。micropython 也移植電腦平台 python 的 socket 方法,這篇就來瞭解如何使用 usocket 類別來做最基礎的 tcp 訊息傳送。
小提醒: 進行 socket 通訊前,ESP32 必須要連上網路,如果不熟悉怎麼 wifi 上網的朋友,可以參考下面這篇:
1. socket 簡介
socket 在 wiki 中的定義如下:
在作業系統中,通常會為應用程式提供一組應用程式介面(API),稱為插座介面(socket API)。應用程式可以通過插座介面,來使用網路插座,以進行資料交換。
有點難懂對吧? 😆 換個角度思考一下,如果有一天你想要撰寫網路應用程式時,什麼是第一個需要面對的課題? 想必就是搞定 TCP/UDP 通訊協定! 幸運的是,OS(作業系統) 通常會提供 socket 這樣的介面,搭起一個橋樑,概念有點像是把 底層 TCP/IP 的工作『封裝』起來,程式設計師僅需專注在 Server 與 Client 端的上層應用開發即可,所以 socket 放在 TCP/IP 網路層級的位置,就會像下面這樣:
有了基本的認知後,現在就來看一下怎麼在 ESP32 內利用 socket API 進行網路通訊。
2. socket 類別與 TCP 連線流程
socket 介面可以用的方法有很多,有分 Server 或 Client 端使用的,也有兩者都可共用的,如果要以 ESP32 的作為遠端受控的角色來說,server 端還是比較常用的,因此哥這邊整理一些關於伺服器端,如採用 TCP 協定時會用到的方法:
- socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP): 初始化 socket 物件,可以用來設定 TCP或 UDP通訊協定; IPv4 或 IPv6 等。
- socket.bind(address):Server 端用來綁定指定 IP 與 Port。
- socket.listen(backlog):設定最大的連線數量。(這邊的 backlog 就是指定在 server 端最大的連線數量)
- socket.accept():接收來自 client 的連接。
- socket.send(bytes):在建立連線後,利用此方法來傳送資料訊息,小地方提醒一下,傳送的資料是 bytes 型態。
- socket.recv(bufsize):接收來自 client 傳遞的資料,bufsize 指定最大的資料 bytes大小。
上面的這些方法先大概有個基本認知,後面會在解釋更清楚,搭配下面的這張圖表應該就會更瞭解完整的TCP連線流程。
首先來看一下 server 端, 一開始初始化 socket(),透過 bind() 綁定 Ip 與 服務 port ,並設定最大連線數,緊接等待 client 的連線,此處由於是阻斷模式(Blocking Mode),也就是程式會在 此處停下等待連線;待 tcp 建立連線成功後(也就是 accept()成功連到 client ),之後接收來自 client 訊息,並回傳訊息,完成關閉此次連線狀態,以上就是 tcp server 最基本的連線流程。
相較於 server 較多的連線步驟, client 的容易理解多了, 初始化 socket() => 嘗試建立連線connect() => 傳送訊息 => 接收訊息=> 結束連線,整個流程就跟我們一般認知的過程類似,所以哥這邊就不再多加著墨。
3. 建立簡易 TCP Server
接著我們可以利用 socket 方法在 ESP32 架設一個簡易 TCP server,並透過電腦平台的 TCP 測試軟體做為進行,實現遠端控制 IO 功能。
>> 電路連接
此次我們透過一個 LED 的亮滅來作為遠端的 IO 控制測試,此顆 LED 連線到 GPIO14 腳位。
>> 完整程式碼
# ESP32 as a tcp server for remote control # the details in https://jimirobot.tw import machine,network,socket,gc,utime from machine import Pin led=Pin(14,Pin.OUT) def go_wifi(): try: wifi.active(False) wifi.active(True) wifi.connect('tp_station','xxxxxxxx') print('start to connect wifi') for i in range(20): print('try to connect wifi in {}s'.format(i)) utime.sleep(1) if wifi.isconnected(): break if wifi.isconnected(): print('WiFi connection OK!') print('Network Config=',wifi.ifconfig()) else: print('WiFi connection Error') except Exception as e: print(e) gc.collect() wifi= network.WLAN(network.STA_IF) go_wifi() hostip = wifi.ifconfig()[0] tcp_server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_server.bind((hostip,2000)) #server ip and port tcp_server.listen(1) #listen only 1 connection rev_num=0 #calculate the message number while 1: print("tcp server is listening") tcp_conn,client_addr = tcp_server.accept() print(client_addr, "Client connects sucessfully") while 1: msg=tcp_conn.recv(128) if len(msg) > 0: msg=msg.decode() #convert bytes to strings if msg[0:4]=="led=": if(msg[4]=='1'): led.value(1) else: led.value(0) else: print('This comannd can not be recognized') rev_num +=1 tcp_conn.send('the server has received your msg {}'.format(rev_num)) else : print('client socket is disconnected') break
>> 程式解說
import machine,network,socket,gc,utime from machine import Pin led=Pin(14,Pin.OUT) def go_wifi(): try: wifi.active(False) wifi.active(True) wifi.connect('tp_station','xxxxxxxx') print('start to connect wifi') for i in range(20): print('try to connect wifi in {}s'.format(i)) utime.sleep(1) if wifi.isconnected(): break if wifi.isconnected(): print('WiFi connection OK!') print('Network Config=',wifi.ifconfig()) else: print('WiFi connection Error') except Exception as e: print(e)
首先匯入一些需要的模組,machine、socket、utime 等,設定 GPIO14 腳位為數位輸出。go_wifi() 函式內容為設定 wifi 連網的機制, 在程式第 9 行 wifi.connect() 內的兩個參數,分別為無線基地台的 ssid 與密碼;程式第 11 行開始利用 for 迴圈設計1個 20 秒的 timeout 機制,連線成功後,顯示相關網路資訊。
gc.collect() wifi= network.WLAN(network.STA_IF) go_wifi() hostip = wifi.ifconfig()[0] tcp_server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_server.bind((hostip,2000)) #server ip and port tcp_server.listen(1) #listen only 1 connection rev_num=0 #calculate the message number
這邊開始為主程式流程,首先執行 gc.collect() 手動將多餘的記憶體空間回收,在建立 wifi 的物件後,執行上網動作取得 IP,相關的網路參數會放在 wifi.ifconfig() 這個回傳值內,格式為 ( “本機IP”,”子網路遮罩”,”Gateway”,”DNS”),所以如果需要當下 ESP32 的 ip 位址,就可以利用 wifi.ifconfig()[0] 得知。
程式第 29 行開始就是我們上一節提到的 tcp 連線流程,各位關注在 tcp server 端即可,socket.socket 方法中的socket.AF_INET是 IPv4 協定(也就是一般常用的4個數字定義的 ip 版本,還有 socket.AF_INET6 則是 IPv6 協定),socket.SOCK_STREAM 是設定 TCP 通訊。
初始化 socket 後,tcp_server.bind((hostip,2000)) 設定主機的ip 與服務 port,這邊隨機選定 2000;tcp_server.listen(1) 限制tcp連線的數量僅能有1個。 最後 rev_num 此參數用來記憶收到簡訊的次數。
while 1: print("tcp server is listening") tcp_conn,client_addr = tcp_server.accept() print(client_addr, "Client connects sucessfully") while 1: msg=tcp_conn.recv(128) if len(msg) > 0: #check if tcp connection is alive msg=msg.decode() #convert bytes to strings if msg[0:4]=="led=": if(msg[4]=='1'): led.value(1) else: led.value(0) else: print('This comannd can not be recognized') rev_num +=1 tcp_conn.send('the server has received your msg {}'.format(rev_num)) else : print('client socket is disconnected') break
程式第 35 行主要的 while 迴圈在於 等待遠端 client 的連線用,在一次成功的 tcp 斷線後,便會重新起動等待再一次連線。當程式跑到第 37 行 tcp_server.accept() 時,將會啟動 Blocking Mode,也就是主執行緒會在此等待 client 端連上我們的 tcp server 。一旦 server 接收 client 連線請求,系統會針對此次連線建立一個新的 socket 物件(tcp_conn) 與 用戶端位址資訊(client_addr),我們也就可以針對此次連線進行後續操作。
程式第 39 行的次 while 迴圈功能為連線成功後的訊息傳遞循環,直到此次連線關閉。msg=tcp_conn.recv(128) 除將接收訊息放入msg變數內, 並指定接收訊息最大的 bufsize 為 128 bytes。還記得我們先前提到 tcp 傳送訊息都是用 bytes 型態傳送嗎?為了能夠順利解析訊息內容,msg=msg.decode() 將訊息的資料從 bytes 型態轉成 string 型態。
程式第 41 – 47 行用來快速判斷伺服器接收的資訊是否符合指定要求,這邊我們用很簡單的指令: led=1 代表 LED 點亮, led= x 其他數字就是熄滅 LED, 若格式不對,則回顯示指令無法辨識(not recognized)。而 server 端在每一次接收接收 client 端送出的訊息後,便透過 tcp_conn.send() 回傳 client 端,表示 server 端有收到訊息。
4. 測試遠端 IO 控制
現在我們的 ESP32 tcp server 已經架設完畢,該怎麼測試?如果各位使用者會電腦平台上的 python 語言,當然可以自己利用 socket api 再寫一個PC版的 tcp client,不過其實可以不用這麼麻煩啦!哥這邊推薦大家可以使用一套 SocketTest ,軟體作者為 Akshathkumar Shetty,執行的環境需安裝 JRE( java runtime environment),完整的安裝與執行過程如下:
- 先到 java 的官網下載 ( https://www.java.com/zh-TW/download/ ),下載 java 軟體,並且進行安裝。(如先前已安裝過就可以跳到步驟 3)
- 將下載下來的檔案,點擊進行安裝,直到安裝完成。
- 接著到作者的 github 網站 https://github.com/akshath/SocketTest,下載打包整個檔案:
- 解壓縮下載的檔案,找到 dist 目錄內的 socketTest.jar,按下滑鼠右鍵,選擇 java platform 執行。
- 此時可以開始測試我們ESP32 的 tcp server,軟體操作很直覺,選擇 client 的標籤頁,輸入 EPS32 TCP Server 的 IP 與服務 Port,按下 Connect 按鈕。
- 連線成功後在訊息欄輸入 led=1,順利的話就可以看到 ESP32 的控制板上的 LED 點亮,也可以看到 server 回傳的相關訊息;若訊息輸入 led=0,LED 則會熄滅。
5. 小結
想要將 ESP32 硬體做成一個可以遠端控制的模組,TCP Server 是最直覺的方法,建立 TCP 連線後,訊息解析與傳遞,就可以實現 Remote IO 的機制,但還是有些不便的地方,也就是在 Client 端軟體部分。此篇是透過現有的 socketTest 工具進行用戶端測試,應用到實際面向的話,程式設計者通常得開發特定的軟體給使用者操作(也就是自己寫介面,不管用 java 或 python 等等),這就得花費更多的時間與成本。有更好的方法可實現簡單的遠端控制嗎?有!就是把 ESP32 架設成 webserver,使用者僅需要瀏覽器,意味著手機或電腦都可進行操作,下一篇哥在來分享 ESP32 如何架設簡易 webserver 步驟與程式碼!
今天的內容就到這邊,如果各位有遇到什麼問題,也歡迎在下面留言或寫信與我討論囉~
↓↓↓↓↓↓賣場連結↓↓↓↓↓↓
歡迎大家有需要的話,可以多多支持一下我們的蝦皮賣場喔! 😀
吉米家官方店-創客機器人材料專賣 https://shopee.tw/jimirobot.tw
Follow JIMI哥 Twitter : https://twitter.com/jimirobot <–得到最新文章通知