Python的非阻塞式(non-blocking)socket通訊程式(一)

超圖解Python物聯網實作入門》第16-20頁提到,socket的方法都屬於阻斷式(block)敘述,以底下的程式為例,程式執行到”a”行就塞住了。本文將補充說明把socket設定成「非阻塞」的程式寫法。

socket造成阻塞

首先以「動手作16-1:一對一通訊程式」的server.py檔為例,程式改寫的兩大重點:

  • 把socket設成「非阻塞」模式
  • 允許多人連線

如同書本第16-19頁說明,伺服器端socket物件將偵聽用戶端連線請求,這個socket相當於「總機」;接受(accept)用戶端連線後,伺服器將動態產生一個與該用戶通信的socket物件,此舉相當於「總機」把電話轉給某專人來服務客戶。

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錯誤)訊息:

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程式的結果如下,它將每隔一秒在終端機顯示“你好”,並且可以處理用戶端的連線請求。

server.py程式的執行畫面

非阻塞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!”,並且可以處理用戶端(瀏覽器)的連線請求。

Posts created 483

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top