【Python/ソケット通信】select関数での複数ソケットでの同時受信待ちの実現

このページにはプロモーションが含まれています

このページでは、Python における select 関数の使い方について説明していきます。

特に、この select 関数を、1つのプログラムで “複数のソケットでの同時受信待ち” を実現することを目的で利用する際の使い方を説明していきます。

select 関数

この select 関数の “役割” や select 関数を使って “実現できること” については下記ページの select 関数 の章で解説を行っていますので、お手数をおかけしますが、これらについて知りたい方はまずは下記ページを読んでみていただければと思います。C言語向けの解説にはなりますが、select 関数自体の説明については Python でも共通の内容になります。

【C言語】select関数の使い方(複数ソケットの監視)

簡単に言えば、select を利用することで下記を実現することができます。FD とはファイルディスクリプターの略になります。

  • 複数の FD の監視
    • (読み込み・書き込み・例外発生の監視)
  • タイムアウト

ただ、select 関数で実現できることはC言語と Python とで共通ではあるものの、select 関数の使い方に関してはC言語と Python とで異なるため、ここからは Python での select 関数の使い方について解説していきます。

select 関数の使い方

では、Python での select 関数の使い方について解説していきます。

スポンサーリンク

select モジュールを import する

select 関数は Python の標準モジュールである select モジュールから提供される関数になります。そのため、select 関数を利用するためには select モジュールを事前に import しておく必要があります。

selectのimport
import select

上記のように select モジュールを import すれば select.select によって select 関数が利用できます。

select 関数の引数

続いて、select 関数の引数について説明します。select 関数の引数は下記の4つとなります。

Python におけるソケット通信で select 関数を利用する場合、socket クラスのインスタンスが FD として扱われます。したがって、Python におけるソケット通信で select 関数を利用する場合に限定すれば、下記における “FD を要素とするリスト” とは、socket.socket の返却値、すなわち “ソケットを要素とするリスト” と読み替えて良いです。本当は、正確にいうと socket クラスのインスタンスは FD ではなく、FD をデータ属性として持つオブジェクトではあるのですが、このページでは簡単のため socket クラスのインスタンスを FD として説明していきます。 

引数 説明
第1引数 読み込みを監視したい FD を要素とするリスト
第2引数 書き込みを監視したい FD を要素とするリスト
第3引数 例外発生を監視したい FD を要素とするリスト
第4引数 タイムアウト時間(浮動小数点数)

また、上記ではリストと限定していますし、今後もリストに限定して説明していきますが、正確にはタプルや集合等を含む “イテラブルなオブジェクト” であれば 第1引数第3引数 に指定可能です。

第1引数第3引数 には監視対象の FD のリストを指定する

select は複数の FD (ファイルディスクリプタ) を監視する関数で、実行すると基本的に待ち状態になります。

そして、第1引数 に指定したリストに含まれるいずれかの FD が読み込みに対して Ready になった (つまり読み込み可能になった) or 第2引数 に指定したリストに含まれるいずれかの FD が書き込みに対して Ready になった (つまり書き込み可能になった) or 第3引数 に指定したリストに含まれるいずれかの FD が例外に対して Ready になった (つまり例外が発生した) タイミングで待ちが解除されて関数が終了します。

例えば、ソケット通信の場合であれば、第1引数 に指定したリストに含まれるいずれかのソケットに対してデータが送信されてきたタイミングで、そのソケットは受信可能状態(読み込みに対して Ready)となるため select 関数が終了することになります。

ソケットが読み込みに対してReadyになる様子

こんな感じで select 関数を利用することで、各種 FD が何らかのイベントが発生して Ready になるまで待ち続けるような、ちょっとしたイベント駆動型のプログラムが実現することができます。

select関数の利用によってイベント駆動型のプログラムが作成可能であることを示す図

もちろん、recv 関数等もデータ受信時に関数が終了するようになっているので、これを利用することでイベント駆動型のプログラムを実現することは可能です。ただし、recv 関数では、監視対象となるイベントは読み込みの1つのみで、さらに監視対象となる FD も1つのみです。それに対し、select 関数では3種類のイベントが監視可能で、さらにリストに含まれる複数の FD を同時に監視することが可能です。そのため、select 関数の方が汎用的かつ複数のイベントの監視に向いています。

