ソケット通信で受信コマンドに対してコールバックする仕組みを作る(Python, socket)

 前回ソケット通信でクライアント側から送信したコマンドに対してサーバーからの返答を待つ仕組みを実験してきました:

 今回は逆の立場、つまりクライアントからのコマンドを受けたサーバ側で、そのコマンドに対応した処理をして結果をクライアントに返す仕組みを作ってみます。前回と今回とで題目が言葉遊びみたいになっていますが、サーバー側のお話です。

コマンドを受けた後に送信するだけ

 やる事は非常に単純です。箇条書きにしてみます:

  • クライアントのコマンドをacceptで待つ

  • コマンドを受けたら対応する処理に飛ぶ

  • 処理の結果をまとめる

  • クライアントのIPアドレスとポート番号に結果を送信

これを愚直にコードにおこすとこうなります:

import socket

sv = socket.socket( socket.AF_INET )
sv.bind( ( "192.168.11.16", 15769 ) )
sv.listen()

while True:
    # クライアントのコマンドを待つ
    client, addr = sv.accept()
    data = client.recv( 1024 )
    if len(data) == 0:
        break

    # 対応する処理に飛ぶ(つもり)
    print( data.decode("utf-8") )

    client.close()

    # クライアントに返答
    res = socket.socket( socket.AF_INET )
    res.connect( ( addr[0], 15769 ) )
    res.send( "Thank you!".encode( "utf-8" ) ) 
    res.close()

 whileの頭でacceptメソッドでブロッキングして受信待ちしています。今回は受信文字列が来たらそれをprintでエコーしています。コマンドを処理したつもりな部分です。

 その次からが返答処理で、新規のクライアント用socketオブジェクトを作ります。その次の1行はちょっとポイントかもしれません:

res.connect( ( addr[0], 15769 ) )

 サーバ側のacceptが通った時、戻り値としてclient socketオブジェクトとaddrに送信したクライアントのIPアドレス情報が返ります。このaddrは実際はaddr = ( "IPアドレス", 送信元ポート )というタプルになっています。このIPアドレスが返送先って事になる訳です。ポート番号は取り決めた物を使います。

 上の例では、どんなコマンドが来ても送信元のクライアントに「Thank you!」という文字列を返しています。意味はもちろんありませんw。

 これでクライアント側もサーバにコマンドを送信すると速攻で「Thank you!」という文字列を受け取るようになります。

本当はclientで即sendしたいのですが…

 acceptで戻されたclientですが、上の例ではそれを使わず返答用のsocketオブジェクト(res)を作ってsendしています。実は環境によってはそうせずに戻されたclientで直接sendできるようなのですが、僕の環境(Raspberry Pi)だとポート番号に不一致があってうまく行かなかったため上の代替方法にしました。

コマンド対応と返答を別スレッドで処理

 さて上の実装は一応動きますが、実用するにはちょっとまだ厳しいです。特にaccept後の処理時間が問題になります。何かコマンドが飛んできてacceptを抜けた後、そのコマンドが凄い処理時間がかかるものである事がわかったとします。もしその処理をメインスレッドで行ってしまうと、その間acceptが呼ばれません。acceptしていなくても次の受信の受理(クライアントから「データを送信したいんだけど良いですか?」という問い合わせを受理している状態)はOS内にスタックしてくれてはいるのですが、サーバー側で処理に時間がかかっているためクライアント側は待ちぼうけになってしまいます。もしサーバーからの返答を期待するコマンドだった場合、下手をするとクライアント側が止まった状態になってしまいます。

 つまり、サーバーはacceptしたコマンドに対応する処理をメインスレッド(acceptループしているスレッド)で行ってはいけないんです。

 そこで、acceptを抜けた後の処理をスレッドに逃がしてしまいましょう。そうすれば返答処理に時間がかかってもメイン側はすぐにまたaccept状態に移行してくれて、クライアントの送信受理に応える事ができます。

 例えばこんな実装になるでしょうか:

import socket
import threading

# コマンドに対する処理と返答をする
def responseToCommand( client, addr ):
    # 処理:コンソールにエコーする
    data = client.recv( 1024 )
    print( data.decode("utf-8") )

    # クライアントに返答
    res = socket.socket( socket.AF_INET )
    res.connect( ( addr[0], 15769 ) )
    res.send( "Thank you!".encode( "utf-8" ) )
    client.close()


sv = socket.socket( socket.AF_INET )
sv.bind( ( "192.168.11.16", 15769 ) )
sv.listen()

while True:
    client, addr = sv.accept()

    # 別スレッドでクライアントに返答
    thre = threading.Thread( target = responseToCommand, args = ( client, addr ) )
    thre.start()

 まずスレッドを使うのでthreadingをインポートします。受けたコマンドに対する処理と返答を行うresponseToCommand関数を次に追加します。関数の中は引数のclientオブジェクトとIPアドレスを用いて先と同様の「処理」と「返答」をしているだけです。

 while内のacceptを抜けた後にスレッドを作りtargetにresponseToCommand関数を、args変数に関数に渡す引数を指定し、別スレッドで関数をじっこうしてもらいます。

 これでコマンドを受信した後処理はスレッド内で行われ、その間メイン側では直ちに再acceptによる受信待ち状態に戻ります。少なくとも最初よりはずっとマシになりました。

 コマンドに対する応答部分は、上の例では単にサーバーのコンソールに表示しているだけですが、実際は元のコマンドを解析して対応する別関数に振り分けて対応する事になると思います。その部分は作るアプリケーションの仕様次第ですので、今回は割愛致します。

終わりに

 コマンドを受けた後の処理をスレッドに逃がす事で、コマンド受信と返答が分離出来ました。外部機器にポチポチと命令を与える程度の頻度ならこれで十分持ちます。ただし大規模なネットワークゲームのようにコマンドが秒間何百何千という凄まじいレベルになりますとスレッド化のコストが激増してしまう為、この方法ではまるで耐えられなくなってしまいます。その場合はロードバランサーなどでコマンドを複数のサーバーに分配するなどフロント処理を挟むなどして処理の最適化を図ったりします。

 さてこれで前回のクライアント側でのコマンド送信とサーバーからの返答(戻り値)待機、そして今回のサーバでのコールバックする仕組みが一通り整いました。クライアント側視点で見ると、

socketmanager.sendCommand(client, sv, cmd, callback)

こういう呼び出し一つでサーバーから答えが返ってくる事になります。ただし上の関数はブロッキングしない「非同期関数」です。非同期関数が一つでも入ると、途端にプログラムはメンドクサイ事になります(^-^;。特にサーバーからの答えを見て次の行動を決めるような「状態遷移」が絡むと面倒くささに拍車がかかります。

 ではそういう「非同期な世界の状態遷移」と具体的にどう付き合っていけば良いのでしょうか?その辺りを次回検討してみましょう。

ではまた(^-^)/

<次回>

この記事が気に入ったらサポートをしてみませんか?