本記事は、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)
で対象サーバに接続をします。
問題がなければTCPの3ウェイ・ハンドシェイクが行われ、サーバのキューに追加されます。
データ送受信
データ送受信に関しては、基本的に前回作成したサーバプログラムを同じですが、前述のように多重化という方法を用いて、標準入力とソケットが読み込み可能であるかをチェックし、これには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
最後に
今回はシンプルなクライアントプログラムの作成方法について学びました。
なお、サーバ側のプログラムに関しては、複数のクライアントからの受付を同時に処理することができませんが、今回クライアント側で実装した多重化をサーバ側でも実装すれば、複数のクライアントからの受付もほぼ同時に処理することができるので、今後の課題としていきたいと思います。