計算機クライアントを作ってみる

次は、クライアント側の作成手順です。サーバーに計算式を送り、結果を受け取るTCPクライアントを作成します。

クライアントの仕様

  • ユーザーから計算式(例: 2+3)を入力

  • サーバーに送信し、結果を受信して表示

  • "quit"と入力すると終了

クライアントのPythonコード

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

 1import socket
 2
 3HOST = '127.0.0.1'  # サーバーのアドレス
 4PORT = 10000        # サーバーのポート番号
 5
 6with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
 7    sock.connect((HOST, PORT))
 8    print(f"[client] Connected to {HOST}:{PORT}")
 9    while True:
10        expr = input("計算式を入力してください(例: 2+3, quitで終了): ")
11        sock_file = sock.makefile('rwb')
12        sock_file.write((expr + '\n').encode())
13        sock_file.flush()
14        if expr == "quit":
15            print("[client] 終了します")
16            break
17        data = sock_file.readline()
18        print(f"[client] 結果: {data.decode()}")

接続について

接続に必要となるソケットは、UDP版と同じくsocket.socket()で作成します。 TCPのため、以下の設定でソケットを作成しています。

  • socket.AF_INET - IPv4アドレスファミリーを使用

  • socket.SOCK_STREAM - ストリームソケットを使用

注釈

ストリームソケットを利用することで、TCPという扱いになっています。

今回のコードでは、with文を使ってソケットを自動的に閉じるようにしています。

6with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:

コネクションの確立

TCPでは、UDPと異なり明確に『接続』が求められます。 そのため、接続に対して送りつける(sendto())ではなく、connect()を使います。

7    sock.connect((HOST, PORT))

socket.connect()はいわゆる『ブロッキング』という動作です。 これは、相手との接続が確立されるまで、プログラムが待機することを意味します。 TCPでの接続は、いわゆる3ウェイハンドシェイクと呼ばれる手順を経て行われます。

もちろん、そもそも相手のポートでの受信ができていない状態(すなわち「サーバーが起動していない」状態)もありえます。 この場合は『接続以前の問題』としてエラーによる終了となります。

よって、以降は接続が確立した状態を前提として進めることになります。

計算式の送信(サーバーへの送信)

接続後、ユーザーからの入力を受け付けてサーバーへ送信します。

1    while True:
2        expr = input("計算式を入力してください(例: 2+3, quitで終了): ")
3        sock_file = sock.makefile('rwb')
4        sock_file.write((expr + '\n').encode())
5        sock_file.flush()
6        if expr == "quit":
7            print("[client] 終了します")
8            break

ソケットはファイルに見える

TCPのソケットは、ストリームと呼ばれている構造となっています。 これは、情報がそれぞれの方向に流れている状況です。

  • 一方の書き込みが他方の読み込みに繋がっている

  • 反対側も同様である

        graph TD
    subgraph クライアント
        direction LR
        C_WRITE(write)
        C_READ(read)
    end
    subgraph サーバー
        direction LR
        S_WRITE(write)
        S_READ(read)
    end

    C_WRITE --> S_READ
    S_WRITE --> C_READ
    

この図のように、TCPソケットは2つの独立したストリーム(流れ)が対になっていると考えることができます。

  • クライアントからサーバーへのストリーム(クライアントのwriteがサーバーのreadに繋がる)

  • サーバーからクライアントへのストリーム(サーバーのwriteがクライアントのreadに繋がる)

この仕様は、ソケットがファイルのように見えるということで『プログラマーにとってはソケットも普通のファイルのように操作できる』つくりになっています[1]

ここではファイルと同様であるということで、Pythonの流儀からは少し外れますがファイルに変換して読み書きをする方法で書いています。

 9    while True:
10        expr = input("計算式を入力してください(例: 2+3, quitで終了): ")
11        sock_file = sock.makefile('rwb')
12        sock_file.write((expr + '\n').encode())
13        sock_file.flush()
14        if expr == "quit":
15            print("[client] 終了します")
16            break
17        data = sock_file.readline()
18        print(f"[client] 結果: {data.decode()}")
  • 11行目でソケットから本来のファイルに相当するオブジェクトを取得しています(Python的には通常は不要ですが)。

  • 12行目で、write()により送信を行います。

  • ですが、write()はバッファリングされるため、すぐに送信されるわけではありません。よって13行目のflush()でバッファをフラッシュして、すぐに送信されるようにしています。

  • 17行目で逆に1行分の読み込みを行っています(readline())。

このように、ソケットをファイルのように扱うことで、Pythonの標準的なファイル操作と同じ感覚でTCP通信を行うことができます。

とりあえずクライアントのコードはここまでにして、続いてサーバー側の実装を考えてみましょう。