事前に FD のリストを作成しておく必要がある

このように select 関数で複数の FD を同時に監視することが実現可能なのは、第1引数第3引数 にリストが指定可能であるからになります。前述で示した表のように、第1引数第3引数 には、各イベントに対しての監視対象となる FD のリストを指定する必要があります(監視するイベントに応じて、指定する引数の位置が異なるので注意してください)。

そのため、select 関数を実行する前には、この監視対象となる FD のリストを事前に作成しておく必要があります。この FD は、前述のとおりソケット通信の場合は socket クラスのインスタンスとなりますので、socket クラスのインスタンスを作成しておき、それを事前にリストに追加しておく必要があります。

さらに、TCP 通信の場合は、サーバーが accept を実行する前に select 関数を実行し、これによってソケットが “接続要求の受付待ち” に対して Ready になった (接続要求が読み込み可能になった) かどうかを監視することが多いです。この場合、accept が実行可能な状態のソケットをリストに追加する必要があります。すなわち、リストに追加する socket クラスのインスタンスは bindlisten を実行しておく必要があります。

TCPでselect関数を利用する際に用意するリストに追加するソケットの説明図

また、UDP 通信の場合は、サーバーが最初の recv を実行する前に select 関数を実行し、これによってソケットが “データの受信待ち” に対して Ready になった (データが読み込み可能になった) かどうかを監視することが多いです。この場合、recv が実行可能な状態のソケットをリストに追加する必要があります。すなわち、リストに追加する socket クラスのインスタンスは bind を実行しておく必要があります(listen は不要)。

UDPでselect関数を利用する際に用意するリストに追加するソケットの説明図

上記のような TCP や UDP の通信の流れについて再確認したい場合は、下記ページでまとめていますので、下記ページを参照していただければと思います。

Pythonでのソケット通信(ポート番号・プロトコル・サーバー / クライアント)

また、基本的には bind で関連付けるポート番号は、それぞれのソケットで異なるものにしておく必要があります。

もし、特定のイベントに対しての監視が不要なのであれば、それに対応する引数には空のリスト [] を指定してやれば良いです。例えば、select 関数で書き込みや例外に関しての監視を行わないのであれば、第2引数 や 第3引数 には [] を指定してやれば良いことになります。

第4引数 でタイムアウトの設定を行う

また、select 関数で待ち続ける時間の上限は 第4引数 で調整することが可能です。要は、第4引数 の指定によって select 関数にタイムアウトを設けることができます。

第4引数 には浮動小数点数 or None を指定します。None を指定した場合はタイムアウト設定なしとなり、Ready となる FD がなければ永遠に待ち続けることになります。それに対し、浮動小数点数を指定すれば、その指定した値の秒数だけ経過しても Ready となる FD が存在しなければ select 関数が終了することになります。

select関数の第4引数の意味合いを示す図

引数の指定の例

例えば、select 関数で、socks というリストに含まれる FD を読み込みに対して監視したい場合は、下記のように select 関数を実行することになります(返却値に関しては後述で解説します)。リスト socks に含まれる FD が 15.7 秒経っても読み込みに対して Ready にならなかった場合は、タイムアウトによって select 関数が終了することになります。

引数の指定
import select

r_socks, _, _ = select.select(socks, [], [], 15.7)

select 関数の返却値

続いて select 関数の返却値について説明していきます。

返却値の意味合い

select 関数は下記の3つのリストを返却します。各種イベントに対して Ready になった FD が存在しない場合は、その返却値は空のリスト [] となります。もし、select 関数が正常に終了したのにもかかわらず全ての返却値が [] なのであれば、タイムアウトが発生したと判断することができます。

引数名 説明
返却値1 読み込みに対してReady となった FD を要素とするリスト
返却値2 書き込みに対してReady となった FD を要素とするリスト
返却値3 例外に対してReady となった FD を要素とするリスト

