ソケット通信でサーバーコールバックを受ける仕組みを作る(Python, socket)

 前回まででPythonのsocket通信で基本的なデータ送受信を実験してみました:

そしてそのままでは実用するに辛いというお話もしました。

 今回は少し実用に寄せるべく、送信側からの取得要求に対してサーバー側がコールバックする仕組みを実験してみようと思います。

 下記記事は前回までの記事の続きと言う形になっておりますので、もし未読の方は是非一読下さい。

サーバーコールバック

 Raspberry Piは外部機器とやり取りするのが醍醐味です。もちろんRaspberry Piだけで完結する形にも出来ますが、折角WiFi等があるのですからネットワークを通して遠隔操作したり、取り付けているセンサー等の機器の情報を別のPCで得たり出来れば面白さ倍増ですよね。ソケット通信はそれを実現する一つの方法です。ただし一方通行な通信なので「○○をくれ」とクライアントからサーバーに伝えても、その結果を受けるにはクライアント側で待ち受けて、サーバ側から返答(コールバック)してもらう必要があります。

 まずは愚直にその辺りを実装してみます。

サーバーからの返答をスレッドで受ける

 サーバーに要求(コマンド)を出すクライアント側では、sendした後に時間差で結果を受ける仕組みが必要になります。もちろんこれは、

# 値取得をサーバーに要求
client = socket.socket( socket.AF_INET )
client.connect( ("192.168.11.16", 15769) )
client.send( "call getValue".encode( "utf-8" ) )
client.close()
 
# 返答待機
sv = socket.socket( socket.AF_INET )
sv.bind( ( "192.168.11.16", 15769 ) )
sv.listen()
res, addr = sv.accept()   # 無限待ち
 
# 得たデータを活用
data = res.recv( 1024 )

こんな感じで送信後に返信を待ち続けて、返答が来たらそのデータを活用するという実装にしても実現できると言えば出来ます。ただし上のコードだとacceptの所で返答をブロッキングで待っているため、これをメインスレッドで実行してしまうと、待っている間何もプログラムが進行しないという事態になってしまいます。

待機処理をスレッドに逃がす

 これを回避する一つの方法として待機処理をスレッドに逃がす方法が考えられます。上のコードで返答待機している所を別関数にして、それをスレッドで再生するようにしてみます。動作する全コードはこちら:

import socket
import threading

# サーバーからの戻り値を待ち受けるスレッドメソッド
def receiveReturn():
    res, addr = sv.accept()   # 受信待機

    #返答をコンソールに出力
    data = res.recv( 1024 )
    str = data.decode( "utf-8" )
    print( str )

# 戻り値待ち受け用のサーバ
sv = socket.socket( socket.AF_INET )
sv.bind( ( "192.168.11.2", 15769 ) )
sv.listen()

# コールバック要求クライアント
while True:
    client = socket.socket( socket.AF_INET )

    try:
        client.connect( ("192.168.11.16", 15769) )
    except:
        input( "Failed connection. Press enter to retry." )
        continue

    cmd = input( "Input command: " )
    if ( len( cmd ) == 0 ):
        break

    client.send( cmd.encode( "utf-8" ) )
    client.close()

    # 返答をスレッドで受ける
    thre = threading.Thread( target = receiveReturn )
    thre.start()

スレッドを使うためにはthreadingライブラリが必要なのでインポートします。

 プログラムが開始するとまずサーバーからの返答を受け取るsocketオブジェクト(sv)が作られます。次にinput関数でサーバーに渡したいコマンドを入力し送信します。送信後ただちにスレッドを立ち上げ、receiveReturn関数が実行されます。receiveReturn関数でサーバーからの返答を待ち受け、受信したらそれをprint関数でコンソールに表示します。

 スレッド内でサーバーからの返答を待っている間、メインスレッドはwhileループがくるっと回り、再びコマンド入力状態になります。

 実際は例外処理を色々挟む必要がありますが、この実装でサーバーコールバックは出来ています。サーバー側が要求に対して何か返答すればちゃんとコンソールにサーバーからの文字列が表示されます。この実装をベースに、もう少し汎用性と使いやすさ向上を目指してみます。

