Don't like WEBrick? Try net-http-server

http, parslet, rack, ruby, server



$ gem install net-http-server


Some have said that the WEBrick HTTP Server is a Ghetto. While WEBrick is very fast for a pure-Ruby HTTP Server, the parsing code is hand written and difficult to read. WEBrick is also one of the oldest Ruby HTTP Servers, but for some reason lacks documentation coverage. Given the rise of Rack, middleware and Rack applications, WEBricks API now seems awkward.

The Parser

When Parslet (a pure Ruby PEG Parser library) was announced, I wondered how hard would it be to write a HTTP Parser with Parslet. After researching the other Ragel based HTTP Parsers (Thin and Unicorn) and double checking RFC 2616, I suddenly had a pure-Ruby HTTP Parser. (in one file, that you can actually read!)

I found that the way in which Parslet nested matches into Arrays of Hashes resulted in data that looked very much like a Rack env Hash.

require 'net/http/server/parser'

parser = Net::HTTP::Server::Parser.new
parser.parse("GET /path?x=1&y=2 HTTP/1.1\r\nCookie: xyz;123\r\n\r\n")
# => {
#      :method=>"GET",
#      :uri=>{:path=>"path", :query=>"x=1&y=2"},
#      :version=>"1.1",
#      :headers=>[{:name=>"Cookie", :value=>"xyz;123"}]
#    }

The Daemon

The next step was to write an actual Daemon that would:

  1. Receive Connections
  2. Parse HTTP Requests
  3. Pass HTTP Requests to a Request Handler
  4. Receive HTTP Responses from the Request Handler
  5. Send HTTP Responses
  6. Close Connections

I settled on using the battle tested GServer class to handle the Connections for Nett:HTTP::Server::Daemon. I also borrowed some ideas from Rack, such as passing HTTP Requests via a call method and returning HTTP Responses as an Array (containing the HTTP Status, Headers and Body).

require 'net/http/server'
require 'pp'

Net::HTTP::Server.run(:host => '', :port => 8080) do |request,socket|
  pp request

  [200, {'Content-Type' => 'text/html'}, ['Hello World']]

The Rack Handler

Given that the API was already very Rack-ish, writing a Rack handler on top of Net::HTTP::Server::Daemon was simple.

require 'rack/handler/http'
require 'sinatra'

class HelloWorld < Sinatra::Base

  get '/' do
    [200, {'Content-Type' => 'text/html'}, ["Hello World"]]


Rack::Handler::HTTP.run HelloWorld, :Host => 'localhost', :Port => 1212


By now your probably wondering, how fast is this pure Ruby HTTP Server?

require 'net/http/server'

Net::HTTP::Server.run(:port => 8080) do |request,socket|
  [200, {'Content-Type' => 'text/html'}, ['Hello World']]

$ ab -n 4000 -c 4 http://localhost:8080/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)

Finished 4000 requests

Server Software:        
Server Hostname:        localhost
Server Port:            8080

Document Path:          /
Document Length:        11 bytes

Concurrency Level:      4
Time taken for tests:   73.405 seconds
Complete requests:      4000
Failed requests:        0
Write errors:           0
Total transferred:      220000 bytes
HTML transferred:       44000 bytes
Requests per second:    54.49 [#/sec] (mean)
Time per request:       73.405 [ms] (mean)
Time per request:       18.351 [ms] (mean, across all concurrent requests)
Transfer rate:          2.93 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       8
Processing:    24   73  31.5     62     236
Waiting:       24   72  31.4     60     236
Total:         25   73  31.5     62     236

Percentage of the requests served within a certain time (ms)
  50%     62
  66%     86
  75%     98
  80%    103
  90%    119
  95%    134
  98%    148
  99%    153
 100%    236 (longest request)

Definitely not as fast as Thin or even WEBrick, but not too bad considering its pure-Ruby and the size of the code-base.