Engineering Note

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

サーバソケットの多重化(poll())(Pythonによるネットワークプログラミング)

client

本記事は、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イベント情報の詳細は以下になります。

 

定数

意味

POLLIN

読み出し可能なデータが存在する

POLLPRI

緊急の読み出し可能なデータが存在する

POLLOUT

書き出しの準備ができている: 書き出し処理がブロックしない

POLLERR

何らかのエラー状態

POLLHUP

ハングアップ

POLLRDHUP

ストリームソケットの他端が接続を切断したか、接続の書き込み側のシャットダウンを行った。

POLLNVAL

無効な要求: 記述子が開かれていない

                   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()を利用したマルチクライアント化について学んでいきます。

 

参考書籍

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

TCP/IPソケットプログラミング C言語編