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

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

今回の検討項目

前回(http://d.hatena.ne.jp/Megumi221/20110409 )はwxPythonでネットワークプログラムをラップした計算プログラムを作成したが、細かい動作の仕様もできるだけ決めたうえで、書き直すことにする。動作が曖昧だと実装も曖昧になってしまい、他の課題に応用する場合に問題点が発現することが往々にしてあるため。
今回は、前回の計算プログラムをSocketServerを使って修正することにする。

  • 参考サイト

SocketServerモジュールを使ったサーバ - Konnichiwa, A doumo

計算プログラム(オリジナル)

GUIから使える、簡単な計算プログラムを書いた。コードはサーバプログラムとクライアントプログラムに分かれている。2つのプログラムは別々に実行する。クライアントプログラムを実行するとGUI画面が起動する。
プログラムの動作は以下の通り。

  • サーバプログラムを実行するだけでは何も起こらない。サーバ側にはGUI画面はない。
  • クライアントのGUI画面上で、「Connect」ボタンを押すと、サーバと接続される。どこから接続されたかが、サーバ側のターミナル画面に表示される。
  • GUI画面上で任意の数式を入力し、「Calculate」ボタンを押すと計算結果が入力欄に表示される。
  • 数式の計算は続けて何度も実行することができる。
  • 「Disconnect」ボタンを押すと、サーバとの接続が切断されサーバプログラムが終了する。
  • クライアントプログラムの終了は、GUI画面を閉じることで行う。
  • クライアントのGUIを起動したままで、再びサーバプログラムを実行すれば「Connect」ボタンを押すことで再び計算可能となる。

これらがちゃんと動作するよう、前回のコードを少し書き直すと以下のようになる。
まず、サーバプログラム(cal_server.py)。

# --- cal_server.py ---
#
import socket
import time

HOST = 'localhost'
PORT = 50007

#接続待ちから接続まで
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(1)
soc, addr = s.accept()
print 'Connected by', addr, time.ctime(time.time())

#データの受信、計算、送信の繰り返し
while 1:
    data = soc.recv(1024)
    try:
        ans = str(eval(data))
        soc.send(ans)
    except:  #計算ができなくなったら終了
        break
soc.close()

続いてクライアントプログラム(cal_client.py)。

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

class Client:
    def __init__(self):
        HOST = 'localhost'
        PORT = 50007
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.s.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, "Calculate")
        btn2 = wx.Button(p, -1, "Connect")
        btn3 = wx.Button(p, -1, "Disconnect")
        btn1.Bind(wx.EVT_BUTTON, self.Send)
        btn2.Bind(wx.EVT_BUTTON, self.Connect)
        btn3.Bind(wx.EVT_BUTTON, self.Disconnect)

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

        self.working = 0

    def Send(self, event):
        self.data = self.text.GetValue()

    def Connect(self, event):
        self.data = None
        self.answ = None
        if not self.working:
            self.working = 1
            self.need_abort = 0

            self.c = Client()
            while 1:
                wx.Yield()  #これがないとフリーズする
                if self.need_abort:
                    break
                if self.data != None:
                    self.c.s.send(self.data)
                    self.data = None
                    self.answ = self.c.s.recv(1024)
                if self.answ != None:
                    self.text.SetValue(self.answ)
                    self.answ = None
            self.working = 0

    def Disconnect(self, event):
        if self.working:  #接続を切断する
            self.c.s.close()
            self.need_abort = 1

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()

前回と大きく異なるところは、終了処理のところ。前回は仕様が曖昧でエラーで終了していた。

サーバプログラムの修正

SocketServerモジュールを使って、サーバプログラムを修正する。
「Disconnect」したとき、どのようにサーバプログラムを終了するかがここでのポイント。serve_forever()で無限ループに入るが、shutdown()を呼べばループを抜けられるらしい。しかし、python2.6からのサポートになるようで、python2.5では使えないようだ。
そこで、os._exit(1)で無理矢理終了させてみた。

# --- cal_server2.py ---
#
import SocketServer
import os

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):
        while 1:
            data = self.request.recv(1024)
            try:
                ans = str(eval(data))
                self.request.send(ans)
            except:
                break
#        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.serve_forever()

LinuxのPython2.5では動作確認できた。Windows(Python2.6)でサーバプログラムを実行しようとすると、以下のエラー。

Traceback (most recent call last):
  File "D:\share\cal_server2.py", line 36, in <module>
    server = SocketServer.TCPServer((host, port), MyTCPHandler)
  File "C:\Python26\lib\SocketServer.py", line 400, in __init__
    self.server_bind()
  File "C:\Python26\lib\SocketServer.py", line 411, in server_bind
    self.socket.bind(self.server_address)
  File "<string>", line 1, in bind
error: [Errno 10048] 通常、各ソケット アドレスに対してプロトコル、
ネットワーク アドレス、またはポートのどれか 1 つのみを使用できます。

この場合は、ポート番号がすでに使われているので、別の番号に変えて試してみたらうまくいった。