
本記事は、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
最後に
今回はシンプルなサーバプログラムの作成方法について学びました。
簡単に動くプログラムはもっと少ないコーディングで実装できますが、いざ実行してみるとクライアントから送られてくるデータや途中の切断など、思わぬところでエラーが出てしまったりするので、これらにも対応できるように作成していくのは勉強になります。