Skip to content

Web Server Assignment

Julie Haché edited this page Jan 26, 2015 · 31 revisions

In this assignment, you will learn to work with an existing piece of code and how to extend it. You will also learn to work with a "standard library" (stdlib) which you need to require in order to use.

By the end of this assignment, you should have a fully-functioning simple web server that serves a requested file.

Setup

Once you've forked this repository (https://github.com/bitmakerlabs/web-server) and cloned it to your computer, you're ready to get started.

This project has one main file called server.rb. This is your starting point. This file is like every other Ruby program you've run so far: you just need to start it with

$ ruby server.rb

When it's started, you should see it output "Server started on localhost:2000 ..." This is where your server is listening. In order to make a request, visit "http://localhost:2000" in your browser.

If all goes well, you should see the current date and time in your browser:

The following text (or something very similar) should've appeared in your terminal.

GET / HTTP/1.1
Host: localhost:2000
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8

(If you see extra output, your browser may be making more than once request at a time. Don't worry about it for now, it won't cause any problems. Compare different browsers, like Firefox, Safari, Chrome to see if they make similar requests.)

1. Create an HTML response inline

Currently, the server.rb file that you're working with is merely outputting the current date and time, but your browser's favourite thing to read is HTML. Why not give it what it likes?

Replace the current time response. Write some HTML inside a string and send it back to your client.

Here's an example:

# inside server.rb
response = "
  <!DOCTYPE html>
  <html>
    <head>
      <title>My first web server</title>
    </head>
    <body>
      <h1>My first web server</h1>
      <p>Oh hey, this is my first HTML response!</p>
    </body>
</html>"

client.puts(response)

Now you've provided a fully valid HTML document back to your browser. Once you have HTML that you like, commit your changes and move to the next step.

Don't forget that you'll need to stop and restart your server for your new changes to take effect.

2. Move your HTML response into a file

Now that you've got an HTML response ready, it's probably a lot of easier to work within a file. Try moving your HTML inside a file called index.html that's located in the same directory.

In your response, have your code read the file and output the contents to your client socket.

Reading a file can be very simple in Ruby:

filename = "index.html"
response = File.read(filename)

After you've replaced your response with a file, don't forget to commit your changes!

PROTIP: If you tried to include a CSS file in your index.html, it won't work just yet. You'll need to move on to step 3 to see how to serve multiple files.

3. Match your response with the requested file

So far we're able to respond to a request, but we're always responding with the same content no matter what we type in as the request. That's not quite how real web servers operate.

Let's work on responding with the content our client really asked us for. To determine what that content is, we need to have a look at the request headers our client has sent along (which is the text that's being output in your terminal with each request). Specifically, we care about the first line:

GET / HTTP/1.1
Host: localhost:2000
...

That first line indicates what the client is actually requesting.

  • GET is the HTTP method being used
  • / represents the path of the resource or file being requested
  • Finally, HTTP/1.1 represents the protocol (we don't really care so much about this last part).

We mainly need to extract the middle part so that we know which file our browser is requesting. Here's some example code that'll help you parse out the requested file. Use it within your server:

filename = lines[0].gsub(/GET \//, '').gsub(/\ HTTP.*/, '')

You can now use this line to get a matching string and check if the filename exists. If it does, you can read it and return it to the client. Otherwise, you should tell the client the file wasn't found.

filename = lines[0].gsub(/GET \//, '').gsub(/\ HTTP.*/, '')

if File.exists?(filename)
  response_body = File.read(filename)
else
  response_body = "File Not Found\n" # need to indicate end of the string with \n
end

4. Add response headers

As we saw up above, each request has a set of headers that gets sent each time. In order for us to follow the HTTP rules, we should do the same for our responses.

The response headers tell our browser all sorts of things about our response, including a response code that tells us if our request was successful.

After each string in the header, we need to add \r\n. These are special symbols that indicate to our system that there's a new line.

Here's an example string for a successful response header. Note the "200 OK"

success_header = []
success_header << "HTTP/1.1 200 OK"
success_header << "Content-Type: text/html" # should reflect the appropriate content type (HTML, CSS, text, etc)
success_header << "Content-Length: #{response_body.length}" # should be the actual size of the response body
success_header << "Connection: close"
header = success_header.join("\r\n")

Here's an example string for a "Not Found" response header.

not_found_header = []
not_found_header << "HTTP/1.1 404 Not Found"
not_found_header << "Content-Type: text/plain" # is always text/plain
not_found_header << "Content-Length: #{response_body.length}" # should the actual size of the response body
not_found_header << "Connection: close"
header = not_found_header.join("\r\n")

Response headers should arrive before the response body. The Content-Type tells the browser how to interpret the rest of the response. In this case above, we're signalling that the browser should expect HTML.

In this step, add the proper response headers using the example strings provided above and join them with the response body to create your full response.

Between the header and footer, the protocol also requires a completely empty line.

response = [header, response_body].join("\r\n\r\n")

There you have it! Feel free to add different files (HTML, CSS, etc) to your server directory and experiment with different request paths to see what works and what doesn't.

When you're done, don't forget to commit all your code and submit your assignment.

5. (BONUS) Think OOP

Now that you have all your code working, start thinking about how some of this code could be moved into methods. Parsing filenames, figuring out content type, etc.

Next, start thinking about how you can make it more modular and object-oriented. Does a Request and a Response warrant their own class so that you can run actions on them? Could the server be its own class too?