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