送信から返答までを丸々関数化してみる

 上のコードで共通する所を検討してみましょう。送信するコマンドは毎度異なります。でもその後の「コマンド送信」→「受信スレッド起動」→「コールバック受信」ここまで流れは一定です。コールバックで戻された戻り値をどう使うかは最初の送信コマンドにより異なります。

 以上から「コマンド送信」→「受信スレッド起動」→「コールバック受信」までを共通化できるのがわかります。実施にやってみましょう:

import socket
import threading

# ↓↓ 共通化出来る所 ↓↓

# サーバーからの戻り値を待ち受けるスレッドメソッド
def receiveReturn( callback ):
    res, addr = sv.accept()   # 受信待機

    #返答をコールバックに返す
    data = res.recv( 1024 )
    str = data.decode( "utf-8" )
    callback( str )

# コマンドを送信して返答をコールバックする
def sendCommand( client, command, callback ):
    val = client.send( command.encode( "utf-8" ) )
    client.close()

    # 返答をスレッドで受ける
    thre = threading.Thread( target = receiveReturn, args = ( callback, ) )
    thre.start()

# ここまで共通化



# 戻り値待ち受け用のサーバ
sv = socket.socket( socket.AF_INET )
sv.bind( ( "192.168.11.2", 15769 ) )
sv.listen()

# コールバック要求クライアント
while True:
    client = socket.socket( socket.AF_INET )

    try:
        client.connect( ("192.168.11.16", 15769) )
    except:
        input( "Failed connection. Press enter to retry." )
        continue

    cmd = input( "Input command: " )
    if ( len( cmd ) == 0 ):
        break

    sendCommand( client, cmd, print )

 まずsendCommand関数を新設しました。この関数内で先の共通する部分(コマンド送信、受信待ち、コールバック受信)までを担います。ですから異なる部分である送信先(client)、送信コマンド(command)そして結果を処理する関数(callback)を引数に受け取っているんですね。

 実際サーバーからの返答を受け取るのはスレッド関数なので、その担い手であるreceiveReturn関数の引数にもcallbackを追加しました。関数内で

callback( str )

となっていますよね。callbackが文字列を受ける物であれば何でも良くなりました。汎用性アップです。

 スレッド関数に引数を渡すにはthreading.Threadメソッドのargs引数に渡したい引数をタプルで指定します。上のコードではsendCommand関数に渡されたcallback関数をそのまま引き継いでいます。

 この共通化によってコード最下段にあるように、

sendCommand( client, cmd, print )

この1行でコマンド送信と受信データの処理が非同期で出来てしまいます!上例ではprint関数を渡していますが、勿論オリジナルな文字列を受ける関数でもOK。各コマンドに対応したコールバック関数を渡せば、サーバーとの多様なやり取りを吸収できます。

 ちょっと面白くなってきましたので、この共通項をさらに「モジュール化」してみましょう。

socketmanagerモジュールを作る

 先のsendCommand関数を毎回書くのは面倒なので、モジュール化(別ファイル化)して再利用できるようにしてみます。

 Pythonの関数をモジュール化するのはとっても簡単。別のPythonファイルを作って関数を移植し、使いたい所でインポートするだけです。これは実際にやってみた方が早いですね。

 先程再利用の為分離したreceiveReturn関数とsendCommand関数をsocketmanager.pyという別Pythonファイル内に移植します。先程はreceiveReturn関数でグローバルにある待ち受け用のsocketオブジェクト(sv)を使っていましたが、モジュール化するとそれが出来なくなるので、代わりにsendCommand関数の引数でそれを与えるようにします:

# socketmanager.py

import threading

