本記事は、Pythonによるネットワークプログラミングについての学習メモとなります。
参考書籍としてLinuxネットワークプログラミングバイブルを用い、同書の内容に沿ったかたちで、Pythonに書き直しをしていきます。
今回は、サーバソケットのマルチクライアント化(多重化)の方法として、poll()を利用した方法について学んでいきます。
ネットワークプログラミングについて
ネットワークプログラミングの目的はデータの送受信をすることで、そのためにソケットというインターフェースを利用します。
このソケットは、TCP/IPの誕生時にBSD Uinux上に実装されたものですが、非常に使い勝手が良かったため、WindowsなどのUnix系以外のOSでも利用されています。
Pythonで実装する際の手順やオプションの設定なども、概ねそのままプログラミングすることができます。
多重化とは
通常のサーバソケットでは、accept()(もしくはrecv())を呼び出した後、それ以降の処理をブロックしてしまいます(ブロッキングIO)。
そのため、他のコネクションが確立したソケットは、その処理が完了するまで待機させられ、クライアントごとの同時接続ができない使い勝手の悪いアプリケーションになってしまいます。
これらの問題を解決するためには、多重化と呼ばれる技術を使い、マルチクライアント化に対応する必要があります。
poll()によるマルチクライアント化
前回はselect()を用いた多重化について学びました。
今回のpoll()は、select()と同じ事をするシステムコールです。
select()の場合、FD_SETSIZEという定数でFD_SETに格納できるディスクリプタ数が制限されますが、poll()は配列で宣言ができるため、このようなOSごとの制限がなくなります。
Pythonのselectモジュールの場合は、select.poll()でオブジェクトを生成した後に、poll.register(fd[, eventmask])でファイルディスクリプタとIOイベントを登録しなければなりません。
監視するIOイベント情報の詳細は以下になります。
定数 |
意味 |
---|---|
|
読み出し可能なデータが存在する |
|
緊急の読み出し可能なデータが存在する |
|
書き出しの準備ができている: 書き出し処理がブロックしない |
|
何らかのエラー状態 |
|
ハングアップ |
|
ストリームソケットの他端が接続を切断したか、接続の書き込み側のシャットダウンを行った。 |
|
無効な要求: 記述子が開かれていない |
https://docs.python.org/ja/3/library/select.htmlより抜粋
登録したファイルディスクリプタを削除する場合はpoll.unregister(fd)で行い、ポーリングを実行する場合はpoll.poll([timeout])で行います。
なお、タイムアウト値はミリ秒単位で指定をします。
サーバプログラムの作成
今回は前回作成したサーバプログラムにpoll()によるマルチクライアント機能を実装していきます。
以下がソースコードになります。
# server_multi_poll.py import socket import sys import fcntl import os import errno import select MAX_CHILD = 20 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): child = [None for i in range(MAX_CHILD)] timeout = 10 * 1000 targets = select.poll() while True: try: targets.register(soc, select.POLLIN) count = 1 for c in child: if c != None: targets.register(c, select.POLLIN) count += 1 print("<<child count:{}>>".format(count-1)) try: r = targets.poll(timeout) except OSError as e: print("poll:{}".format(e)) break if not r: print("poll:timeout") else: for fd, event in r: if fd == soc.fileno() and event & select.POLLIN: acc, addr = soc.accept() hbuf, sbuf = socket.getnameinfo(addr, socket.NI_NUMERICHOST | socket.NI_NUMERICSERV) print("accept:{}:{}".format(hbuf, sbuf)) pos = None for i, c in enumerate(child): if c == None: pos = i break if pos == None: print("child is full: cannot accept") acc.close() if pos != None: child[pos] = acc elif event & (select.POLLIN | select.POLLERR): for i, c in enumerate(child): if c != None and c.fileno() == fd: if send_recv(c, i) == -1: targets.unregister(c) c.close() child[i] = None except InterruptedError as e: if e.errno != errno.EINTR: print("accept:{}".format(e)) def send_recv(acc, child_no): buf_size = 512 try: data = acc.recv(buf_size) except InterruptedError as e: print("recv:{}".format(e)) return -1 if (len(data) == 0): # EOF print("[child{}]recv:EOF".format(child_no)) return -1 data = data.rstrip() try: print("[child{}]{}".format(child_no, data.decode('utf-8'))) except UnicodeDecodeError: pass try: acc.send(data + b':OK\r\n') except InterruptedError as e: print("send:{}".format(e)) return -1 return 0 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)
動作確認
それでは、上記プログラムを実行してみます。
> python3 server_multi_poll.py 8888 port=8888 ready for accept <<child count:0>> accept:127.0.0.1:51588 # クライアント1接続 <<child count:1>> [child0]ok <<child count:1>> accept:127.0.0.1:51589 # クライアント2接続 <<child count:2>> [child1]ok <<child count:2>> [child1]recv:EOF # クライアント1切断 <<child count:1>> [child0]recv:EOF # クライアント2切断 <<child count:0>>
最後に
今回はサーバのマルチクライアント化として、poll()を利用した方法について学びました。
次回はepoll()を利用したマルチクライアント化について学んでいきます。