Nonblocking TCP Server

Socket(TCP)通信をかじったのでメモ。

first implementation

サーバ。

require 'socket'
server = TCPServer.new(4481)
loop do
  connection = server.accept
  request = connection.read
  connection.write("request served: #{request}")
  connection.close
end

クライアント。

require 'socket'
client = TCPSocket.new('localhost', 4481)
client.write('hoge')
client.close_write
client.read  # => "request served: hoge"
client.eof?  # => true

multiple read/write

1つのTCPコネクションで複数回read/writeできるようにする。

サーバ。

require 'socket'
server = TCPServer.new(4481)
loop do
  connection = server.accept
  loop do
    request = connection.gets.chomp
    break if request == 'exit'
    connection.puts("request served: #{request}")
  end
  connection.close
end

クライアント。

require 'socket'
client = TCPSocket.new('localhost', 4481)
client.puts('hoge')
client.gets  # => "request served: hoge\n"
client.puts('fuga')
client.gets  # => "request served: fuga\n"
client.puts('exit')
client.eof?  # => true

nonblock accept

上記のサーバでは、クライアントからの接続要求を待っている間ブロックされてしまう。(accept)

仮にサーバが1秒に1ずつ数を数え上げる処理も行いたいとする。

クライアントからの接続要求を待ちつつも数え上げも行えるようにしたサーバが以下。

require 'socket'
server = TCPServer.new(4481)
counter = 0
loop do
  begin
    connection = server.accept_nonblock
    loop do
      request = connection.gets.chomp
      break if request == 'exit'
      connection.puts("request served: #{request}")
    end
    connection.close
  rescue Errno::EAGAIN
    p counter += 1
    sleep(1)
    retry
  end
end

サーバを立ち上げた瞬間から数え上げ始める。

クライアントからの接続があるとリクエスト処理に専念する。

クライアントが接続を切ると数え上げを再開する。

nonblock read

上記のサーバでは、クライアントからのデータ送信を待っている間ブロックされてしまう。(read)

クライアントからのデータ送信を待ちつつも数え上げも行えるようにしたサーバが以下。

require 'socket'
server = TCPServer.new(4481)
counter = 0
loop do
  begin
    connection = server.accept_nonblock
    loop do
      begin
        request = connection.read_nonblock(1024).chomp
        break if request == 'exit'
        connection.puts("request served: #{request}")
      rescue Errno::EAGAIN
        p counter += 1
        sleep(1)
        retry    
      end
    end
    connection.close
  rescue Errno::EAGAIN
    p counter += 1
    sleep(1)
    retry
  end
end

クライアントからの接続を待っている間にも数え上げを続けることができた。

nonblock write

いま、サーバから返すデータが大量になったとする。

before: "request served: #{request}\n"

after: "request served: #{request}" * 1_000_000 + "\n"

この場合、クライアントがメッセージを送ると、レスポンス送信においてクライアント側のバッファがいっぱいになり、writeがブロックされる。

このとき数え上げは中断されるが、クライアントがreadするとクライアント側のバッファに空きができるのでwriteを再開し、writeを終えると数え上げが再開される。

これを、クライアントがreadしなくてもサーバで数え上げを続けるようにしてみる。

require 'socket'
server = TCPServer.new(4481)
counter = 0
loop do
  begin
    connection = server.accept_nonblock
    loop do
      begin
        request = connection.read_nonblock(1024).chomp
        break if request == 'exit'
        payload = "request served: #{request}" * 1_000_000 + "\n"
        loop do
          begin
            sent = connection.write_nonblock(payload)
            break if sent >= payload.size
            payload.slice!(0, sent)                
          rescue Errno::EAGAIN
            p counter += 1
            sleep(1)
            retry
          end
        end
      rescue Errno::EAGAIN
        p counter += 1
        sleep(1)
        retry    
      end
    end
    connection.close
  rescue Errno::EAGAIN
    p counter += 1
    sleep(1)
    retry
  end
end

レスポンス時にクライアントがreadせずwriteがブロックされた状態でも、数え上げを続けることができた。

nonblock connect

まとめようと思ったが、connectがブロックされる状態を再現できなかったので省略する。

IO.selectとの関係

各手法の特徴
  • read/write/accept/connectは、1つのsocketにはりついてブロックする。

  • xxxx_nonblockは、1つのsocketについて全くはりつかずに、試みたらただちにreturnする。

  • IO.selectは複数のsocketを見渡しながらブロックする。

つかいわけ
  1. 1つのsocketにはりついてブロックしてOKな場合
    • read/write/accept/connectをつかう
  2. socketにはりつかなくてよい(時間軸上の点でたびたびトライすればよい)場合
    • xxxx_nonblockとretryを組み合わせてつかう
  3. 複数のsocketを見渡して待ち構えたい場合
    • xxxx_nonblockとIO.selectを組み合わせてつかう
    • さらにtimeoutとretryをつかうことで、時々待ち構える状態から抜けることができる

上記の例は全て2.の場合の実装とした。