Engineering Note

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

クライアントプログラムの作成(Pythonによるネットワークプログラミング)

client

本記事は、Pythonによるネットワークプログラミングについての学習メモとなります。

参考書籍としてLinuxネットワークプログラミングバイブルを用い、同書の内容に沿ったかたちで、Pythonに書き直しをしていきます。

今回は、シンプルなクライアントプログラムの作成方法について学んでいきます。

 

 

ネットワークプログラミングについて

ネットワークプログラミングの目的はデータの送受信をすることで、そのためにソケットというインターフェースを利用します。

このソケットは、TCP/IPの誕生時にBSD Uinux上に実装されたものですが、非常に使い勝手が良かったため、WindowsなどのUnix系以外のOSでも利用されています。

Pythonで実装する際の手順やオプションの設定なども、概ねそのままプログラミングすることができます。

 

クライアントプログラムの作成

前回TelnetやNetcatなどでデータ送受信をするようなシンプルなサーバプログラムを作成しました。

今回はシンプルなクライアントプログラムを作成します。

流れとしては、サーバとのコネクションを作成したら、クライアント側の標準入力から文字列を読み込み、それをサーバに送信します。

サーバ側では、前回作成したプログラムをそのまま利用しますので、クライアントから送られてきたデータに対して、「:OK\r\n」を付与して送り返します。

しかし、クライアント側では、socket.recv()stdin.readline()などの関数が実行されると、読み込まれるデータがない場合に処理をブロックしてしまいます。

これを解決するために多重化と呼ばれる方法を用いて処理をしていきます。

 

以下がソースコードになります。

 

# client.py
import select
import socket
import sys

def client_socket(hostnm, portnm):
  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)
    
  try:
    soc.connect(sa)
  except InterruptedError as e:
    print("connect:{}".format(e))
    sys.exit(1)
    
  return soc 
    
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):
          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>".format(sys.argv[0]))
    sys.exit(1)
  try:
    host = sys.argv[1]
    port = int(sys.argv[2])
    soc = client_socket(host, port)
    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)

  

アドレス情報を取得する

まずはサーバのアドレス情報を取得します。

これにはsocket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)を利用します。

第1引数のhostには、接続するサーバのIPアドレスやホスト名を指定し、第2引数のportには接続するサーバのポート番号やサービス名を指定します。

そして戻り値として、(family, type, proto, canonname, sockaddr)がタプルとして返ってきます。

また、socket.getnameinfo(sockaddr, flags)では、上記で取得したsockaddrを第一引数として、ホストとポート番号をタプルで取得します。

 

ソケットの作成

ソケットを作成するにはsocket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)を利用します。

引数には、getaddrinfo()で取得した情報をそのまま利用します。

 

次にsocket.connect(address)で対象サーバに接続をします。

問題がなければTCP3ウェイ・ハンドシェイクが行われ、サーバのキューに追加されます。

 

データ送受信

データ送受信に関しては、基本的に前回作成したサーバプログラムを同じですが、前述のように多重化という方法を用いて、標準入力とソケットが読み込み可能であるかをチェックし、これにはselect.select(rlist, wlist, xlist[, timeout])を利用します。

rlistで読み込み可能になるまで待機するオブジェクトを指定し、これには整数値のファイル記述子かfileno()のメソッドを持つオブジェクトをリストにして指定します。

なお、ここでは標準入力の0とソケットオブジェクトのファイル記述子(FD)を指定していますが、Windowsの場合、ソケット以外(WinSockによって生成されたFD以外)のオブジェクトはselect()で扱えないので、標準入力を指定するとエラーが出ます。

 

それでは、実際に動作確認をしてみます。

 

動作確認

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

まずサーバ側で前回作成したプログラムを実行し、クライアントからの接続を待ちます。

 

 # server側
 $ python3 server.py 55555
 port=55555
 ready for accept

 

クライアント側で以下コマンドで接続をします。

 # client側
 $ python3 client.py 127.0.0.1 55555
 addr=127.0.0.1
 port=55555
 hello
 > hello:OK
 ^C

 

サーバ側では以下のデータが受信できました。

 # server側
 accept:127.0.0.1:43004
 [client]hello
 recv:EOF

 

最後に

今回はシンプルなクライアントプログラムの作成方法について学びました。

なお、サーバ側のプログラムに関しては、複数のクライアントからの受付を同時に処理することができませんが、今回クライアント側で実装した多重化をサーバ側でも実装すれば、複数のクライアントからの受付もほぼ同時に処理することができるので、今後の課題としていきたいと思います。 

 

参考書籍

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