【Python】シングルスレッドで複数通信を処理!selectを使ったイベント駆動プログラミングの基本

この記事は約8分で読めます。

組み込みシステムやネットワークアプリを開発していると、「データを受信した時だけ処理を行う(イベント駆動)」という仕組みを作りたい場面がよくあります。

しかし、Pythonの標準的な通信用API(例えばソケットのrecv()関数など)は「同期関数」と呼ばれ、データが来るまでそこでプログラムの動きが完全にストップ(ブロック)してしまいます。これでは、1つのスレッドで複数の通信やイベントを同時に待ち受けることが難しくなってしまいます。

この問題を解決し、複数の入出力をスマートに監視できるのが、Pythonの標準ライブラリであるselectモジュールです。

この記事では、select文を使ったPythonでのイベント駆動プログラミングの基本と実装方法を解説します。

selectモジュールとは?

LinuxなどのOSには、複数のI/O(入力・出力)を一度に監視できるselectというシステムコールがあります。Pythonのselectモジュールは、これと同じ機能をPython上で使えるようにしたものです。

「複数のソケット(通信の窓口)を監視し、どれか一つでもデータを受信したら、そのソケットを教えてくれる」という動きをするため、イベント駆動型のプログラムを作るのに非常に役立ちます。

実践!selectを使ったサーバー&クライアント通信

今回は「サーバー用スレッド」と「クライアント用スレッド」を立ち上げ、タイマーをきっかけにしてお互いにデータを送受信し合うサンプルコードを作成しました。

サンプルコード全体像

import time
import threading
import socket
import select

PORT_NUM = 12345
stop_application = False

def create_server(ip, port):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((ip, port))
    server.listen()
    return server

def create_client(ip, port):
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((ip, port))
    return client

# クライアント側から通信のきっかけを作るタイマー処理
def tm_callback(con):
    if not stop_application:
        snd = bytearray([5, 6, 7, 8])
        con.send(snd)

# サーバー側スレッド処理
def th1_func():
    global stop_application
    server = create_server('127.0.0.1', PORT_NUM)
    (con, client) = server.accept()
    
    # 監視対象のリストを作成(今回は接続されたソケットのみ)
    fd_set = set()
    fd_set.add(con)
    
    while True:
        try:
            if stop_application:
                con.close()
                break
            
            # select関数でデータの受信を監視
            r, w, x = select.select(list(fd_set), [], [], 10)
            
            for fd in r:
                data = fd.recv(1024)
                print(f"Server received: {data}")
                time.sleep(0.1)
                snd = bytearray([1, 2, 3, 4])
                fd.send(snd)  # 応答を返す
                
        except Exception:
            con.close()
            break

# クライアント側スレッド処理
def th2_func():
    global stop_application
    con = create_client('127.0.0.1', PORT_NUM)
    
    fd_set = set()
    fd_set.add(con)
    
    # 1秒後に通信のきっかけを送信
    tm = threading.Timer(1, tm_callback, args=[con,])
    tm.start()
    
    while True:
        try:
            if stop_application:
                con.close()
                break
                
            r, w, x = select.select(list(fd_set), [], [], 10)
            
            for fd in r:
                data = fd.recv(1024)
                print(f"Client received: {data}")
                time.sleep(0.1)
                snd = bytearray([9, 10, 11, 12])
                fd.send(snd)  # 応答を返す
                
        except Exception:
            con.close()
            break

# メイン処理
def main_func():
    global stop_application
    
    # サーバーとクライアントのスレッドを起動
    thread1 = threading.Thread(target=th1_func)
    thread1.start()

    thread2 = threading.Thread(target=th2_func)
    thread2.start()
    
    while True:
        try:
            time.sleep(1)
        except KeyboardInterrupt:
            # Ctrl+Cで安全に停止
            print("stop")
            stop_application = True
            thread1.join()
            thread2.join()
            break

if __name__ == '__main__':
    main_func()

💡 プログラムを理解するための関連記事 今回のコードでは「マルチスレッド」「Socket通信」「タイマー処理」を組み合わせて使用しています。それぞれの基本については、以下の過去記事で詳しく解説していますので、あわせて確認してみてください。

selectの使い方とコードの解説

コード内のselectに関する重要な部分を抜き出して解説します。

1. ライブラリのインポートと監視リストの作成

import select

fd_set = set()
fd_set.add(con) # 監視したいソケットを追加

selectは標準ライブラリなので、インストール不要でそのままインポートできます。次に、監視したいソケット(コネクション)をリストやセット(集合)に追加します。複数の入力を待ちたい場合は、ここでadd()を複数回行います。

2. select関数の呼び出し

一番の要となるのが以下の関数です。ここでは同期関数として動くため、イベントとして処理したいものはすべてここに登録します。

r, w, x = select.select(list(fd_set), [], [], 10)

引数の意味はそれぞれ以下の通りです。

  • 第1引数(r):データを受信した(読み込み可能になった)かを監視するリスト
  • 第2引数(w):データを送信できる(書き込み可能になった)かを監視するリスト
  • 第3引数(x):エラー(例外)が発生したかを監視するリスト
  • 第4引数:タイムアウト時間(秒)

今回は「データを受信したか」だけを知りたいため、第1引数に監視リストを渡し、第2・第3引数は空のリスト[]を指定しています。

3. イベント発生後の処理

for fd in r:
    data = fd.recv(1024) 
    fd.send(snd)

select.select()は、イベントが発生する(またはタイムアウトする)まで処理をブロックして待機します。 データを受信すると、戻り値のrの中に「データが届いたソケット」のリストが入って返ってきます。あとはそれをfor文で回し、recv()やsend()を実行すればOKです。

実行結果

上記のプログラムを実行すると、それぞれが正しくデータを受信・送信し合うログが出力されます。

  1. タイマーのコールバックから送信:b'\x05\x06\x07\x08'
  2. サーバー(Thread1)が受信をトリガーにして送信:b'\x01\x02\x03\x04'
  3. クライアント(Thread2)が受信をトリガーにして送信:b'\t\n\x0b\x0c'

このように、お互いの受信をきっかけ(イベント)にして処理を連鎖させることができます。

まとめ

Pythonで複数のソケット通信やイベント入力を1つのスレッドで効率よく処理したい場合、標準ライブラリのselectモジュールが非常に役立ちます。

  • 監視したいソケットをリストにまとめる
  • select.select()の引数に渡して待機する
  • 反応があったソケットだけを対象に処理を行う

使いこなすまでに少し手順が必要ですが、複雑な通信アプリやイベント駆動型のプログラムを作る際には必須となる知識です。ぜひ使い方を覚えて、ご自身のプロジェクトに取り入れてみてください。

コメント

タイトルとURLをコピーしました