echoサーバーを書いてみたときのメモ その2 なぜ複数クライアントを捌けないのか

前回、echoサーバーを書いてみた。その続き。

echoサーバーを書いてみたときのメモ その1 ソケットAPIとTCP

このechoサーバーだと同時に複数のクライアントを捌けない。どうしてか?実際に試して見る。クライアントから2回telnetでつないでみる。

クライアント1

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!

クライアント2

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.

2つ目の接続では、 Hello World! が表示されていない。

サーバー側で接続を確認してみる。

netstatでみると複数の接続ができてる。

$ netstat -an | grep 30000
tcp        0      0 0.0.0.0:30000           0.0.0.0:*               LISTEN
tcp        0      0 192.168.33.10:30000     192.168.33.1:64085      ESTABLISHED
tcp        0      0 192.168.33.10:30000     192.168.33.1:64084      ESTABLISHED

しかし、ソケットに対するディスクリプタを確認すると、1つしかない(64084ポートを使ってる方)。

$ lsof -i:30000
COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
echo_serv 2316 ubuntu    3u  IPv4  52901      0t0  TCP *:30000 (LISTEN)
echo_serv 2316 ubuntu    4u  IPv4  52902      0t0  TCP 192.168.33.10:30000->192.168.33.1:64084 (ESTABLISHED)

$ ll /proc/2316/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar  5 03:12 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar  5 03:12 ../
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:12 2 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 3 -> socket:[52901]
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 4 -> socket:[52902]
$ ps x | grep echo_server
 2316 pts/0    S+     0:00 ./echo_server

psのSTATが S+ なので割り込み可能なスリープ状態でフォアグラウンドのプロセスグループに入っているという状態。

straceで見てみると、readでスリープしている。

$ sudo strace -p 2316
strace: Process 2316 attached
read(4,

クライアント1で文字列を入力

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!
hoge
hoge
Connection closed by foreign host.

接続がサーバー側から切られて、クライアント2のtelnetHello World! が表示される。

$ telnet 192.168.33.10 30000
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
Hello World!

サーバー側で確認してみると新しい接続に変わってることがわかる。

$ lsof -i:30000
COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
echo_serv 2316 ubuntu    3u  IPv4  52901      0t0  TCP *:30000 (LISTEN)
echo_serv 2316 ubuntu    4u  IPv4  56445      0t0  TCP 192.168.33.10:30000->192.168.33.1:64085 (ESTABLISHED)

 ll /proc/2316/fd
total 0
dr-x------ 2 ubuntu ubuntu  0 Mar  5 03:12 ./
dr-xr-xr-x 9 ubuntu ubuntu  0 Mar  5 03:12 ../
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 0 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 1 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:12 2 -> /dev/pts/0
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 3 -> socket:[52901]
lrwx------ 1 ubuntu ubuntu 64 Mar  5 03:14 4 -> socket:[56445]

まとめると、 read から処理が進まず、クライアント2の接続に対するacceptが呼ばれない。そのためサーバーはクライアントを1つずつしか処理できない。

I/Oモデル

前回に引き続きUNIXネットワークプログラミングを参考にする。

UNIXネットワークプログラミングを読むと5種類のI/Oがあるって書いてある。

Unixネットワーキングプログラミング P.140

さらに、入力操作は2つのステップから成ると書いてある。

入力操作は次の2段階で構成されている。

  1. データの用意ができるまで待ち、
  2. そのデータをカーネルからプロセスにコピーする。

ソケットに関する入力では、最初のステップでは普通のネットワークからのデータの到着を待つ。パケットが到着すると、カーネル内のバッファにコピーされる。2つ目のステップでは、このデータをカーネルのバッファからアプリケーションのバッファにコピーすることになる。

Unixネットワーキングプログラミング P.140

ブロッキングI/O、ノンブロッキングI/O、I/Oの多重化、シグナル駆動I/Oの4つはステップ1をどう扱うかが異なる。ステップ2については同じ。非同期I/Oだけが他と異なる方法でステップ1,2を扱う。

非同期I/Oは、Linuxだとaioとして提供されているが、ググって色々読んでみた感じだとあまり使われてないっぽい?

同期I/O操作と非同期I/O操作

I/Oモデルとは別の観点で同期I/O操作と非同期I/O操作というのがある。

Posix.1 ではこれら2つの語を以下の様に定義している。

  • 同期I/O操作では、これを要求したプロセスは要求したI/O操作が完了するまでブロックする
  • 非同期I/O操作では、これを要求したプロセスはブロックしない

この定義によれば、最初の4つのI/Oモデル、すなわちブロッキング、非ブロッキング、多重化I/OおよびシグナルI/O操作のすべてにおいてプロセスはブロックする。非同期I/Oモデルのみが非同期I/Oの定義に一致している。

UNIXネットワーキングプログラミング P.144

書いてみたechoサーバーのI/Oモデル

書いてみたechoサーバーで使ってる readブロッキングI/Oである。ブロッキングI/Oではステップ1と2が終わるまで元のプロセスに処理が戻らない。 read で処理が止まっていたのはステップ1で、クライアントからのデータの到達を待っているからである。クライアントからのデータが届くまでechoサーバーのプロセスはブロックされる。

複数クライアントを同時に捌くために、echoサーバーを以下の方法で書き換えてみようと思う。

  • ブロッキングI/Oのまま。forkを使って複数プロセスを立ち上げ、1プロセス1クライアントで対応する
  • ブロッキングI/Oを使って対応する。1プロセスで複数クライアントに対応する
  • I/O多重を使って対応する。1プロセスで複数クライアントに対応する