《超圖解Python物聯網實作入門》第16-20頁提到,socket的方法都屬於阻斷式(block)敘述,以底下的程式為例,程式執行到”a”行就塞住了。本文將補充說明把socket設定成「非阻塞」的程式寫法。
首先以「動手作16-1:一對一通訊程式」的server.py檔為例,程式改寫的兩大重點:
- 把socket設成「非阻塞」模式
- 允許多人連線
如同書本第16-19頁說明,伺服器端socket物件將偵聽用戶端連線請求,這個socket相當於「總機」;接受(accept)用戶端連線後,伺服器將動態產生一個與該用戶通信的socket物件,此舉相當於「總機」把電話轉給某專人來服務客戶。
當有新的用戶端請求連線時,同樣由「總機」接線並轉交給新產生的socket物件處理。後面的程式將宣告一個列表變數來暫存動態產生的socket物件,方便透過迴圈處理全部連線用戶的通訊。
使用setblocking()方法取消socket的阻塞狀態
要取消socket預設的阻塞狀態,請執行socket物件的setblocking()方法(原意為set blocking,設定阻塞),並傳入False或0。改寫之後的server.py程式如下,新增引用time程式庫、宣告一個clients列表變數、透過setblocking()方法設定非阻塞,建立一個hello()自訂函式:
import socket import time clients = [] # 儲存用戶端socket物件的列表變數 HOST = 'localhost' PORT = 5438 s = socket.socket() s.bind((HOST, PORT)) s.setblocking(False) # 將此socket設成非阻塞 s.listen(5) print('{}伺服器在{}埠開通了!'.format(HOST, PORT)) def hello(): time.sleep(1) print('你好!')
若while迴圈程式不做任何修改:
while True: client, addr = s.accept() print('用戶端位址:{},埠號:{}'.format(addr[0], addr[1])) : 以下省略…
執行此server.py時,將出現底下的BlockingIOError(阻塞IO錯誤)訊息:
原本應該停在s.accept()那一行,等到有用戶端連線再自動繼續執行後面的敘述,因socket物件(s)被設定成以「非阻塞」方式執行,導致程式沒有停住。在沒有用戶端連線的情況下,強置執行accept(),就會出現如上圖的錯誤。
解決的方法是用try…except包裝可能會出錯的敘述。socket物件的recv(接收訊息)方法預設也會造成阻塞,當socket物件改成非阻塞之後,在尚未收到用戶端訊息時強置執行recv()將造成錯誤,因此也要用try…except包裝。修改後的while迴圈如下:
while True: try: client, addr = s.accept() print('用戶端位址:{},埠號:{}'.format(addr[0], addr[1])) # 也把跟用戶端連線的socket設成「非阻塞」 client.setblocking(False) # 將此用戶端socket物件存入clients列表備用 clients.append(client) except: pass # 不理會錯誤 # 逐一處理clients列表裡的每個用戶端socket… for client in clients: try: msg = client.recv(100).decode('utf8') print('收到訊息:' + msg) reply = '' if msg == '你好': reply = b'Hello!' elif msg == '再見': client.send(b'quit') client.close() # 將此用戶端socket從列表中移除 clients.remove(client) break # 退出for迴圈 else: reply = b'what??' client.send(reply) except: pass # 不理會錯誤 # 處理socket物件程式之餘,執行其他程式碼… hello()
執行此伺服器socket程式的結果如下,它將每隔一秒在終端機顯示“你好”,並且可以處理用戶端的連線請求。
非阻塞socket的MicroPython網站伺服器程式
底下是運用同樣手法改寫17-2頁的MicroPython網站伺服器程式:
import socket import time clients = [] s = socket.socket() HOST = '0.0.0.0' PORT = 80 httpHeader = b"""\ HTTP/1.0 200 OK Welcome to MicroPython! """ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((HOST, PORT)) s.setblocking(False) s.listen(5) print("Server running on port ", PORT) def hello(): time.sleep(1) print('hello!') while True: try: client, addr = s.accept() print("Client address:", addr) clients.append(client) except: pass for client in clients: try: req = client.recv(1024) print("Request:") print(req) client.send(httpHeader) client.close() clients.remove(client) print('-----------------------') except: pass hello()
把程式上傳到控制板之後執行,它將每隔1秒在終端機顯示“hello!”,並且可以處理用戶端(瀏覽器)的連線請求。