Engineering Note

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

サーバプログラムの作成(Pythonによるネットワークプログラミング)

server

本記事は、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が完了した状態でクライアントから接続が来た場合、TCP3ウェイ・ハンドシェイクが行われ、キューに追加されます。

 

データ送受信

クライアントから接続が来たら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

 

最後に

今回はシンプルなサーバプログラムの作成方法について学びました。

簡単に動くプログラムはもっと少ないコーディングで実装できますが、いざ実行してみるとクライアントから送られてくるデータや途中の切断など、思わぬところでエラーが出てしまったりするので、これらにも対応できるように作成していくのは勉強になります。

 

参考書籍