Engineering Note

プログラミングなどの技術的なメモ

クライアントのタイムアウト処理(Pythonによるネットワークプログラミング)

client

本記事は、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)

 

動作確認

それでは、上記プログラムを実行してみます。

今回は参考書籍を基に、以下の状況でのパターンで実行します。

 

  1. 存在しないホスト & 異なるセグメント & タイムアウトなし
  2. 存在しないホスト & 同一セグメント & タイムアウトなし
  3. 存在しないホスト & 異なるセグメント & タイムアウトあり

 

それでは順番に実行してみます。

 

存在しないホスト & 異なるセグメント & タイムアウトなし

 

 >/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秒でタイムアウトになりました。

 

最後に

今回はクライアントが接続しに行く際のタイムアウト処理について学びました。

このような知識を学んだことで、異常時の動作にも十分対応できるプログラムに少し進化をさせることができました。

 

参考書籍

Linuxネットワークプログラミングバイブル