例えば、select 関数の 第1引数 に対して socket1socket2socket3 の3つのソケットを要素とするリストを指定し、さらに select 関数実行後に socket2 のみが読み込みに対して Ready になった場合、socket2 のみを要素とするリストが 返却値1 として返却されることになります。

select関数の返却値1の意味合いを示す図

返却値の利用手順

この3つの返却値より Ready となった FD を確認することが可能となります。具体的には、各返却値のリストに含まれる要素が Ready となった FD となります。

そして、返却値1 に含まれる FD に対して読み込み処理、例えばソケットの場合は recvaccept 等を実行すれば、その FD は既に読み込みに対して Ready であるため、即座に読み込みが行われることになります。同様に、返却値2 に含まれる FD に対しては即座に書き込み処理が行えることになります。

なので、select 関数が正常に終了した際には、それらの Ready になった FD に対して実際の処理(読み込みや書き込み等)を行うことになります。あくまでも、select は FD が Ready になったかどうかを監視するだけの関数なので、実際の FD に対する処理は select 関数終了後に別途実行する必要があります。

ポイントは、一度に Ready となる FD は1つのみとは限らないという点になります。まぁ、リストが返却されるようになっている時点で察してくれているかもしれませんが、同時に複数の FD が Ready となる場合があります。なので、Ready になった FD すべてに対して処理を行いたいような場合は、返却されたリストに対してループを行うような処理が必要となります。

返却値に含まれる要素全てに対して処理を行う必要があることを示す図

また、前述の通り、もし select 関数が正常に終了したのにもかかわらず全ての返却値が [] なのであれば、タイムアウトが発生したと判断することができます。

ということで、select 関数実行後の基本的な処理の流れは次のようになります。まず実行すべき処理はタイムアウト発生の有無の確認で、3つの返却値のリストが全て空であるか場合はタイムアウトが発生したと判断してタイムアウト時の処理を行います。タイムアウトが発生していない場合は、3つの返却値のリストに含まれる FD それぞれに対して Ready になった処理(読み込みや書き込み等)を実行することになります。

下記が、その処理の流れをスクリプトとして記述したものになります。例外発生時のことを考慮するともうちょい複雑になりますが、今回は省略しています。

返却値の利用例
r_socks, w_socks, x_socks = select.select(略)

if len(r_socks) == 0 and len(w_socks) == 0 and len(x_socks) == 0:
    # 返却値が全て空ならタイムアウトが発生
    
    # タイムアウト発生時の処理

for sock in r_socks:
    # sockに対する処理

for sock in w_socks:
    # sockに対する処理

for sock in x_socks:
    # sockに対する処理

ただ、必ず3つの返却値のリストに対して “空であるかどうかの判断” や “ループを処理” を実施する必要があるというわけではありません。select 関数の 第1引数第3引数 に空のリスト [] を指定した場合は、その引数の位置に対応する返却値は必ず空のリスト [] となります。もう少し具体的に言えば、第 n 引数[] と指定した場合、返却値 n は必ず [] となります。

したがって、その返却値は [] となることを前提にスクリプトを記述しても問題ありません。例えば、第1引数 のみに対して空でないリストを指定する場合、つまり読み込みのイベントに対してのみ監視を行う場合は、下記のようなスクリプトでも十分タイムアウトの発生の有無や Ready になった FD に対する読み込み処理が漏れなく行えることになります。

特定の返却値のみに対する処理
r_socks, _, _ = select.select(socks, [], [], None)

if len(r_socks) == 0:
    # 返却値が全て空ならタイムアウトが発生
    
    # タイムアウト発生時の処理

for sock in r_socks:
    # sockに対する読み込み処理

スポンサーリンク

select 関数の利用例

最後に、select 関数で複数のソケットに対する受信待ちを行う例を示していきたいと思います。ここでは、計4つのスクリプトを紹介していきます。最初の2つが TCP 通信を行うサーバー・クライアントの例で、これらではサーバーで select 関数を用いて複数のソケットでの接続要求の受信待ちを行う例を示していきます。

