RubyでHTTPクライアントを書いてみる の兄弟記事です。Rubyのsocketライブラリを使って、シンプルなHTTPサーバを書いてみましょう。
HTTPサーバの前に超シンプルなTCPサーバを書いてみる
以下のようなスクリプトを作成し、実行してみましょう(あるいは、irb上に貼り付けてみましょう)。
require 'socket'
server = TCPServer.new 2000
i = 0
loop do
client = server.accept
p client
client.puts "Hello #{Time.now} counter:#{i}"
client.close
i += 1
end
別の端末を立ち上げて、以下のコマンドを実行してみましょう。複数回実行すると、最後のカウンタが更新されていくことも確認できるかと思います。
$ nc 127.0.0.1 2000
このままでは、クライアントからのメッセージをサーバで確認することができないので、もう少し改造します。
シンプルなTCPサーバをRubyで書いてみる
TCPサーバのコードを以下のように変更してみましょう (while buffer = client.gets
あたりを追加しました)
require 'socket'
server = TCPServer.new 2000
i = 0
loop do
client = server.accept
p client
while buffer = client.gets
puts buffer
end
client.puts "Hello #{Time.now} counter:#{i}"
client.close
i += 1
end
そして、クライアント側の実行も、以下のように変更してみます。
$ echo hello | nc 127.0.0.1 2000
このようにして、TCPリクエストに応答するシンプルなTCPサーバを作ることができました。
シンプルなHTTPサーバをRubyで書いてみる
ここまではTCPのリクエスト/レスポンスの話でした。このままではHTTPクライアントの求めるHTTPサーバの振る舞いになっていないので、もう少し改造を進めてみます。
require 'socket'
server = TCPServer.new 2000
loop do
client = server.accept
headers = []
while header = client.gets
break if header.chomp.empty?
headers << header.chomp
end
p headers
client.puts "HTTP/1.0 200 OK"
client.puts "Content-Type: text/plain"
client.puts
client.puts "message body"
client.close
end
クライアント側は、 curl
コマンドを使ってみましょう。
$ curl -v http://127.0.0.1:2000/
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 2000 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:2000
> User-Agent: curl/7.43.0
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Type: text/plain
<
message body
* Closing connection 0
その時のサーバ側の出力も確認してみましょう。
$ ruby server.rb
["GET / HTTP/1.1", "Host: 127.0.0.1:2000", "User-Agent: curl/7.43.0", "Accept: */*"]
curl
コマンドのHTTPリクエストの内容が、RubyのHTTPサーバで受け取れていることが確認できました。
シンプルなHTTPサーバに対するDoS攻撃してみる
端末を3つ用意して、以下のコマンドを順に実行してみてください。
terminal-1$ ruby server.rb
terminal-2$ nc 127.0.0.1 2000
terminal-3$ curl -v http://127.0.0.1:2000/
nc
コマンドによりTCPセッションを接続したまま放置すると、シンプルなHTTPサーバは while header = client.gets
の行で待ち続けるため、次の接続 (curl
によるリクエスト) を受け付けることができません。実際のインターネットサービス環境では、このような問題に対処するために、 selectを使う/Threadを使う/forkする、等の対策が行われます。ここではThreadを使う方法でこの問題への対処を行ってみましょう。
HTTPリクエストに並列で応えられるHTTPサーバをRubyで作ってみる
require 'socket'
server = TCPServer.new 2000
pids = []
loop do
Thread.start(server.accept) do |client|
p [Thread.current]
headers = []
while header = client.gets
break if header.chomp.empty?
headers << header.chomp
end
p [Thread.current, headers]
client.puts "HTTP/1.0 200 OK"
client.puts "Content-Type: text/plain"
client.puts
client.puts "message body"
client.close
end
end
このプログラムを使って、さきほどと同じように3つの端末でコマンドを実行してみましょう。
terminal-1$ ruby server.rb
terminal-2$ nc 127.0.0.1 2000
terminal-3$ curl -v http://127.0.0.1:2000/
すると、 server.rb を実行した端末の出力は以下のようになるはずです。
terminal-1$ ruby server.rb
[#<Thread:0x007fb8e184dea0@server.rb:6 run>]
[#<Thread:0x007fb8e184da40@server.rb:6 run>]
[#<Thread:0x007fb8e184da40@server.rb:6 run>, ["GET / HTTP/1.1", "Host: 127.0.0.1:2000", "User-Agent: curl/7.43.0", "Accept: */*"]]
最初の #<Thread:0x007fb8e184dea0>
は、HTTPヘッダを受信することなく待ち受け続けています。その後にcurl
によって新たなスレッド#<Thread:0x007fb8e184da40>
が作成され、HTTPヘッダを受け取った後に応答を返しています。
まとめ
あとで書く