Ain't Nothing But A G Thang
October 9, 2009
You’ve probably seen more than a few articles on the web showing how to build a Rack app. If not, here’s a good one to start with. You’ll quickly see that building a Rack app is really simple, which is why Rack is awesome, because it’s simple. But what about writing a Rack-compliant server? Well it turns out that is pretty easy as well.
I just pushed a little Rack-compliant HTTP Server that I wrote using GServer to github. The whole thing is less than 200 lines of code. The core of it is short enough that I can explain how it works here.
First, GServer. GServer, which is short for “Generic Server” makes it pretty simple to create a multithreaded TCP Server. Taking out some error handling code, here’s what the GServer looks like for our Rack HTTP Server:
module GThang
class HttpServer < GServer
attr_reader :port, :rack_app
def initialize(options={})
@port = options[:Port] || 8080
@rack_app = options[:rack_app]
super(@port)
end
def serve(socket)
RackHandler.new(socket, rack_app, port).handle_request
end
end
end
So all there is to a GServer is basically a serve
method. This will be called each time a client connects to the server. The argument to the method is the client socket connection. You read and write data from the socket as you see fit for your application. As you can see here, we just pass the socket, along with the rack app and the port to the RackHandler initializer and then call handle_request
on that. We’ll look at how you setup the rack app in a minute, but first let’s take a look at the meat of what the RackHandler does. The handle_request
method looks like this:
def handle_request
return unless add_rack_variables_to_env
return unless add_connection_info_to_env
return unless add_request_line_info_to_env
return unless add_headers_to_env
send_response(app.call(env))
end
So what happens is the various add_
methods build up the rack environment. Once the environment is ready, we call the rack app. The rack app responds with the standard 3 element array, which we pass off to the send_response method, which writes the actual http response to the client. Take a look at the full code for this on github for the details.
Now the fun part is that we now have a fully functional HTTP server that is capable of acting as a file server or serving a Rails app. All we have to do is give the HttpServer
the correct Rails app. If you look in the examples, you see this for the file server:
GThang::HttpServer.run(
Rack::CommonLogger.new(
Rack::Lint.new(
Rack::Directory.new(root, Rack::File.new(root)))),
:Port => 8080)
Now I choose to write it this way to make it clear what is actually happening. You will normally see the builder DSL used to configure a rack app, which would look like this:
use Rack::CommonLogger.new
use Rack::Lint.new
use Rack::Directory.new(root)
run Rack::File.new(root)
This is obviously a lot cleaner, but to understand how Rack works, you have to realize that all this is doing is what we see in the first example. A Rack app with Rack middleware is simple a chain of apps that call the next app in the chain, possibly modifying the environment or response before or after the rest of the chain is called.
So there you have it, beauty in simplicity.