残りの2つが UDP 通信を行うサーバー・クライアントの例で、これらではサーバーで select 関数を用いて複数のソケットでのデータの受信待ちを行う例を示していきます。

TCP 通信を行うスクリプトではデータの送受信を行う前の接続要求の受信待ちに対して、UDP 通信を行うスクリプトでは最初のデータの受信に対して select 関数を用いて複数のソケットでの待ちを実現しますので、いずれの例に関しても select 関数を用いるのはサーバー側のスクリプトのみとなります。クライアントは select 関数も利用しない普通のスクリプトになりますが、サーバーに対応するスクリプトの例として参考に載せておきます。

TCP 通信での select 関数の利用例

では、TCP 通信で複数のソケットに対する接続要求の受信待ちを行うスクリプトの例を紹介していきます。TCP を利用するソケット通信プログラムにおいて、接続要求の受信待ちを行うのは accept メソッドになりますので、accept メソッドの前で select 関数を利用することになります。

TCP 通信のサーバー

TCP 通信で複数のソケットに対して接続要求の受付待ちを行うサーバーのスクリプトの例は下記になります。

server.py
import socket
import select

# 受信がReadyになったかどうかを監視するFDのリスト
socks = []

# ポート40001から40010に対して受信待ちを行うソケットを生成
for i in range(10):
    # ソケットの作成
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 各ソケットで異なるポートをbind
    sock.bind(('0.0.0.0', 40001 + i))

    # ソケットをリッスン状態に設定
    sock.listen()

    # bindとlistenを行ったソケットをリストに追加
    socks.append(sock)

while True:

    # 事前に作成したリストを第1引数に指定
    # 書き込みや例外は監視しないので第2引数と第3引数には[]を指定
    # タイムアウトは60秒に設定するため第4引数には60を指定
    r_socks, _, _ = select.select(socks, [], [], 60)

    if len(r_socks) == 0:
        # 返却値が全て空ならタイムアウトが発生
        # (今回リストに要素が存在する可能性があるのは1つ目の返却値のみ)

        print('time out!')
        break

    for sock in r_socks:
        # リスト内の全ソケットに対してacceptとデータの送受信を実施

        # 読み込みに対してReady状態なのでacceptはすぐに完了
        e_sock, addr = sock.accept()

        # 後はいつも通りデータの送受信を行えば良い
        data = e_sock.recv(1024)
        print(data)

        e_sock.send(b'OK')
        e_sock.close()
    
# ソケットをクローズ
for sock in socks:
    sock.close()

上記スクリプトでは、まずソケットを 10 個作成し、それを socks というリストに追加していく処理を行っています。それぞれのソケットは 4000140010 のポートで bind し(IP アドレスは 0.0.0.0 固定)、さらに listen によってリッスン状態にした上でリストに追加していっています。

そして、その後に select 関数を実行しています。この select 関数の 第1引数 には事前に用意したリスト socks を指定しているため、4000140010 のポートで bind された計 10 個のソケットで同時に受信待ちが行われることになります。そして、クライアントから、これらのいずれかのソケットに bind された IP アドレス&ポート宛の接続要求を通信端末が受信すると、そのソケットが読み込みに対して Ready 状態となり、select 関数が終了することになります。

この場合、返却値1 のリストには Ready 状態となったソケットが全て含まれているため、返却値1 のリストに含まれる全ソケットに対して accept を実行して実際の接続要求の受信および接続の確立を行い、あとは accept で生成されたソケットを使っていつも通り TCP でのデータの送受信を行うようにしています。

ただし、select 関数の 第4引数 を指定しているため、1つもソケットが Ready になっていなくても、この select 関数の実行はタイムアウトで終了する場合があります。タイムアウトが発生した場合は、返却値が [] となってリスト長は 0 となるため、タイムアウト発生時に特別な処理を行いたい場合は、上記のように返却値のリストの長さを調べ、0 の場合にその処理を行うように記述する必要があります。

TCP 通信のクライアント

TCP 通信のサーバー で示したスクリプトの通信相手となるクライアントは下記のようなスクリプトで実現できます。

client.py
import socket

port = input('ポート番号を40001から40010で指定してください:')

