注: ここでの説明は、TCP ソケット、または少なくとも接続ベースのタイプについて話していることを前提としています。UDP およびその他のデータグラム (つまり、接続ベースではない) ソケットはいくつかの点で似ていますが、select
それらの使用方法はわずかに異なります。
各ソケットは、データを読み書きできるオープン ファイルのようなものです。書き込んだデータは、システム内のバッファに入れられ、ネットワークに送信されるのを待ちます。ネットワークから到着したデータは、読み取るまでシステム内にバッファリングされます。その下では多くの巧妙な処理が行われていますが、ソケットを使用している場合は、(少なくとも最初は) 本当に知っておく必要があるのはそれだけです。
OS の TCP/IP スタックがアプリケーションとは独立してデータを送受信することがわかるため、次の説明でシステムがこのバッファリングを行っていることを覚えておくと便利なことがよくあります。シンプルなインターフェース (それがソケットであり、コードからすべての TCP/IP の複雑さを隠す方法です)。
この読み取りと書き込みを行う 1 つの方法は、ブロッキングです。そのシステムを使用して、たとえば を呼び出したときにrecv()
、システムで待機中のデータがある場合は、すぐに返されます。ただし、待機中のデータがない場合、呼び出しはブロックされます。つまり、プログラムは、読み取るデータが存在するまで停止します。タイムアウトを使用してこれを実行できる場合もありますが、純粋なブロッキング IO では、相手側がデータを送信するか、接続を閉じるまで、本当に永遠に待つことができます。
これは、いくつかの単純なケースではそれほどうまく機能しませんが、他の 1 台のマシンと通信している場合のみです。複数のソケットで通信している場合、1 台のマシンからのデータをただ待つことはできません。あなたに物を送っているかもしれません。ここではあまり詳しく説明しませんが、他にも問題があります。これは良い方法ではないとだけ言っておきましょう。
1 つの解決策は、接続ごとに異なるスレッドを使用することです。そのため、ブロックは問題ありません。他の接続の他のスレッドは、互いに影響を与えることなくブロックできます。この場合、接続ごとに 2 つのスレッドが必要になります。1 つは読み取り用で、もう 1 つは書き込み用です。ただし、スレッドは厄介な獣になる可能性があります。スレッド間でデータを慎重に同期する必要があるため、コーディングが少し複雑になる可能性があります。また、このような単純なタスクにはやや非効率的です。
このselect
モジュールを使用すると、この問題をシングルスレッドで解決できます。単一の接続でブロックする代わりに、「これらのソケットの少なくとも 1 つに読み取り可能なデータが含まれるまでスリープ状態になる」という関数を使用できます (つまり、すぐに修正します)。そのため、その呼び出しがselect.select()
返されたら、待機している接続の 1 つにデータがあることを確認でき、それを安全に読み取ることができます (IO をブロックしていても、注意が必要です。そこにデータがあれば、それを待ってブロックすることはありません)。
アプリケーションを初めて起動するときは、リッスン ソケットであるソケットが 1 つしかありません。したがって、 への呼び出しでそれを渡すだけですselect.select()
。前に行った単純化は、実際には呼び出しが読み取り、書き込み、およびエラー用のソケットの 3 つのリストを受け入れることです。最初のリストのソケットは読み取りのために監視されます - したがって、それらのいずれかに読み取るデータがある場合、select.select()
関数は、プログラムに制御を返します。2 番目のリストは書き込み用です。いつでもソケットに書き込むことができると思うかもしれませんが、実際には、接続の反対側が十分な速度でデータを読み取っていない場合、システムの書き込みバッファーがいっぱいになり、一時的に書き込みができなくなる可能性があります。 . あなたのコードを提供した人はこの複雑さを無視しているように見えますが、これは単純な例としてはそれほど悪くはありません。通常、このような単純なケースでは問題が発生する可能性が低いバッファが十分に大きいためです。しかし、それはあなたがすべき問題ですコードの残りの部分が機能したら、将来アドレスを指定してください。最終的なリストはエラーがないか監視されています - これはあまり使われていないので、今は飛ばします。ここでは空のリストを渡しても問題ありません。
この時点で、誰かがサーバーに接続します。select.select()
これは、リッスン ソケットを「読み取り可能」にするものと見なされるため、関数が返され、読み取り可能なソケットのリスト (最初の戻り値) にリッスン ソケットが含まれます。
次の部分は、読み取るデータがあるすべての接続で実行され、リッスン ソケットの特殊なケースを確認できますs
。コードはaccept()
それを呼び出し、リッスン ソケットから次の待機中の新しい接続を取得し、それをその接続用の新しいソケットに変換します (リッスン ソケットは引き続きリッスンし、他の新しい接続も待機している可能性がありますが、問題ありません -これについてはすぐに説明します)。真新しいソケットがconnections
リストに追加され、リッスン ソケットの処理が終了します。 は、continue
から返された次の接続に移動しますselect.select()
(存在する場合)。
読み取り可能な他の接続の場合、コードはそれらを呼び出して次のバイト (または 1024 バイト未満の場合は利用可能なもの)recv()
を回復します。1024
重要な注意 - 接続が読み取り可能であることを確認していなかった場合select.select()
、この への呼び出しによりrecv()
、その特定の接続にデータが到着するまでプログラムがブロックされ停止する可能性select.select()
があります。
一部のデータが読み取られると、コードは他のすべての接続 (存在する場合) で実行され、send()
メソッドを使用してデータをコピーします。コードは、到着したばかりのデータと同じ接続を正しくスキップし (これは に関するビジネスですq != i
)、またスキップしますs
が、私が見る限り、実際にはconnections
リストに追加されていないため、これは必須ではありません。
select.select()
すべての読み取り可能な接続が処理されると、コードはループに戻り、次のデータを待ちます。接続にまだデータがある場合、呼び出しはすぐに戻ることに注意してください。これが、リッスン ソケットから 1 つの接続のみを受け入れても問題ない理由です。さらに接続がある場合は、select.select()
すぐに再び戻り、ループは次に使用可能な接続を処理できます。ノンブロッキング IO を使用してこれをもう少し効率的にすることもできますが、複雑になるため、ここでは単純にしておきましょう。
これは妥当な例ですが、残念ながらいくつかの問題があります。
- 前述したように、コードは常に
send()
安全に呼び出すことができることを前提としていますが、もう一方の端が適切に受信されていない (おそらくそのマシンが過負荷になっている) 接続がある場合、ここのコードは送信バッファーをいっぱいにしてハングする可能性があります。を呼び出そうとしますsend()
。
- このコードは接続のクローズに対応していないため、 から空の文字列が返されることがよくあります
recv()
。これにより、接続が閉じられてconnections
リストから削除されるはずですが、このコードはそれを行いません。
次の 2 つの問題を解決するために、コードを少し更新しました。
connections = []
buffered_output = {}
while True:
rlist,wlist,xlist = select.select(connections + [s],buffered_output.keys(),[])
for i in rlist:
if i == s:
conn,addr = s.accept()
connections.append(conn)
continue
try:
data = i.recv(1024)
except socket.error:
data = ""
if data:
for q in connections:
if q != i:
buffered_output[q] = buffered_output.get(q, b"") + data
else:
i.close()
connections.remove(i)
if i in buffered_output:
del buffered_output[i]
for i in wlist:
if i not in buffered_output:
continue
bytes_sent = i.send(buffered_output[i])
buffered_output[i] = buffered_output[i][bytes_sent:]
if not buffered_output[i]:
del buffered_output[i]
ここで、リモート エンドが接続を閉じた場合、ここでもすぐに閉じたいと想定していることを指摘しておく必要があります。厳密に言えば、これは、リモート エンドがリクエストを送信し、そのエンドを閉じるが、データが返されることを期待するTCPハーフ クローズの可能性を無視します。非常に古いバージョンの HTTP では、リクエストの終了を示すためにこれを行うことがあったと思いますが、実際にはこれはほとんど使用されておらず、おそらくあなたの例には関係ありません。
また、多くの人が使用時にソケットを非ブロックにすることにも注意してください。これは、またはブロックするselect
呼び出しが代わりにエラーを返すことを意味します (Python 用語で例外を発生させます)。これは、コードの不注意なビットがアプリケーションをブロックしないようにするための安全のための部分です。ただし、データがなくなるまで複数のチャンクで読み書きするなど、もう少し効率的なアプローチも可能です。ブロッキング IO を使用すると、呼び出しは読み取りまたは書き込みするデータがあることを保証するだけであるため、これは不可能です。データの量は保証されません。したがって、呼び出す必要がある前に、ブロッキングを安全に呼び出すことができます。または、各接続で 1 回だけ呼び出すことができます。recv()
send()
select.select()
send()
recv()
select.select()
再度実行できるかどうかを確認します。accept()
リスニング ソケットの にも同じことが当てはまります。
ただし、効率の節約は、一般に、ビジー状態の接続が多数あるシステムでのみ問題になるため、あなたのケースでは物事を単純に保ち、今のところブロックについて心配する必要はありません. あなたの場合、アプリケーションがハングアップして応答しなくなったように見える場合は、どこかでブロッキング呼び出しを行っている可能性があります。
最後に、このコードを移植可能にしたり、より高速にしたい場合は、 のようなものを見る価値があるかもしれません。これには、基本的に、さまざまなプラットフォームでうまく機能libev
するいくつかの代替手段があります。ただし、原則は大まかに似ているため、コードを実行できるようになるまでは今のselect.select()
ところ集中して、後で変更を調査することをお勧めします。select
また、コメンターがTwistedを提案していることにも注意してください。これは、より高いレベルの抽象化を提供するフレームワークであり、すべての詳細について心配する必要はありません。個人的には、便利な方法でエラーをトラップするのが難しいなど、過去にいくつかの問題がありましたが、多くの人がうまく使用しています。それは、彼らのアプローチがあなたの考え方に合っているかどうかの問題です. そのスタイルが私よりもあなたに合っているかどうかを確認するために、少なくとも調査する価値があります. 私は C/C++ でネットワーク コードを書いていたバックグラウンドを持っているので、おそらく私が知っていることに固執しているだけです (Pythonselect
モジュールは、ベースとなっている C/C++ バージョンに非常に近いものです)。
十分に説明できていることを願っています。まだ質問がある場合は、コメントでお知らせください。回答に詳細を追加できます。