実際に使ってみると…

では、実装したTCPプログラムを実際に使ってみましょう。

端末を2つ起動しください。1つはサーバー用、もう1つはクライアント用です。

各部の起動

サーバーを起動する端末で以下のコマンドを実行します。

# サーバー側
uv run calc_server.py

これで待機状態になります(プロンプトが戻ってこず、実行中という状況になる)。 ちなみに止まっているのは、listenが実行されている部分です。

次に、クライアントを起動する端末で以下のコマンドを実行します。

# クライアント側
uv run calc_client.py

これでクライアントが起動します。 この時にサーバー側の端末では、接続されたことがわかるように出力を入れております。

# サーバー側
[server] Connected by ('127.0.0.1', 41412)

そしてクライアント側で適当な数値を書き込むと、サーバー側で演算が行われて結果が得られます。

# クライアント側
2+3 # 入力して⏎
5

Python側の計算式がそのまま使えるため、累乗(**)も実行できます。ただしあまり大きなものはしない方がいいでしょう。

動作が確認できたら、一度接続を正しく止めてみましょう。 今回の実装では、quitと入力すると接続が終了します。

# クライアント側
quit # 入力して⏎

サーバー側では、切断されたことが出力されています。

# サーバー側
[server] Connection closed by client

これでサーバーとクライアントの基本的な動作が確認できました。 サーバー側も一度止めておきたいので、Ctrl+Cでサーバーを終了してください。

接続の様子を見てみる

TCPでの接続は、明確に接続していないと失敗します。 ということは、UDP版と異なり、サーバー側が起動して待ち受け(listen)していないと、クライアント側は接続できません。 ということで、サーバー側を起動せずクライアント側でサーバーに接続しようとすると、以下のようなエラーが出ます。

# クライアント側
uv run calc_client.py
...(いろいろ出力がありますが省略)...
ConnectionRefusedError: [Errno 111] Connection refused

では、サーバー側を起動して、あらためてクライアント側で接続してみてください。

# サーバー側
uv run calc_server.py
# クライアント側
uv run calc_client.py

ここでもう一段階進めてみます。 端末をもうひとつ起動して(3分割は表示の上では少々厳しいですが)、もうひとつクライアント側を起動するとどうなるでしょうか。

# 追加した端末上でクライアントを追加で起動する
uv run calc_client.py

普通に繋がったように見えますが、実は計算式を送信しても結果が返ってきません

../../_images/send-and-wait.png

指示を送っても返事が返ってこない状態

一方で、先に起動しているクライアント側は普通に応答があります。

../../_images/send-and-reply.png

元のクライアントは普通に応答がある

本来なら、私たちはどちらでも計算結果を得たいところです。 しかしながら、片方では応答が無い状態となってしまいました。 これは、サーバー側の実装が「1つのクライアントからの接続に対して1つの計算を処理する」ようになっているためです。 早い者勝ちで接続した側にだけacceptしているため、後から接続したクライアントは計算結果を得ることができません。 このように、TCPでは接続ごとに1つのクライアントしか処理できないという制約があります。

まとめ

ここまでで、TCPを使った簡単な計算機サーバーとクライアントの作成方法を学びました。 基本的なつなぎ方はそれほど難しいものはありませんが、接続ごとに1つのクライアントしか処理できないという制約があることを理解しておく必要があります。

この問題にどのように対応していくのかを、次の章で考えてみましょう。