Pythonでネットワークプログラミング(5)

TCP/IPプロトコルでのネットワークプログラムをPythonで書くための練習。

今回の検討項目

前回(http://d.hatena.ne.jp/Megumi221/20110412 )までに、クライアント-サーバ型の簡単な計算プログラムを作成した。計算プログラムでは、サーバプログラムは、送られてきた数式をevalして値を送信するだけだったが、今回はもう少しサーバ側の処理が重い場合の実装を考えてみる。
具体的には、サーバプログラムで数十分かかる計算を実行することにする。計算プログラムはあらかじめ別に作成しておき、その実行モジュールを外部プロセスとしてPythonプログラムから起動する。
外部プロセスの起動方法としては以下を参考に。

  • 参考サイト

試験運用中なLinux備忘録 Pythonで外部プロセスを起動して出力と戻り値を処理する

外部プロセス実行プログラム

前回の計算プログラムをベースに、今回の実装を行う。
クライアントプログラムでは、wxPythonで作成されたGUIが起動する。サーバとのコネクションを確立して、サーバプログラムに計算プログラムの実行指示を送信する。サーバプログラムから実行完了の返答があるまでしばらく待つ。実行が完了すると、「Done」と表示される。
クライアントプログラムは以下(cal_client3.py)の通り。

# --- cal_client3.py ---
#
import wx
import time
import socket

class Client:
    def __init__(self):
        HOST = 'localhost'   #localhostでテスト実行
        PORT = 50007
        self.soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.soc.connect((HOST, PORT))  #サーバに接続

class MainFrame(wx.Frame):
    def __init__(self, parent, id):
        wx.Frame.__init__(self, parent, id, title="Client",
                          size=(300, 200))
        p = wx.Panel(self)
        self.text  = wx.TextCtrl(p, -1, "", size=(200, 30))
        btn1 = wx.Button(p, -1, "Run")   #「Run」ボタン
        btn2 = wx.Button(p, -1, "Connect") #「Connect」ボタン
        btn1.Bind(wx.EVT_BUTTON, self.Run)
        btn2.Bind(wx.EVT_BUTTON, self.Connect)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.text, 0, wx.ALL, 5)
        sizer.Add(btn2, 0, wx.ALL, 5)
        sizer.Add(btn1, 0, wx.ALL, 5)
        p.SetSizer(sizer)

    def Run(self, event):
        self.data = '1'
        self.c.soc.send(self.data)  #'1'をサーバへ送信
        self.answ = self.c.soc.recv(32)  #サーバから受信(0は正常終了)
        if self.answ == '0':
            self.text.SetValue('Done')  #正常終了の場合「Done」を表示
        self.c.soc.close()   #接続を切る

    def Connect(self, event):
        self.c = Client()

class MainApp(wx.App):
    def OnInit(self):
        self.frame = MainFrame(None, -1)
        self.frame.Show(True)
        self.SetTopWindow(self.frame)
        return True

if __name__ == '__main__':
    app = MainApp(0)
    app.MainLoop()

サーバプログラムでは、クライアントから'1'を受信したら、あらかじめ用意しておいたプログラムa.outを外部プロセスとして実行する。
サーバプログラムは以下(cal_server3.py)の通り。

# --- cal_server3.py ---
#
import SocketServer
import socket
import os
import subprocess

class MyTCPHandler(SocketServer.BaseRequestHandler):
    def __init__(self, request, client_address, server): #いらない
        SocketServer.BaseRequestHandler.__init__(self, request, client_address, server)
        return

    def setup(self):
        print "Connected by", self.client_address
        return SocketServer.BaseRequestHandler.setup(self)

    def handle(self):
        data = self.request.recv(1)
        if data:
            fd = open('log', 'w')  #a.outが標準出力に書きだす内容を保存するファイル
            args = ['a.out']
            subproc_args = { 'stdin': subprocess.PIPE,
                             'stdout': subprocess.PIPE,
                             'stderr': subprocess.STDOUT,
                             'close_fds' : True }  #WIndowsではclose_fdsは使えない
            p = subprocess.Popen(args, **subproc_args)
            stdout = p.stdout
            while 1:
                line = stdout.readline() #標準出力に書き出される内容の読み込み
                if not line:
                    break
                fd.write(line)  #ファイルに保存しておく
            ret = p.wait()    #プロセスが終了したときの返り値(正常終了なら0)
            fd.close()
            self.request.send(str(ret))
            self.request.close()
            self.server.socket.close()
            os._exit(1)      #サーバプログラムの終了
        return

    def finish(self):  #いらない
        return SocketServer.BaseRequestHandler.finish(self)

if __name__ == '__main__':
    host, port = '', 50007
    server = SocketServer.TCPServer((host, port), MyTCPHandler)
    server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #ソケットのオプション
    server.serve_forever()

プログラムの実行

実行は以下のように行う。

  • ターミナル画面を二つ開いて、一方でcal_server3.pyを実行すると、サーバプログラムが走る。
  • もう一つのターミナル画面で、クライアントのGUIを起動する。
  • GUI画面で「Connect」ボタンを押し、サーバと接続する。
  • 続けて「Run」ボタンを押すとサーバ側でa.outが実行される。
  • 数分後に、a.outが終了してGUI画面に「Done」と表示され実行が完了したことが分かる。


▲クライアントプログラムのGUI画面

▲「Run」ボタンを押した後の状態。サーバからの受信を待つ間、「Run」ボタンが押されたままなのだが...。計算完了までこのまま。再び押せる状態に戻ってほしい。そうするためにここはthreadで実行するべきか。

▲サーバ側の計算が完了したときの画面。「Done」と表示されている。