Engineering Note

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

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

client

本記事は、Pythonによるネットワークプログラミングについての学習メモとなります。

参考書籍としてLinuxネットワークプログラミングバイブルを用い、同書の内容に沿ったかたちで、Pythonに書き直しをしていきます。

今回は、サーバソケットのマルチクライアント化(多重化)の方法として、プリフォークを利用した方法について学んでいきます。

 

 

ネットワークプログラミングについて

ネットワークプログラミングの目的はデータの送受信をすることで、そのためにソケットというインターフェースを利用します。

このソケットは、TCP/IPの誕生時にBSD Uinux上に実装されたものですが、非常に使い勝手が良かったため、WindowsなどのUnix系以外のOSでも利用されています。

Pythonで実装する際の手順やオプションの設定なども、概ねそのままプログラミングすることができます。

 

多重化とは

通常のサーバソケットでは、accept()(もしくはrecv())を呼び出した後、それ以降の処理をブロックしてしまいます(ブロッキングIO)。

そのため、他のコネクションが確立したソケットは、その処理が完了するまで待機させられ、クライアントごとの同時接続ができない使い勝手の悪いアプリケーションになってしまいます。

これらの問題を解決するためには、多重化と呼ばれる技術を使い、マルチクライアント化に対応する必要があります。

 

プリフォークによるマルチクライアント化

 

前回はfork()を用いた多重化について学びました。

 


前回ではaccept()後にfork()を実行し、その都度子プロセスを生成しましたが、処理的には負荷がかかります。

これを回避するために、予め子プロセスを生成しておくプリフォークと呼ばれる方法があります。

ここでは、listen()におけるBacklog分はTCP3ウェイハンドシェイクを済ませて置き、lockf()を利用した排他制御により、空きができるまでクライアントを放置させます。 

なお、ファイルロックにはflock()とfcntl()があり、前者はBSD、後者はSystem Vによるもので、lockf()はfcntl()を内部で呼び出しています。

 

サーバプログラムの作成 

それでは、Pythonのfcntlモジュールを利用したファイルロックを行うコードを作成します。

なお、本来はロックファイルを生成した後にunlink()(remove()と等価)を実行し、見かけ上はファイルを削除しますが、Pythonで実行するとエラーとなってしまうため、割愛します。

 

以下がソースコードになります。

 

# server_multi_lockf.py
import socket
import sys
import errno
import os
import fcntl
import time

NUM_CHILD = 2
LOCK_FILE = "server.lock"
g_lock_fd = -1

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):
    try:
        while True:
            print("<{}>start getting lock".format(os.getpid()))
            fcntl.lockf(g_lock_fd, fcntl.LOCK_SH)
            print("<{}>got a lock!".format(os.getpid()))

            acc, addr = soc.accept()
            hbuf, sbuf = socket.getnameinfo(addr, socket.NI_NUMERICHOST | socket.NI_NUMERICSERV)
            print("accept:{}:{}".format(hbuf, sbuf))
            print("<{}>unlock".format(os.getpid()))
            fcntl.lockf(g_lock_fd, fcntl.LOCK_UN)
            send_recv(acc) 
            acc.close()                           
    except InterruptedError as e:
        if e.errno != errno.EINTR:
            print("accept:{}".format(e))
        fcntl.flock(g_lock_fd, fcntl.LOCK_UN)
    except OSError as e:
        print("lockf:{}".format(e))
        sys.exit(1)

def send_recv(acc):
    buf_size = 512
    id = os.getpid()
    while True:
        try:
            data = acc.recv(buf_size)
        except InterruptedError as e:
            print("recv:{}".format(e))
            break 

        if (len(data) == 0):
            # EOF
            print("[{}]recv:EOF".format(id))
            break

        data = data.rstrip()
        try:
            print("[{}]{}".format(id, data.decode('utf-8')))
        except UnicodeDecodeError:
            pass
        try:
            acc.send(data + b':OK\r\n')
        except InterruptedError as e:
            print("send:{}".format(e))
            break
    acc.close()

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])
    
    g_lock_fd = os.open(LOCK_FILE, os.O_RDWR | os.O_CREAT, 0o666)
    # os.unlink(LOCK_FILE)
    print("start {} children".format(NUM_CHILD))
    try:
        for i in range(NUM_CHILD):    
            pid = os.fork()
            if pid == 0:
                accept_loop(soc)
                
            elif pid > 0:
                pass
        print("ready for accept")
        while True:
            time.sleep(10)
            # print("<{}>lock status:{}".format(os.getpid(), fcntl.fcntl(g_lock_fd, os.F_TEST, 0))

    except OSError as e:
        print(e)
        sys.exit(1)
    except KeyboardInterrupt:
        sys.exit(1)
    except Exception as e:
        print(e)
        sys.exit(1)  
    finally:
        soc.close()
        os.close(g_lock_fd)

 

動作確認

それでは、上記プログラムを実行してみます。

 

 > python3 server_multi_lock.py 8888
 port=8888
 start 2 children
 ready for accept
 <2800>start getting lock
 <2800>got a lock!
 <2799>start getting lock
 <2799>got a lock!
 accept:127.0.0.1:44069
 <2800>unlock
 accept:127.0.0.1:44070
 <2799>unlock
 [2800]ok
 [2799]ok
 [2799]recv:EOF
 <2799>start getting lock
 <2799>got a lock!
 accept:127.0.0.1:44071
 <2799>unlock
 [2799]ok
 [2799]recv:EOF
 <2799>start getting lock
 <2799>got a lock!
 [2800]recv:EOF
 <2800>start getting lock
 <2800>got a lock!

 

最後に

今回はサーバのマルチクライアント化として、プリフォークによる方法とファイルロックによる排他制御について学びました。

次回はプリスレッドによる方法について学んでいきます。

 

参考書籍

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

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