Building a Server in Ruby

The project spec for HTTP Yeah You Know Me (I don’t name these things!) can be found here, and the repo containing the project I completed with Nate Venn can be found here. I know Nate and I both would like to have had more time to refactor and improve our code, but the nature of the beast prohibited it. It may yet change if we find that time.

To date, this is the project that has taught me the most, so here are some reflections on what worked and what didn’t.

Turns out it’s not, actually. Some of that feeling lingers, but I have a better understanding now of how the HTTP Request/Response cycle works that I hope to be able to apply once we move into things like Rails.

Here’s the gist of it:

We created a server that listens to port 9292 using Ruby’s TCPServer class:

class WebServer 
  def initialize 
    @server = TCPServer.new(9292) 
  end 
end

You can see all the methods a TCP Server can take here. It’s not much.

But what it can do is accept requests (it’s really the 2nd we’re looking at here):

def start_server 
  @client = @server.accept 
  @request_lines = [] 
  while line = client.gets and !line.chomp.empty? 
    request_lines << line.chomp 
  end 
  request_lines 
end

So once you open your server (do some other stuff, and then run the file from your command line), you can send it a request from your browser by visiting localhost:9292

This is what the request looks like:

Got this request: ["GET / HTTP/1.1", "Host: localhost:9292", "Connection: keep-alive", "Cache-Control:max-age=0, "Accept: html, application/xml;q=0.9,image/webp,*/*;q=0.8", "Upgrade-Insecure-Request: 1", "User-Agent: Mozilla/5.0 (M 11_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36", "Accept-Encoding: gzp of-age: en-US,en;q=0.8,ex;q=0.6"]

Technically, this is the formatted version we were creating above, shoveling each line (each time you run client.gets) into an array, so that we could work with it later — each array element is one line of the client request.

Once you have that, you can do anything with it. I exaggerate, but you can do a whole lot with it. It becomes more obvious when you send a request that has a path and parameters:

localhost:9292/word_search?word=happy

You can do different things based on the verb and path (show different output, redirect…), you can do things with the parameters (we had to use our computer’s dictionary to determine whether the parameter was a known word or not), and on and on.

The above approach becomes a little problematic once you need to send POST requests with the parameters in the body instead of the url. There is an empty line between the headers and the body of the request, and that code definitionally stops when it hits a blank line, but we’ll leave it there for our purposes.

All that’s left after that is to send the server a response, so that it knows what to do with the request:

def server_response 
  @output = "<html><head></head><body>#</body></html>"
  @headers = ["http/1.1 200 ok", 
              "date: #{Time.now.strftime('%a, %e %b %Y %H:%M:%S %z')}", 
              "server: ruby", 
              "content-type: text/html; charset=iso-8859-1",
              "content-length: #\r\n\r\n"].join("\r\n") 
  client.puts headers 
  client.puts output 
end 

def game_server_response 
  @output = "<html><head></head><body>#</body></html>" 
  @headers = ["HTTP/1.1 302 Found", 
              "Location: http://127.0.0.1:9292/game\r\n\r\n", 
               "content-length: #\r\n\r\n"].join("\r\n") 
  client.puts headers 
  client.puts output 
end

Mostly, ours ran the server_response method, but the game required a redirect, so followed the game_server_response method. The @response variable contained the information we wanted outputted to the browser, and was determined and formatted elsewhere, not shown.

This was all run in a loop, so you could send a new request after each response. So there you have it — an HTTP Request/Response cycle!

All of the instructors at Turing tells us (verbally, but also through things like the grading rubric at the bottom of project specs), that functionality is one of the things that matters, but by no means is it the only thing that matters. I get caught up in that sometimes — wanting it to work so badly that I lose sight of all the rest. That definitely happened in this project, but I think I’ll revisit this topic on its own some other time. It’s worth the consideration.

As you can see in the project spec, we were expected to do one iteration on a time, with each iteration built upon the last. This makes a lot of sense, but isn’t something I do naturally. (Here’s to changing that). If I had been given a project spec that listed all the things I needed to be able to accomplish through iteration four, I would have approached it differently.

It’s hard to say for sure what I would have done, but a likely scenario involves the awareness of the need to support different paths and parameters, which might have led me to start parsing the request and building out the support for paths before worrying about being able to output a message and debugging statistics to the browser at all. Think about how confusing that might have been. Inefficient, at the least.

The lesson: when given projects that aren’t broken out into iterations for me — think about that before I start writing code.