TCPプログラミングでハマった

TCPを勉強しようと思って、Rubyネットワークプログラミングを参考にしながら色々といじっていたらハマった。

以下のようなサーバープログラム(tcp_server.rb)とクライアントプログラム(client.rb)を書いた。

# tcp_server.rb

require "socket"


server = TCPServer.open(12345)

puts "[waiting connection]"
sock = server.accept
puts "[established connection]"

p sock.gets
puts "foo"

sock.puts "OK"

sock.close
puts "[closing connection]"

server.close
# client.rb

require "socket"

sock = TCPSocket.open("127.0.0.1", 12345)

puts "a"
sock.write("Hello\nWorld!\n")
puts "b"

p sock.gets
puts "c"

sock.close

先にruby tcp_server.rbでサーバーを起動させると、

[waiting_connection]

と表示され待ち状態に入る。 ruby client.rbでサーバーにアクセスすると、サーバー側のコンソールは

[waiting_connection]
[established_connection]
"Hello\n"
foo
[closing connection]

と表示され終了し、クライアント側のコンソールには

a
b
"OK\n"
c

と表示されて終了する。

一応、ちゃんと動く。
細かいことを言うと、サーバー側はクライアント側から送られてきたデータを全部読み込まずに書き込みを行ったけど、読み込まれなかったデータはどうなったのかとか、ストリームにデータが残ったまま書き込みを行ってもいいのかとか色々ある。

サーバー側のコードsock.getssock.readに変更する。
そしてサーバー側のコードを実行し、クライアントでアクセスすると、サーバー側のコンソールには

[waiting_connection]
[established_connection]

と表示されたまま待ち状態に入り、クライアント側では

a
b

と表示されたまま待ち状態に入ってしまう。

これはまだわかる。

TCPSocketもTCPServerも遥か先祖でIOクラスを継承していて、サーバー側で使ってるreadメソッドはIO#readメソッドだ。(どっかでオーバーライドされているかもしれないけど、多分そんなことはないだろう。システムコールレベルでみれば、read、writeはストリームに対する処理として抽象化されているので。)
IO#readは第一引数のlengthを指定しなかった場合EOFが来るまで読み込む。
逆に言うと、EOFを読み込むまでブロックし続ける。
今回の場合、サーバーはsock.readで待ち状態に入り、クライアントの方もsock.getsで待ち状態に入り(IO#getsはEOFか改行を読み込むまでブロックする)、どちらも待ち状態の時にクライアント側のプログラムをCtrl Cとかで中断させると、サーバー側はBroken pipe (Errno::EPIPE)というエラーになる。
これもわかる。
クライアント側のプロセスが止まると必然的にコネクションが切れて、コネクションが切れたソケットストリームに対して読み込みを行うとEOFが返るのでサーバー側のプログラムはsock.readから戻り、sock.puts "OK"を実行する。
しかし、コネクションが切れたストリームに対して書き込みをしてしまってエラーが発生するという訳だ。

最後にサーバー側のプログラムのsock.getsの部分をsock.read(6)とreadに読み込むバイト数を指定するようにする。
これは上手くいくと思ってた。
readでブロックされることはないし、一番最初のプログラムのときのようにソケットストリームに読み込まずに残ったデータがあるまま書き込みを行っても問題ないと考えたからだ。

まあでもうまく行かなかった。
サーバー側のプログラムは正常終了した。
しかし、クライアント側のプログラムでエラーが発生した。
`gets': Connection reset by peer @ io_fillbuf - fd:7 (Errno::ECONNRESET)というエラーメッセージが出た。 エラーメッセージから察するに接続がリセットされたらしいけどなぜ?
あと、io_fillbufがよくわからん。
ぐぐってみるとどうもRuby組み込みライブラリ中のCで書かれた関数らしい。
ここでエラーが発生したということだろう。
ちなみに、ソースはio.c:1673。

static int
io_fillbuf(rb_io_t *fptr)
{
    ssize_t r;

    if (fptr->rbuf.ptr == NULL) {
        fptr->rbuf.off = 0;
        fptr->rbuf.len = 0;
        fptr->rbuf.capa = IO_RBUF_CAPA_FOR(fptr);
        fptr->rbuf.ptr = ALLOC_N(char, fptr->rbuf.capa);
#ifdef _WIN32
        fptr->rbuf.capa--;
#endif
    }
    if (fptr->rbuf.len == 0) {
      retry:
        {
            r = rb_read_internal(fptr->fd, fptr->rbuf.ptr, fptr->rbuf.capa);
        }
        if (r < 0) {
            if (rb_io_wait_readable(fptr->fd))
                goto retry;
            {
                VALUE path = rb_sprintf("fd:%d ", fptr->fd);
                if (!NIL_P(fptr->pathv)) {
                    rb_str_append(path, fptr->pathv);
                }
                rb_sys_fail_path(path);
            }
        }
        fptr->rbuf.off = 0;
        fptr->rbuf.len = (int)r; /* r should be <= rbuf_capa */
        if (r == 0)
            return -1; /* EOF */
    }
    return 0;
}

んー、よくわからん。