🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Q&A

解決済

1回答

3593閲覧

Python3 Tkinter Tkinterとソケット通信のサーバの立ち上げ

person

総合スコア224

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

0グッド

1クリップ

投稿2021/01/22 14:57

編集2021/01/25 05:20

前回の質問(Python3 Tkinter チャットを作成したい
にて、Tkinterとソケット通信でチャットのようなアプリを作る際にサーバとクライアントの両方を使うことができるような回答を頂いたので、とりあえず作ろうと思いました。

(サーバは受信専用、クライアントは送信専用な使い方をしようとしています。)

まずは、TkinterのUI表示とサーバの立ち上げをしたいのですが、早速わからないことがいっぱいです。

Tkinter使いながらソケット通信する上で下記要件を満たす必要がある(?)と思いました。

  1. ソケット通信の開始と終了を任意の位置でできるようにする。

(理由は、勝手に通信を開始したり終了されては困るから。)
とりあえず、開始はTkinterの画面が出る前、終了はTkinterの画面が閉じたときとします。
2. ソケット通信が切れたときの復帰、復帰もできなかった際の処理。
(Tkinter閉じてしまえばいい?)
3. サブスレッドで受け取ったデータをメインスレッドに渡す。
(TkinterのLabelなどで表示したいから。)

3については、過去にスレッドセーフなグローバル変数等で渡すやり方を教わったので、キューを使うことにしました。

2は現状できなくてもいいです。(そもそも、それ以外が完成しないと実装が難しいかもしれないので。)

1についてがわからない部分です。
ソケット通信(サーバ)を開始〜立ち上げ状態については、サブスレッドスタートさせて繰り返しループさせればいいのかなって思っています。メッセージを常に受信できるようにしたいため。
終了するときの方法がわからず、受信で繰り返しループしているので、そのループフラグを落とせばいいのかと思ってやってみたのですが、できませんでした。(閉じるボタンを押したときにエラー。単純に変数にアクセスできない?)

コードは下記に記載しますが、そもそもソケット通信(サーバ)の開始終了についてはこの方法でできますか。
もし、ソケット通信の開始終了でフラグを落とすだけではなく他に処理が必要な場合は教えて下さい。

回答よろしくおねがいします。

該当のソースコード

Python:

1from tkinter import ttk 2import queue 3import socket 4import threading 5import tkinter as tk 6 7class Controller: 8 def __init__(self): 9 self.rcv_data = queue.Queue() 10 self.main() 11 12 def main(self): 13 self.thread = threading.Thread(target=self.loop) 14 self.thread.start() 15 self.win = tk.Tk() 16 self.view = View(self.win) 17 self.win.protocol("WM_DELETE_WINDOW", self.close) 18 self.win.mainloop() 19 20 def loop(self): 21 self.svr = Server(self.rcv_data) 22 23 def close(self): 24 # サーバ切断したい 25 self.svr.loop_flag = False 26 27 self.thread.join() 28 self.win.destroy() 29 30class View: 31 def __init__(self, win): 32 self.win = win 33 self.win.geometry("400x300") 34 35# ソケット通信参考 36# https://techacademy.jp/magazine/19147 37class Server: 38 def __init__(self, rcv_data): 39 self.loop_flag = True 40 self.ip_adr = "127.0.0.1" 41 self.port = 50000 42 self.buffer_size = 1024 43 self.start(rcv_data) 44 45 def start(self, rcv_data): 46 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 47 s.bind((self.ip_adr, self.port)) 48 s.listen(1) # <--待ち状態に入る(これも接続終了時に対応する必要あり?) 49 while True: 50 con, adr = s.accept() 51 print(con, adr) 52 while self.loop_flag: 53 data = con.recv(1024) 54 if not data: 55 break 56 print("recv") 57 rcv_data.put() 58 59 60if __name__ == "__main__": 61 ctrlr = Controller()

閉じるボタンを押したときに表示されたエラー

Exception in Tkinter callback Traceback (most recent call last): File "C:\Users\user01\AppData\Local\Programs\Python\Python38\lib\tkinter\__init__.py", line 1883, in __call__ return self.func(*args) File "c:/Users/user01/Desktop/test.py", line 32, in close self.svr.loop_flag = False AttributeError: 'Controller' object has no attribute 'svr'

追記

try-except追加、daemon化、二重ループに再変更

Python

1from tkinter import ttk 2import queue 3import socket 4import threading 5import tkinter as tk 6 7loop_flag = True 8 9 10class Controller: 11 def __init__(self): 12 self.rcv_data = queue.Queue() 13 self.main() 14 15 def main(self): 16 self.thread = threading.Thread(target=self.loop, daemon=True) 17 self.thread.start() 18 self.win = tk.Tk() 19 self.view = View(self.win) 20 self.win.protocol("WM_DELETE_WINDOW", self.close) 21 self.win.mainloop() 22 23 def loop(self): 24 self.svr = Server(self.rcv_data) 25 self.svr.start(self.rcv_data) 26 27 def close(self): 28 global loop_flag 29 # サーバ切断したい 30 loop_flag = False 31 32 self.svr.s.close() 33 #self.thread.join() 34 self.win.destroy() 35 36 37class View: 38 def __init__(self, win): 39 self.win = win 40 self.win.geometry("400x300") 41 42# ソケット通信参考 43# https://techacademy.jp/magazine/19147 44 45 46class Server: 47 def __init__(self, rcv_data): 48 self.ip_adr = "127.0.0.1" 49 self.port = 50000 50 self.buffer_size = 1024 51 52 def start(self, rcv_data): 53 global loop_flag 54 self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 55 self.s.bind((self.ip_adr, self.port)) 56 self.s.listen(1) 57 58 while loop_flag: 59 try: 60 con, adr = self.s.accept() 61 print(con, adr) 62 while loop_flag: 63 data = con.recv(1024) 64 print(data) 65 if not data: 66 break 67 print("recv") 68 rcv_data.put() 69 except Exception as e: 70 print(type(e)) 71 print(e) 72 73 74if __name__ == "__main__": 75 ctrlr = Controller() 76

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

68user

2021/01/22 15:24

動かしてませんし、Tkinter を知りませんが、生成したスレッドでは self.svr にはオブジェクトがあるんでしょうけど、それをメインスレッドで self.svr によって参照できるんでしたっけ?
68user

2021/01/23 11:55

的はずれな指摘でしたね。グローバル変数経由で渡すものかと思っておりました。失礼しました。
guest

回答1

0

ベストアンサー

問題点: スレッドの使い方

スレッドで開始される処理 target=self.loop を追っていくと

  • Server のインスタンスを生成
  • __init__ 内で start() を呼び出し
  • start() はソケットからデータを読み取るループ
  • __init__ の処理が終わらない為、

  self.srv = Server(...) の右辺は値を返す前に止まっている。
インスタンス変数 self.srv は、未設定のままとなってます。

暫定的な解決策ですが、
Serverのstart を threadのtargetで呼ぶようにしてく下さい。

python

1 def loop(self): 2 self.srv = Server(self.rcv_data) 3 self.srv.start() 4 5# class Server __inif__ の self.start(rcv_data) を削除

<--待ち状態に入る(これも接続終了時に対応する必要あり?)

listen() の時点では待機状態になりません。accept() が接続を待機する場所です。

スレッドの終了後に、何か後始末の処理を行いたい場合は、
accept() で処理が待機してしまわないような工夫が必要になります。
(ノンブロッキング、タイムアウトを設定、asyncio を使う等)

ですが、スレッド内で tkinterへは直接参照してないので、
スレッドを終了するだけであれば、Thread(..., daemon=True)で
終了時に強制的にスレッドも終了(強制的に中断)できます。
※スレッド内でデータベースやファイルやネットワークへの書き込みがある場合、
中断はあまり良い方法ではありません。


3 について キューを使う場合は、tkinterの
タイマー (afterメソッド) を使って定期的に queue の内容を読み出しします。

投稿2021/01/23 03:17

teamikl

総合スコア8738

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

person

2021/01/23 05:29 編集

回答ありがとうございます。 68user様からもご指摘いただいているのですが、 AttributeError: 'Controller' object has no attribute 'svr' については、グローバル変数に変更したほうがよろしいでしょうか。 グローバル変数に変更、teamikl様の回答を反映したときに、 実行→閉じるしたときに応答なしになりました。 エラー表示なし。 変更後のソースコードは質問文に追記します。
teamikl

2021/01/23 05:40 編集

スレッドを跨いでのオブジェクトの参照は (スレッドセーフかどうかは別として)メインスレッドでも可能です。 問題点は、スレッドで実行されるコードが self.svr = に到達していない為です。 ---- 別スレッドからのインスタンス変素の参照は可能ではありますが、 出来る限りスレッドを跨ぐ参照は、定数以外には行わない方が良いです。 リソースの生成と後始末は同スレッド内で行い、 スレッドを跨ぐリソース(この場合はキュー)の共有はスレッドのargs= で渡します。
teamikl

2021/01/23 06:10 編集

thread.join() は、このコード (Server.startメソッド) では ループを終了できないので、thread.join() から戻ることはなく、 GUIのイベントループはその間動かないので、応答なしになります。 簡単な解決策は回答に書いた、daemon thread にする方法です。 その場合、任意の後始末処理はできませんが、thread.join() は不要です。 thread.join() して、正常にループを抜けて終了する場合は 以下の点を解消する必要があります。 - while True: ループを抜けない - accept() や read() のブロッキング操作 詳しくは、ソケットプログラミング HOWTO~ノンブロッキング https://docs.python.org/ja/3/howto/sockets.html#socket-howto 具体的な実装としては、標準ライブラリ内ではsocketserver のソースコードが参考になります。 https://docs.python.org/ja/3/library/socketserver.html
person

2021/01/23 08:00

”with 〜" を close() 使うに変更したところ、閉じるボタンを押したところで次のようなエラーが発生しました。 Exception in thread Thread-1: Traceback(most recent call last): File "C:\Users\user01\AppData\Local\Programs\Python\Python38\lib\threading.py", line 932, in _bootstrap_inner self.run() File "C:\Users\user01\AppData\Local\Programs\Python\Python38\lib\threading.py", line 870, in run self._target(*self._args, **self._kwargs) File "c:/Users/user01/Desktop/test.py", line 25, in loop self.svr.start(self.rcv_data) File "c:/Users/user01/Desktop/test.py", line 58, in start con, adr = self.s.accept() File "C:\Users\user01\AppData\Local\Programs\Python\Python38\lib\socket.py", line 292, in accept fd, addr = self._accept() OSError: [WinError 10038] ソケット以外のものに対して操作を実行しようとしました。
teamikl

2021/01/23 12:09 編集

accept や recv の部分を try ~ except ~ finaryで例外を補足して return すれば ループを抜けることはできますが、(breakでは内側のループしか抜けない点に注意) 別スレッドで生成したソケットに対してaccept()やrecv()している最中に メインスレッドでclose()を呼び出す事になるので、 出来るだけ同一スレッド内でのみで完結するようにした方が良いです。
person

2021/01/25 02:57 編集

try-except追加、daemon化しました。 あと参考元でwhile Trueのループが二重になっている理由がよくわからなかったため、色々サイトを回ってみたら二重になってなかったものがあったのでそれをもとに多少変更しました。(質問文の追記を変更) 一見、exceptの方でエラーは弾かれているようですが、 動作的にはこれで問題ないでしょうか? except, finallyに追加するべき処理はありますか?
teamikl

2021/01/25 04:45 編集

ループの2重化は、接続の受付(accept)とデータの受信の2つですね。 現状の実装では1人用(データの受信が終わるまで次の接続を受け付けない) なので、チャットサーバーを実装予定なら 複数の同時接続に対応する必要があるはずです。 ソケットをノンブロッキングで扱い、 単一スレッド内で実装する方法もありますが、(selectを使う。asyncioを使う等) tkinter と同一プロセス内で使うとなると事例が少ないと思うので、 まずは率直にそれぞれのループを個別のスレッドにしてみては如何でしょう。 - 接続を受け付けるスレッド (accept のループ) - データを受信する接続毎のスレッド (データを受信するループ) - メインスレッド (tkinter のGUI のイベントループ) ---- > 動作的にはこれで問題ないでしょうか? > except, finallyに追加するべき処理はありますか? 問題は無いかもしれませんが、別スレッドから close するのは - accept() 中に例外が発生する場合 - recv() 中に例外が発生する場合 を考慮しなければならず、例外時の動作テストがしにくい構造になってます。 (他にもclose済みのソケットに対する操作全てで、 例外を投げられる可能性がある → 例外が広範囲で起こる可能性) 一般的にマルチスレッドでスレッドを跨いでリソースを操作するのは、 スレッドセーフな操作かどうかを調べておかないとタイミングによる問題 問題が起こっても再現が難しい・条件次第で稀に起こる等、 問題が起きた時にデバッグが難しくなりやすいので 早期の段階で、その様な設計は避けた方が無難です。 「GUIがフリーズするのを防ぐ」という部分については、 原因は thread.join() でのブロッキングなので、 これ自体は thread.join() をなくし daemonフラグを設定で解消できます。 以下は、スレッド終了時のsocketの後始末をきちんとしたい場合、 例えば、tkinter は起動したままサーバーのみ再起動したいとき等に取り組んでみてください。 ブロッキングの問題: accept() 等の時点で待機中~となってるため while loop_flag: のフラグによる終了通知が出来ない。 別スレッドからソケットをcloseして、強制的に例外を起こさせるのは、 スレッドを正常終了させるための手段の一つではありますが、 socket 自体は強制的な終了で、上述のデメリットもあります。 HOW TO で紹介されてる socketをノンブロッキングで扱う方法では select モジュールを使い 例えば、accept() が必ず成功するタイミングを通知されてから 実際にaccept()を呼び出すような設計に出来る為、 コードの実行がどこかで止まることはなく、 フラグ判定によるループの終了が可能になります。 但し、コードは複雑になりがちです。
person

2021/01/25 05:10

> ループの2重化は、接続の受付(accept)とデータの受信の2つですね。 あ、なにか勘違いしてました。接続の受付とデータの受信でそれぞれ待ち状態が発生するってことですね。 同一ループ内に書けば、recvできなくてもacceptを上書きすればいいと思ってましたが、待ち状態になればそもそも処理止まっちゃいますよね・・・。ということは無理ですよね。 ここは二重ループに直します。 > 現状の実装では1人用(データの受信が終わるまで次の接続を受け付けない) > なので、チャットサーバーを実装予定なら listen 数も増やし > 複数の同時接続に対応する必要があるはずです。 listenの引数については必要に応じて増やします。 ドキュメントには -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= サーバーを有効にして、接続を受け付けるようにします。backlog が指定されている場合、少なくとも 0 以上でなければなりません (それより低い場合、0 に設定されます)。システムが新しい接続を拒否するまでに許可する未受付の接続の数を指定します。指定しない場合、デフォルトの妥当な値が選択されます。 バージョン 3.5 で変更: backlog 引数が任意になりました。 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= とあるので、指定可能な最大数はわかりませんが・・・。 例外については、クライアント処理も作って、実際に通信できるかどんなエラーを吐くかちょっと試してみることにします。(多分、想定外のエラーもいっぱい出るんでしょうね・・・。)
teamikl

2021/01/25 05:44 編集

listen については、チャットに同時参加可能な接続数(人数)の事**ではなく** 紛らわしいかなと思い、入れ違いでコメントから削除してしまいました。 接続が同時にあった場合、accept() が呼ばれる迄に待機可能な接続数です。 少人数での運用あれば1でも問題ありません。 参考までに socketserver でのデフォルト値は 5 です。 SOCKET HOW TO https://docs.python.org/ja/3/howto/sockets.html より > 最後に: listen の引数はソケットライブラリに、接続要求を 5 個 (通常の最大値) まで順番待ちさせるように命じている。これ以降の外部接続は拒否するのだが、コードが適切に書かれていれば、それで十分すぎるほどだ。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問