# ソケットの生成
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)


# 入力されたポート宛に接続要求を送信
sock.connect(('127.0.0.1', int(port)))

# データの送信
data_str = 'ポート:' + port + 'に対して送信'
data = data_str.encode()
sock.send(data)

# データの受信
data = sock.recv(1024)
str_data = data.decode()

print(str_data)
sock.close()

基本的にクライアント側はサーバーが select を実行しているかどうかは意識せずに作成することができます。そのため、クライアントに関してはいつも通りの処理の流れで実装してやれば良いです。ただ、サーバーが複数のポート(ソケット)での受信待ちを行っているため、様々なポートに対してデータの送信ができるよう、上記のスクリプトでは最初に送信先のポートをユーザーが指定できるようになっています。

TCP 通信プログラムの実行手順

TCP 通信のサーバーTCP 通信のクライアント とで示したスクリプトの実行方法を紹介しておきます。

まず、TCP 通信のサーバー で示したスクリプトを server.py という名前、さらに TCP 通信のクライアント で示したスクリプトを client.py という名前で適当な同じフォルダに保存してください。そして、ターミナルアプリ(Windows の場合はコマンドプロンプトや PowerShell 等)を2つ起動し、cd コマンドを実行して両方のターミナルでスクリプトを保存したフォルダーに移動してください。

続いて、一方側のターミナルで下記のように server.py を python コマンドで実行してください。このターミナルでの操作は以上で終了です。これによりサーバーが立ち上がり、4000140010 のポートでの接続要求待ちが行われることになります。

python server.py

次に、他方側のターミナルで下記のように client.pypython コマンドで実行してください。

python client.py

すると、データの送信先となるポート番号の入力が促されますので、まずは 4000140010 の整数を入力してエンターキーを押してください。

ポート番号を40001から40010で指定してください:

これにより、入力したポート番号のポートにクライアントからサーバーへ接続要求が送信されることになります。そして、接続要求が上手く送信できれば、OK が出力されることが確認できると思います。

ポート番号を40001から40010で指定してください:40005
OK

例えば、上記ではポート 40005 に対してクライアントからサーバーに対して接続要求が送信されることになります。サーバーは、ポート 40005bind したソケットが受信に対して Ready になったかどうかを select 関数で監視していますので、クライアントからの接続要求の送信によってサーバーの select 関数が終了することになります。そして、サーバーは、Ready になったソケットに対して accept で接続要求の受付・接続の確立を実施し、その後にクライアントからデータを受信した後に b'OK' をクライアントに返却するようになっています。さらに、その b'OK' をクライアントが受信し、文字列として出力しているため、上記のようにターミナルに OK が出力されることになります。

server.pyとclient.pyとの間で行われる通信をシーケンス化した図

つまり、上記のように OK が出力されるということは、サーバーが select 関数によって監視しているソケットから接続要求が受信されて接続が確立され、その上でサーバーとクライアント間でデータの送受信が正常に行えていることを意味します。

同様に、再度 client.pypython で実行し、ポート番号として先ほどとは異なる 4000140010 の整数を入力してみても同様の動作になることが確認できると思います。このように、複数のポートで接続要求の受信が行えるのは select 関数で、各ポートに bind した複数のソケットを監視しているからになります。select 関数を使わずに同様の動作を実現しようと思うと結構苦労すると思うので、それを試してみるのも良いと思います。

また、サーバーを起動した状態でクライアントの操作を行わずに 60 秒ほど放置していると、サーバーでタイムアウトが発生してサーバープログラムが終了することも確認できると思います。これも select 関数によって実現している動作となります。タイムアウト自体はソケットのオプション設定等で実現することも出来るのですが、select 関数によっても実現可能であることも覚えておきましょう!

ちなみに、サーバーは無限ループで常駐させるようにしているため、終了させたい場合は、server.py を実行した方のターミナルで control + c を入力してプログラムを強制終了させてください。

UDP 通信での select 関数の利用例

