LoginSignup
86
66

More than 5 years have passed since last update.

RubyでシンプルなHTTPサーバを作る

Posted at

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ヘッダを受け取った後に応答を返しています。

まとめ

あとで書く

86
66
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
86
66