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

TCP/IPプロトコルでのネットワークプログラミングをPythonで実装することが目標。

今回の検討項目

前回(http://d.hatena.ne.jp/Megumi221/20110310)は、簡単なチャットプログラムを実装してみたが、ここではネットワークプログラムにGUIをラップする方法を検討する。
というのも、実際にコードを書いてみると、GUIのループ(wxPythonではapp.MainLoop()のこと)とネットワークプログラムのwhileループ(データの送受信をするところ)の両立が単純ではない。コードの中にwhileループがあると、GUIが立ち上がらない、もしくは立ちあがってもwhileループに入るとフリーズすることが分かる。
これを避けるためには、通信部分をThreadで実行するか、wx.Yieldを使ってフリーズするのを回避する。

  • 参考サイト

wxPyWiki LongRunningTasks
how to make python socket server work with the app.MainLoop() in wxPython?

計算プログラムの実装

GUIから使える、簡単な計算プログラムを書いてみた。クライアントプロセスから数式を送信し、サーバプロセスで数式の計算を実行し結果をクライアントへ送信する。プログラムの内容としては、

  • 通信にはコネクション型を用いる。
  • ユーザはクライアントプログラムだけを操作する。
  • クライアントのGUI画面はwxPythonで実装する。
  • クライアント画面上で、サーバプログラムへの接続、数式の入力、結果の表示が行われる。
  • サーバプログラムにはGUI画面はない。
  • ユーザはサーバプログラムを最初に起動するだけで、以後操作を行わない。

サーバプログラムは以下の通り。

# --- 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)     #1つの接続要求を待つ
soc, addr = s.accept()  #要求がくるまでブロックする
print 'Connected by', addr
print 'Time :', time.ctime(time.time()) #接続完了

while 1:
    data = soc.recv(1024)  #1024バイトまでのデータを受け取る
#    if data == "quit": 
#        break
    ans = str(eval(data))  #dataは数式なので計算を実行する
    soc.send(ans)      #結果を送信する
soc.close()

クライアントプログラムは以下の通り。

# --- 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, "End")
        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:
                time.sleep(0.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()

プログラムの実行方法

テストなので、localhostで実行してみる。

  • ターミナル画面を2つ開く。
  • 一方の画面(サーバ側画面)で、python cal_server.pyを実行する。この時点では、画面には何も表示されない。
  • もう一方の画面(クライアント側画面)で、python cal_client.pyを実行する。するとGUI画面が起動する。

  • 画面上の「Connect」ボタンを押すと、サーバとの接続が完了する。サーバ側画面には、接続日時がprintされる。
  • GUI画面上の入力欄に数式を入力する。この例では、「1+2+3+5」とした。

  • 画面上の「Calculate」ボタンを押すと、計算結果である「11」が表示される。

  • 別の計算を行うときには、入力欄に数式を再び入力して、「Calculate」ボタンを再び押す。
  • 「End」ボタンでサーバとの接続が終了する。

ちゃんと動くが、現状だと、サーバプログラムの終了がうまくできていない。