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

[RubyでHTTPクライアントを書いてみる](http://qiita.com/akiray03/items/0689497093c961a3e627) の兄弟記事です。Rubyのsocketライブラリを使って、シンプルなHTTPサーバを書いてみましょう。

# HTTPサーバの前に超シンプルなTCPサーバを書いてみる

以下のようなスクリプトを作成し、実行してみましょう(あるいは、irb上に貼り付けてみましょう)。

```rb
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
```

別の端末を立ち上げて、以下のコマンドを実行してみましょう。複数回実行すると、最後のカウンタが更新されていくことも確認できるかと思います。

```bash
$ nc 127.0.0.1 2000
```

このままでは、クライアントからのメッセージをサーバで確認することができないので、もう少し改造します。

# シンプルなTCPサーバをRubyで書いてみる

TCPサーバのコードを以下のように変更してみましょう (`while buffer = client.gets` あたりを追加しました)

```rb
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
```

そして、クライアント側の実行も、以下のように変更してみます。

```bash
$ echo hello | nc 127.0.0.1 2000
```

このようにして、TCPリクエストに応答するシンプルなTCPサーバを作ることができました。

# シンプルなHTTPサーバをRubyで書いてみる

ここまではTCPのリクエスト/レスポンスの話でした。このままではHTTPクライアントの求めるHTTPサーバの振る舞いになっていないので、もう少し改造を進めてみます。

```rb
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` コマンドを使ってみましょう。

```bash
$ 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つ用意して、以下のコマンドを順に実行してみてください。

```bash
terminal-1$ ruby server.rb
```

```bash
terminal-2$ nc 127.0.0.1 2000
```

```bash
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で作ってみる

```rb
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つの端末でコマンドを実行してみましょう。

```bash
terminal-1$ ruby server.rb
```

```bash
terminal-2$ nc 127.0.0.1 2000
```

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

# まとめ

あとで書く