Don't like WEBrick? Try net-http-server
— http, parslet, rack, ruby, server
TL;DR
$ gem install net-http-server
- pure-Ruby HTTP Parser and Server
- Small codebase
- Fast-ish
- Rack-like API
- Rack Handler included
- full YARD Documentation
WEBrick
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:
- Receive Connections
- Parse HTTP Requests
- Pass HTTP Requests to a Request Handler
- Receive HTTP Responses from the Request Handler
- Send HTTP Responses
- 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 => '127.0.0.1', :port => 8080) do |request,socket|
pp request
[200, {'Content-Type' => 'text/html'}, ['Hello World']]
end
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"]]
end
end
Rack::Handler::HTTP.run HelloWorld, :Host => 'localhost', :Port => 1212
Benchmarks
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']]
end
$ 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.