2019-09-24
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.の場合の実装とした。