本記事は、Pythonによるネットワークプログラミングについての学習メモとなります。
参考書籍としてLinuxネットワークプログラミングバイブルを用い、同書の内容に沿ったかたちで、Pythonに書き直しをしていきます。
今回は、前回作成したクライアントプログラムにタイムアウト処理の実装方法について学んでいきます。
ネットワークプログラミングについて
ネットワークプログラミングの目的はデータの送受信をすることで、そのためにソケットというインターフェースを利用します。
このソケットは、TCP/IPの誕生時にBSD Uinux上に実装されたものですが、非常に使い勝手が良かったため、WindowsなどのUnix系以外のOSでも利用されています。
Pythonで実装する際の手順やオプションの設定なども、概ねそのままプログラミングすることができます。
タイムアウト処理とは
クライアントはソケットを生成し、必要な接続先(サーバ)の情報を使いconnect()
で接続しに行きます。
この時、IPアドレスを基にネットワークセグメントへパケットをルーティングし、目的のネットワークセグメントが見つかれば最終的にARPを使い、そのホストのMACアドレスを解決させ、通信を行います。
もし同じネットワークセグメント上のホストであれば、自身のARPテーブルを参照し、そこに無ければ同じネットワークセグメント上の端末全てにブロードキャストし、対象のIPアドレスの端末からの応答を待ちます(異なるネットワークセグメントの場合、これを同セグメントを管理しているルータが行います)。
しかし、もし接続先のホストが存在しない場合、このARPの応答待ちの時間がかなりかかります。
そのため、指定した時間内で応答がなければすぐに終了させたい場合にタイムアウト処理を使うと便利です。
クライアントプログラムの作成
今回は前回作成したクライアントプログラムにタイムアウト処理を実装していきます。
以下がソースコードになります。
# client_timeout.py import select import socket import sys import fcntl import os import errno def set_block(fd, flag): try: flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0) if flag == 0: fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) elif flag == 1: fcntl.fcntl(fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) return 0 except OSError as e: print("set_block:{}".format(e)) return -1 def client_socket_with_timeout(hostnm, portnm, timeout_sec): try: for res in socket.getaddrinfo(hostnm, portnm, socket.AF_INET, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res break except socket.gaieor as e: print("getaddrinfo():{}".format(e)) sys.exit(1) try: nbuf, sbuf = socket.getnameinfo(sa, socket.AI_PASSIVE) except socket.gaieor as e: print("getnameinfo():{}".format(e)) sys.exit(1) print("addr={}".format(nbuf)) print("port={}".format(sbuf)) try: soc = socket.socket(af, socktype, proto) except OSError as e: print("socket:{}".format(e)) sys.exit(1) if (timeout_sec < 0): try: soc.connect(sa) return soc except InterruptedError as e: print("connect:{}".format(e)) return -1 except TimeoutError as e: print("connect:{}".format(e)) return -1 except OSError as e: print("connect:{}".format(e)) return -1 else: try: set_block(soc, 0) soc.connect(sa) set_block(soc, 1) return soc # NOT REACHED except BlockingIOError as e: if e.errno != errno.EINPROGRESS: print("connect:{}".format(e)) soc.close() return -1 width = [0, soc.fileno()] while True: try: r, w, x = select.select(width, [], [], timeout_sec) if len(r) == 0: print("select:timeout") soc.close() return -1 # NOT REACHED else: if soc.fileno() in w or soc.fileno() in r: try: val = soc.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if val == 0: set_block(soc, 1) return soc else: print("getsockopt: ", val) soc.close() return -1 except InterruptedError as e: print("getsockopt") soc.close() return -1 except BlockingIOError as e: if e.errno != errno.EINTR: print("select") soc.close() return -1 def send_recv_loop(soc): buf_size = 512 end = 0 width = [0, soc.fileno()] timeout = 1.0 while True: try: r, w, x = select.select(width, [], [], timeout) except OSError: continue if len(r) == 0: continue for fd in r: if fd == soc.fileno(): try: data = soc.recv(buf_size) except InterruptedError as e: print("recv:{}".format(e)) break if (len(data) == 0): # EOF print("recv:EOF") end = 1 break data = data.rstrip() try: print("> {}".format(data.decode('utf-8'))) except UnicodeDecodeError: pass if fd == 0: try: buf = sys.stdin.readline() except EOFError: end = 1 continue try: soc.send(buf.encode('utf-8')) continue except InterruptedError as e: print("send:{}".format(e)) break if end: break if __name__ == '__main__': if (len(sys.argv) <= 3): print("Usage: {} <server host> <server port> <timeout>".format(sys.argv[0])) sys.exit(1) try: host = sys.argv[1] port = int(sys.argv[2]) timeout = int(sys.argv[3]) soc = client_socket_with_timeout(host, port, timeout) if soc == -1: print("client_socket_with_timeout()") sys.exit(1) send_recv_loop(soc) soc.close() except KeyboardInterrupt: soc.close() sys.exit(1) except ConnectionRefusedError as e: print(e) sys.exit(1) except ConnectionResetError as e: print(e) sys.exit(1)
動作確認
それでは、上記プログラムを実行してみます。
今回は参考書籍を基に、以下の状況でのパターンで実行します。
それでは順番に実行してみます。
存在しないホスト & 異なるセグメント & タイムアウトなし
>/usr/bin/time -p python3 client_timeout.py 192.168.111.111 8000 -1 addr=192.168.111.111 port=8000 connect:[Errno 110] Connection timed out client_socket_with_timeout() Command exited with non-zero status 1 real 127.42 user 0.08 sys 0.00
この場合、127秒でタイムアウトになりました。
存在しないホスト & 同一セグメント & タイムアウトなし
>/usr/bin/time -p python3 client_timeout.py 10.0.2.1 8888 -1 addr=10.0.2.1 port=8888 connect:[Errno 113] No route to host client_socket_with_timeout() Command exited with non-zero status 1 real 3.10 user 0.08 sys 0.01
この場合、3秒でタイムアウトになりました。
存在しないホスト & 異なるセグメント & タイムアウトあり
>/usr/bin/time -p python3 client_timeout.py 192.168.111.111 8000 5 addr=192.168.111.111 port=8000 select:timeout Command exited with non-zero status 1 real 5.10 user 0.06 sys 0.02
この場合、指定した5秒でタイムアウトになりました。
最後に
今回はクライアントが接続しに行く際のタイムアウト処理について学びました。
このような知識を学んだことで、異常時の動作にも十分対応できるプログラムに少し進化をさせることができました。