# サーバーからの戻り値を待ち受けるスレッドメソッド
#  sv      : listen済のサーバーソケットオブジェクト(socket)
#  callback: サーバーからの戻り値文字列を処理するコールバック関数
def receiveReturn( sv, callback ):
    res, addr = sv.accept()   # 受信待ち

    #返答をコールバックに返す
    data = res.recv( 1024 )
    str = data.decode( "utf-8" )
    callback( str )


# コマンドを送信して返答をコールバックする
#  client  : connect済みの送信用socketオブジェクト
#  sv      : listen済のサーバーソケットオブジェクト(socket)
#  command : 送信コマンド文字列
#  callback: サーバーからの返答文字列を受けるコールバック関数 
def sendCommand( client, sv, command, callback ):
    val = client.send( command.encode( "utf-8" ) )
    client.close()

    # 返答をスレッドで受ける
    thre = threading.Thread( target = receiveReturn, args = ( sv, callback ) )
    thre.start()

このように共通部分をモジュールとして分離出来たら、socketmanager.pyをメインとなるpythonファイルと同じフォルダに入れて、メインコード側でインポートして利用します:

#メイン

import socket
import socketmanager

# 戻り値待ち受け用のサーバ
sv = socket.socket( socket.AF_INET )
sv.bind( ( "192.168.11.2", 15769 ) )
sv.listen()

# コールバック要求クライアント
while True:
    client = socket.socket( socket.AF_INET )

    try:
        client.connect( ("192.168.11.16", 15769) )
    except:
        input( "Failed connection. Press enter to retry." )
        continue

    cmd = input( "Input command: " )
    if ( len( cmd ) == 0 ):
        break

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

import socketmanager というインポート文でモジュールを使えるようにします。

 戻り値待ち受け用のサーバーsocketオブジェクト(sv)を作りlistenしておきます。whileループで送信用のクライアントsocketオブジェクトを都度作り、送信コマンドをinput関数で得たら、

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

このようにsocketmanagerモジュール内のsendCommad関数を呼び出して送信とコールバックを担ってもらいます。この関数はブロッキングせずに即時に戻ってきますから、第4引数のコールバック(ここではprint関数)に返答が返ってくるまでの間にwhileがくるっと回って次のコマンド送信がすぐに出来ます。

 改修前にメイン側でごちゃごちゃとしていた所がsocketmanagerモジュールに移転した事で、メインのコードがスッキリしました。もちろん別のプログラムでもsocketmanagerモジュールは再利用できるので利便性もアップです。ただしこれはサンプルなので実際に実用するには例外処理をしっかり入れないといけません。その点はご注意下さい。

終わりに

 今回はsocket通信でサーバーからのコールバックを処理する方法について見て来ました。サーバーからの返答をacceptで無限待ちにすると他の事が出来ないため、まずそこをスレッドに逃がしました。次に汎用性を高めるため、戻って来た値を処理するコールバック関数を登録するスタイルにし、最後に諸々をsocketmanagerモジュールとして分離し再利用できるようにしました。これでクライアント側としての動作は実現できました。

 ただしこの実装はまだまだ脆弱です。そもそも例外処理を色々入れないと実用出来ないですし、戻り値を待ち受ける所も整合性が狂うパターンが考えられます。例えば返答に時間がかかるコマンドAを送信したとします。その後すぐにコマンドBも送信したとすると、2つのスレッドで同時に待ち受ける状態が発生します。この場合、おそらく2回目のBのaccept呼び出しで例外が発生します。

 これを防ぐ手として、コマンドAはポート番号Aを、コマンドBはポート番号Bを…という風に受けるポートを別々にする方法があります。サーバーに送信する情報に返答先ポート番号を付記しておいて、サーバーはその番号に返答します。これは現実世界で言えば「返信用封筒」みたいなものですね。

 色々頑健にしたい所はありますが、次回はこのクライアントからの送信を受けてコールバックするサーバー側の実装を見て行こうと思います。

ではまた(^-^)/

<次回>

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