サーバプログラムの作成(Pythonによるネットワークプログラミング)
本記事は、Pythonによるネットワークプログラミングについての学習メモとなります。
参考書籍としてLinuxネットワークプログラミングバイブルを用い、同書の内容に沿ったかたちで、Pythonに書き直しをしていきます。
今回は、シンプルなサーバプログラムの作成方法について学んでいきます。
ネットワークプログラミングについて
ネットワークプログラミングの目的はデータの送受信をすることで、そのためにソケットというインターフェースを利用します。
このソケットは、TCP/IPの誕生時にBSD Uinux上に実装されたものですが、非常に使い勝手が良かったため、WindowsなどのUnix系以外のOSでも利用されています。
Pythonで実装する際の手順やオプションの設定なども、概ねそのままプログラミングすることができます。
サーバプログラムの作成
今回はTelnetやNetcatなどでデータ送受信をするようなシンプルなサーバプログラムを作成します。
内容は単純なエコーサーバとなり、クライアントが送信してきたデータに「:OK\r\n」を付与して送り返します。
以下がソースコードになります。
# server.py import socket import sys def server_socket(portnum): try: for res in socket.getaddrinfo(None, portnum, socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_PASSIVE): 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("port={}".format(sbuf)) try: soc = socket.socket(af, socktype, proto) except OSError as e: print("socket:{}".format(e)) sys.exit(1) try: soc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) except OSError as e: print("setsockopt:{}".format(e)) sys.exit(1) try: soc.bind(sa) except OSError as e: print("bind:{}".format(e)) soc.close() sys.exit(1) try: soc.listen(socket.SOMAXCONN) except OSError as e: print("listen:{}".format(e)) soc.close() sys.exit(1) return soc def accept_loop(soc): while True: try: acc, addr = soc.accept() except InterruptedError as e: print("accept:{}".format(e)) continue print("accept:{}:{}".format(addr[0], addr[1])) send_recv_loop(acc) acc.close() def send_recv_loop(acc): buf_size = 512 while True: try: data = acc.recv(buf_size) except InterruptedError as e: print("recv:{}".format(e)) break if (len(data) == 0): print("recv:EOF") break data = data.rstrip() try: print("[client]{}".format(data.decode('utf-8'))) except UnicodeDecodeError: pass try: acc.send(data + b':OK\r\n') except InterruptedError as e: print("send:{}".format(e)) break if __name__ == '__main__': if (len(sys.argv) != 2): print("Usage: {} <server port>".format(sys.argv[0])) sys.exit(1) soc = server_socket(sys.argv[1]) print("ready for accept") try: accept_loop(soc) soc.close() except KeyboardInterrupt: soc.close() sys.exit(1)
今回は参考書籍に倣って、細かいところでエラー処理を入れており、どこでエラーが発生したかを分かりやすくしています。
それでは、内容について詳しく見ていきます。
アドレス情報を取得する
まずはサーバで利用できるアドレス情報の確認をします。
これにはsocket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)
を利用します。
第1引数のhostには、利用するIPアドレスやホスト名を指定します。
ここでNoneを指定すると、NULLとしてC APIに渡され、利用できるすべてのインターフェースに割り当てられているIPアドレスを対象とします。
なお、family, type, proto
を指定するとアドレスリストを絞り込むことができます。
そして、(family, type, proto, canonname, sockaddr)
がタプルとして返ってきます。
また、socket.getnameinfo(sockaddr, flags)
では、上記で取得したsockaddrを第一引数として、ホストとポート番号をタプルで取得します。
ソケットの作成
ソケットを作成するにはsocket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
を利用します。
引数には、getaddrinfo()
で取得した情報をそのまま利用します。
またsocket.setsockopt(level, optname, value: int)
を利用して再利用フラグを立てます。
これはクライアントとの切断が中途半端であったり、並列処理などで別のクライアントから接続が来た際、通常は同じIPアドレスとポート番号では1度しかbindできませんが、このフラグを立てれば、同じIPアドレスとポート番号の組み合わせでもbindできるようになります。
ソケットを作成し、オプションの設定が完了したらIPアドレスとポート番号をsocket.bind(address)
でbindさせます。
ここで、引数のIPアドレスとポート番号はタプルで渡します。
無事bindが完了したらsocket.listen([backlog])
で接続の受付を開始します。
backlogとは、接続待ちをするキューの数となります。
socket.SOMAXCONN
には、そのシステムの最大値(自環境の場合2147483647)が指定されます。
listenが完了した状態でクライアントから接続が来た場合、TCPの3ウェイ・ハンドシェイクが行われ、キューに追加されます。
データ送受信
クライアントから接続が来たらsocket.accept()
でキューから先頭の接続要求を一つ取り出し、ここではaccept_loop()
で行います。
accept()
の返り値は(conn, address)
のタプルとなり、connはデータの送受信を行うための新しいソケットオブジェクトになります。
実際のデータ送受信はsend_recv_loop()
で行い、socket.recv(bufsize[, flags])
でソケットオブジェクトからbytesオブジェクトのデータを取り出します。
またデータの送信にはsocket.send(bytes[, flags])
を利用し、bytesオブジェクトのデータを引数として渡します。
それでは、実際に動作確認をしてみます。
動作確認
それでは、上記プログラムをVagrant上のUbuntuで実行してみます。
まずサーバ側で以下を実行し、クライアントからの接続を待ちます。
# server側 $ python3 server.py 55555 port=55555 ready for accept
クライアントからTelnetコマンドで接続をします。
# client側 $ telnet 127.0.0.1 55555 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. hello hello:OK ^] telnet> quit Connection closed.
サーバ側では以下のデータが受信できました。
# server側 accept:127.0.0.1:55127 [client]hello recv:EOF
最後に
今回はシンプルなサーバプログラムの作成方法について学びました。
簡単に動くプログラムはもっと少ないコーディングで実装できますが、いざ実行してみるとクライアントから送られてくるデータや途中の切断など、思わぬところでエラーが出てしまったりするので、これらにも対応できるように作成していくのは勉強になります。