計算機サーバーを作ってみる

次は、サーバー側の作成手順です。クライアントから計算式(例: 2+3)を受け取り、計算結果を返すTCPサーバーを作成します。

サーバーの仕様

  • クライアントから1行の計算式(例: 2+3)を受信

  • 受信した式を評価し、結果を返す

  • "quit"が送られてきたら接続を終了

サーバーのPythonコード

ファイル名はcalc_server.pyとします。

 1import socket
 2import ast # 現在は不要でした(削除可能)
 3
 4HOST = '127.0.0.1'  # ローカルホスト
 5PORT = 10000        # 任意の空いているポート番号
 6
 7def handle_client(conn, addr):
 8    print(f"[server] Connected by {addr}")
 9    # ソケットをファイルのように扱うため、makefile()でファイルオブジェクトを作成
10    # 'rw'は読み書き両用、encoding='utf-8'で送受信する文字列のエンコーディングを指定
11    # newline='\n'は、行末をLFに統一
12    with conn.makefile('rw', encoding='utf-8', newline='\n') as f:
13        while True:
14            # 5. f.readline()で一行受信 (read)
15            expr = f.readline().strip()
16            if not expr:
17                print("[server] Connection closed (EOF)")
18                break
19            if expr == "quit":
20                print("[server] Connection closed by client")
21                break
22            try:
23                result = str(eval(expr, {"__builtins__": {}}))
24            except Exception as e:
25                result = f"Error: {e}"
26            # 6. f.write()で一行送信 (write)
27            f.write(result + '\n')
28            # バッファをフラッシュして、データを即座に送信
29            f.flush()
30
31def run_server():
32    # 1. ソケットの作成
33    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
34    # サーバーを閉じた後、すぐに同じポートで再起動できるように設定
35    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
36    try:
37        # 2. bind()で、作成したソケットにIPアドレスとポート番号を割り当てる
38        sock.bind((HOST, PORT))
39        # 3. listen()で、クライアントからの接続を待ち受ける状態に移行
40        sock.listen()
41        print(f"[server] Listening on {HOST}:{PORT}")
42        while True:
43            # 4. accept()で、クライアントからの接続要求を受け入れ、
44            #    新しいソケット(conn)とクライアントのアドレス(addr)を取得
45            conn, addr = sock.accept()
46            with conn:
47                handle_client(conn, addr)
48    finally:
49        print("[server] Shutting down")
50        sock.close()
51
52if __name__ == '__main__':
53    run_server()

コードの解説

このサーバープログラムは、TCPサーバーの基本的な要素で構成されています。一つずつ見ていきましょう。

ソケットの作成 (socket.socket)

32    # 1. ソケットの作成
33    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

socket.socket(socket.AF_INET, socket.SOCK_STREAM) で、IPv4 (AF_INET) を使うTCP (SOCK_STREAM) ソケットを作成します。これが、ネットワーク通信の出入り口になります。

setsockopt()は、サーバーを終了してすぐに再起動した際に、同じポートをすぐ使えるようにするためのおまじないです。

アドレスとポートの割り当て (bind)

37        # 2. bind()で、作成したソケットにIPアドレスとポート番号を割り当てる
38        sock.bind((HOST, PORT))

sock.bind((HOST, PORT)) で、作成したソケットに特定のIPアドレス (HOST) とポート番号 (PORT) を結びつけます。これにより、クライアントはこのアドレスとポートを目がけて接続に来ることができます。

接続待機 (listen)

39        # 3. listen()で、クライアントからの接続を待ち受ける状態に移行
40        sock.listen()

sock.listen() で、ソケットを「待機モード」にします。これにより、クライアントからの接続要求を受け付けられるようになります。

接続の受け入れ (accept)

43            # 4. accept()で、クライアントからの接続要求を受け入れ、
44            #    新しいソケット(conn)とクライアントのアドレス(addr)を取得
45            conn, addr = sock.accept()

sock.accept() は、クライアントからの接続要求を待ち、実際に接続が確立されると、新しいソケットオブジェクト (conn) とクライアントのアドレス (addr) を返します。

accept()ブロッキング呼び出しであり、クライアントが接続してくるまでプログラムの実行はここで停止します。以降のクライアントとの通信は、この新しいソケット conn を通じて行われます。

ファイルオブジェクトによる送受信 (makefile)

 9    # ソケットをファイルのように扱うため、makefile()でファイルオブジェクトを作成
10    # 'rw'は読み書き両用、encoding='utf-8'で送受信する文字列のエンコーディングを指定
11    # newline='\n'は、行末をLFに統一
12    with conn.makefile('rw', encoding='utf-8', newline='\n') as f:

handle_client関数内でconn.makefile()を使うと、ソケットをファイルのように扱うことができます。 以降のソケットに対する読み書きは、ファイルオブジェクトと同様のread()メソッドやwrite()メソッドなどで行えるようになります。

  • conn.makefile('rw', encoding='utf-8', newline='\n') as f:

    • 接続済みのソケットconnから、ファイルオブジェクトfを作成します。

    • 'rw'は読み書き両用モードを意味します。

    • encoding='utf-8'を指定することで、送受信時に自動でUTF-8へのエンコード・デコードが行われ、encode()decode()を呼び出す手間が省けます。

    • newline='\n'は、異なるOS間でも改行コードを\nとして扱うための設定です。

これにより、データの送受信は後述のように、使い慣れたファイル操作のメソッドで行えます。

データの読み取り・書き込みについて

ここまでくると、ソケットはファイルの形でのも読み書きが普通に行えるようになります。

14            # 5. f.readline()で一行受信 (read)
15            expr = f.readline().strip()
26            # 6. f.write()で一行送信 (write)
27            f.write(result + '\n')
28            # バッファをフラッシュして、データを即座に送信
29            f.flush()
  • 受信: f.readline()

    • クライアントから送られてくるデータを1行ずつ読み込みます。

  • 送信: f.write(result + '\n')

    • 計算結果の文字列の末尾に改行を付けて書き込みます。クライアント側がreadline()で待っているため、改行を送ることが重要です。

また、クライアントでの動作と同じく、バッファリングされているデータを書き出すためにflush()も使っていることに注意してください。

接続の終了

  • 12行目、46行目にてwith構文を使っています。これによりブロックを抜けるときに自動的にf.close()conn.close()が呼ばれ、クライアントとの接続が安全に閉じられます。

  • サーバー全体を終了するとき(例えばCtrl+Cを押したとき)は、finallyブロックでsock.close()が呼ばれ、待機用のソケットも確実に閉じられます。

閉じ忘れていても、プロセスの終了後に適当なタイミングでクローズが行われますが、明示的にクローズすること以下のメリットが得られます。

  • リソースの解放: ソケットを閉じることで、システムリソースが解放されます。

  • クライアントに終了を通知: クライアント側は、サーバーが接続を閉じたことを認識できます(いきなり着られることはうれしくないとは思いますが)。

以上で、基本的なサーバーの実装になっています。