続いて、UDP 通信で複数のソケットに対するデータの受信待ちを行うスクリプトの例を紹介していきます。TCP の場合と違い、UDP の場合は接続の確立が不要であるため listenaccept の実行は不要で、最初にデータの受信を行う recv の前で select 関数を利用することになります。

UDP 通信のサーバー

TCP 通信で複数のソケットに対してデータの受信待ちを行うサーバーのスクリプトの例は下記になります。

server.py
import socket
import select

# 受信がReadyになったかどうかを監視するFDのリスト
socks = []

# ポート40001から40010に対して受信待ちを行うソケットを生成
for i in range(10):
    # ソケットの作成
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    # 各ソケットで異なるポートをbind
    sock.bind(('0.0.0.0', 40001 + i))

    # bindとlistenを行ったソケットをリストに追加
    socks.append(sock)

while True:

    # 事前に作成したリストを第1引数に指定
    # 書き込みや例外は監視しないので第2引数と第3引数には[]を指定
    # タイムアウトは60秒に設定するため第4引数には60を指定
    r_socks, _, _ = select.select(socks, [], [], 60)

    if len(r_socks) == 0:
        # 返却値が全て空ならタイムアウトが発生
        # (今回リストに要素が存在する可能性があるのは1つ目の返却値のみ)

        print('time out!')
        break

    for sock in r_socks:
        # リスト内の全ソケットに対してデータの送受信を実施
        # データの送受信自体はいつも通りに行えば良い

        # 読み込みに対してReady状態なのでrecvはすぐに完了
        data, addr = sock.recvfrom(1024)
        print(data)

        sock.sendto(b'OK', addr)
        
# ソケットをクローズ
for sock in socks:
    sock.close()

TCP 通信のサーバー のスクリプトと見比べ見ていただければ TCP と UDP とでのスクリプトの作り方の違いが理解していただけるのではないかと思います。select 関数の引数の指定や返却値の扱いは TCP の場合と UDP の場合とで大差はないですが、UDP の場合は、select 関数実行後には accept ではなく recvfrom を実行することになります。

また、リストに追加するソケット生成時には、socket.socket の第2引数に socket.SOCK_DGRAM を指定する必要があるので注意してください。

UDP 通信のクライアント

UDP 通信のサーバー で示したスクリプトの通信相手となるクライアントは下記のようなスクリプトで実現できます。

client.py
import socket

port = input('ポート番号を40001から40010で指定してください:')

# ソケットの生成
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# データの送信
data_str = 'ポート:' + port + 'に対して送信'
data = data_str.encode()
sock.sendto(data, ('127.0.0.1', int(port)))

# データの受信
data = sock.recv(1024)
str_data = data.decode()

print(str_data)
sock.close()

TCP 通信のクライアント でも説明したように、クライアント側ではサーバーが select を利用して複数ソケットからの受信待ちを同時に行っているかどうかを意識せずに作成可能で、上記のスクリプトも何の変哲もない UDP 通信におけるクライアントのスクリプトとなっています。

UDP 通信プログラムの実行手順

スクリプトの中身や TCP or UDP でプロトコルは異なるものの、実行手順や動作に関しては TCP 通信プログラムの実行手順 と同様であるため手順の説明は省略します。是非、実際に試してみて、サーバーが同時に複数のソケットでの受信待ちを実現できていることを確認してみていただければと思います!

スポンサーリンク

まとめ

このページでは、Python における select 関数の使い方について説明しました!

select 関数を利用することで、複数のソケットでの受信待ちを行う通信プログラムを簡単に実現することができます!特に、複数のポートからのデータの受信待ちを行う場合は、複数のソケットで受信待ちを行う必要があるため、このページで説明した内容が活用できると思います!

select 関数を利用することで、より高機能な通信プログラムを開発していくことができるようになりますので、select 関数の使い方に関しては是非覚えておきましょう!

実際に select 関数を利用して「2人プレイ用じゃんけんアプリ」を開発する例も下記ページで解説していますので、興味があればこちらも是非読んでみてください!

2人プレイ用じゃんけんアプリの作り方の解説ページアイキャッチ 【Python】2人プレイ用じゃんけんアプリの作り方

同じカテゴリのページ一覧を表示