雖然實作出來,點擊後可以讓樹莓派自行運作的程式
不過,實作的交握有點旁門左道
是靠php發射socket給背景程式去運作
怎麼想怎麼怪,雖然讓我硬幹出來了,但總想著要用正規一點的作法
這次在獲得好用的python websocket套件後
正式捲.土.重.來
把之前的程式改寫了一遍
雖然還稱不上完美
但交握確定是繞過了php,而直接python寫出的程式執行websocket交握了
至於會希望使用websocket來取代原本的寫法
最基本原因是這樣會讓javascript的寫法會更簡潔
現在主要瀏覽器的javascript引擎都支援websocket了
只要簡單地寫websocket = new WebSocket("ws://server_adress:port/");
然後靠websocket.send()就可以送出想要的命令
不用寫個ajax落落長才送得出命令
接著靠websocket.onmessage()就可以取得送回來的資料
好寫多了啊
之前是懶得架websocket伺服器
現在有簡單python的套件可用,那當然好好地來用一下啊
使用的套件是這個!
https://websockets.readthedocs.io/
檔案非常小巧,而且非常易學
小巧的原因是
它仰賴的套件只有python本身的asyncio
只是談到asyncio(異步處理套件)講起來又個問題所在,因為我不是很懂
異步處理的概念上我是有了,很簡單
以程式在跑時,除了在cpu內計算,通常還需要等外部IO的處理
例如硬碟/光碟機的讀取或網路通訊對方伺服器的反應
這些事都需要時間,在等待的這些時間可以跑去做其它事,就是異步處理的概念
不過概念有了,實作上還是處於混亂狀態
什麼時候可以加await,怎麼加怎麼跑還不是很清楚
雖然這樣講起來asyncio有點難,但websockets套件使用上沒那麼複雜
因為大概部分的應用都脫離不了官方提供的範例
跟著範例跑過一輪,基本就大概掌握好了
官方範例規劃如下
https://websockets.readthedocs.io/en/stable/intro.html
稍微翻譯大綱說明
首先是基本的echo server與client
再來是加上SSL通訊的設定法
接著是把client換成網頁
然後是比較複雜的同步通訊範例
最後是講述共通樣板(Common Patterns)
像是消費者、生產者、兩者共用與註冊
共通樣板的部分好像比較難懂,沒關係,後面會說明
先從第一個範例開始
第一個範例嘛
其實我只需要server的部分,client是靠網頁
所以就直接講server的基本寫法吧
#導入需要的函式庫 import asyncio import websockets #Step1 建立主要的server函數 async def hello(websocket, path): #Step2 在使用websocket的接收 name = await websocket.recv() print(f"< {name}") greeting = f"Hello {name}!" #與傳送功能掛上await await websocket.send(greeting) print(f"> {greeting}") #Step3 server的主函數hello,放入websocket伺服中 # 並設定ip與port start_server = websockets.serve(hello, "localhost", 8765) #Step4 將websocket服務丟入異步迴圈中啟動 asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever() #並設定為永遠啟動第二範例ssl加入
那要使用ssl的話也很簡單
我不使用pathlib載入檔案
簡化一下流程與列出兩個檔案的建立法
import ssl #設定sll通訊為TLS Server ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) #載入fullchain檔案 ssl_context.load_verify_locations("/ssl_file_path/fullchain.pem") #如果是各別兩個檔案,就這樣載入,前面放憑證,後面放key ssl_context.load_verify_locations("/ssl_file_path/server.crt", "/ssl_file_path/server.key") #建立websocket伺服時,設定好ssl的部分 start_server = websockets.serve( hello, "localhost", 8765, ssl=ssl_context )
第三、第四個範圍我打算跟後面的共通樣板一起講
簡單地說,第三個範別是所謂的生產者模式(Producer)
async def producer_handler(websocket, path): while True: message = await producer() await websocket.send(message)一個不斷生產出訊息給客戶端的樣式
這個範例就是不斷地發送時間給瀏覽器顯示
是個很簡單的生產者的模式
async def time(websocket, path): while True: now = datetime.datetime.utcnow().isoformat() + "Z" await websocket.send(now) await asyncio.sleep(random.random() * 3)
第四個範例之所以複雜就是夾雜了兩種共通樣板
分別是消費者(Consumer)與註冊(Registration)
消費者很單純就是接收瀏覽器發送後的訊息再處理
async def consumer_handler(websocket, path): async for message in websocket: await consumer(message)而註冊則是利用一組set記錄連線進來的資料
然後在離開時刪除其資訊
connected = set() async def handler(websocket, path): # Register. connected.add(websocket) try: # Implement logic here. await asyncio.wait([ws.send("Hello!") for ws in connected]) await asyncio.sleep(10) finally: # Unregister. connected.remove(websocket)這兩組結合後,就是第四範例
可以得知連線人數的加減法控制器
連上後,不但可以知道目前人數(註冊的功能)
還可以加減目前的數字(生產者的功能)
當然,是全部的連線者都看得到同一組數字
參考這範例可以寫出一個簡單但十足夠用的的聊天室了
如果希望不但可以從伺服器主動接受到訊息(生產者)
還可以發送訊息讓伺服器回應的機制(消費者)
那就是共通樣板的兩者共用(BOTH)
async def handler(websocket, path): consumer_task = asyncio.ensure_future( consumer_handler(websocket, path)) producer_task = asyncio.ensure_future( producer_handler(websocket, path)) done, pending = await asyncio.wait( [consumer_task, producer_task], return_when=asyncio.FIRST_COMPLETED, ) for task in pending: task.cancel()在函數裡創建兩個任務(task),然後混合在一起
再把這個函數交給websockets進行伺服器的建立就完成了
看,這樣很簡單吧
看到這裡眼睛都亮起來了
有這個範例的話,再加第三任務就輕鬆很多了...應該啦
雖然我一開始是這麼想的,不過這關一卡就是卡了兩個禮拜多
因為不管怎麼加入新的任務,都有一定的機率在關掉網頁後就停止不動了
甚至到後來就是網頁一關,第三任務就停了
後來一步一步加了log去追蹤才發現,它在for task in pending那邊被取消了
這真的是很漂亮的寫法,這樣就可以當斷線時,保留資源給其它連線者
只是我需要離線後還能運作的功能,那就只跳脫這裡
有方向後就簡單一點,很明確地要跟start_server,或說跟websocekts.serv裡的handler切開
我這邊創建一個內藏無限迴圈的函數當自己想要外加的服務機制
然後它的實際運作綁在start server的後面
async def mission_handler(mission_list): while True: #這邊要補上sleep,避免真的卡住無限迴圈 await asyncio.sleep(1) if len(mission_list)>0: await do_mission(mission_list) start_server = websockets.serve(handler, "localhost", 6789) asyncio.get_event_loop().run_until_complete(start_server) #新的handler自己跑run_until_complete asyncio.get_event_loop().run_until_complete(mision_handler(mission_list)) asyncio.get_event_loop().run_forever()寫到這邊或許有人會有疑問
mission_handler裡為什麼要補上asyncio的sleep
其實也很簡單,因為假如mission_list是空的(== 0),那就會一直在這裡循環,無法異步跳到其它需要處理的程序
所以當沒事做時,要跳到其它需要的地方,需要加個await asyncio.sleep(1)來處理
那個加個1秒的暫停,先離開這裡是必要的處置
最後就是上網找run_until_complete的用法
把第三個任務加入整個異步的任務中了,就可以用了
那這樣的狀態下控制樹莓派的話
真的就簡單啦
例如要亮燈,就用消費者模式配合裡用RPI.GPIO函式庫去控制IO
也可以用生產者模式持續讀取目前IO狀況回傳給網頁
就算這網頁是離線的(直接跑電腦裡寫的)也行,畢竟實際運作是靠websocket傳輸
而想要像我一樣,有個背景服務在這裡面,可以給予任務後就不予理會的話
那就像最後我的範例一樣再加個新任務進去執行就好
可以用websocket通訊加上異步處理後,這個項目應該就算正式完成
繼續學習其它東西啦
沒有留言:
張貼留言