- Understand how a web server works
- Use Rack to create a simple, bare-bones web server
How does a web server work?
We open a browser and it uses HTTP to send a message to a server. Servers are
just computers running code that waits for requests and sends back responses.
But when you say /search?item=shoes&size=13M
, how does it know to run the code
to search for shoes of size 13M?
All web servers have a core architecture in common. By looking at that architecture, we can build a mental model for how all web servers work. As an analogy, we can explain how all cars work like this:
"Explosions made by gasoline and fire make an inside wheel go round and that inside wheel makes the outside wheels go round"
In the same way, we can say that all web servers work like this:
"They wait for an HTTP request and look at the HTTP verb and path, and then run some conditional logic to find out which stuff to send back in the response"
In Ruby, this idea of "a core architecture" for all web-server like things is captured in a gem called Rack. Rails, which you'll learn in Phase 4, "rides on top of" Rack. Sinatra, which you'll learn in the coming lessons, "rides on top of" Rack too.
In fact, the idea of a base, common web-server library was such a good idea, other languages like Python and JavaScript (via the NodeJS environment) implemented their own "base" web server. By understanding the core mechanics of how a server works in Ruby, you'll have a much easier time learning how to work with servers in those other languages.
Before we get to the complexity of things built on top of Rack, let's get a simple server working on Rack by itself.
Note: We'll be moving on from Rack shortly, so don't worry too much about understanding the exact syntax in this lesson. Focus on the concepts.
To code along with this lesson, run bundle install
. We'll be using the
Rack gem, which is included in the Gemfile.
Our goal with any web server is to be able to receive a request and send a response.
To accomplish this with Rack, we need to create a class that responds to a
single method: #call
. All this method needs to do is return an array with
three elements:
- A status code (where
200
is used forOK
) - A response headers hash with a
"Content-Type"
key that returns the value oftext/html
(for HTML-based responses) - An array of strings to send back in the body of the response (in our case,
we can format the string like HTML:
"<p>Like this!</p>"
)
Essentially, we need the #call
method to return something like this:
[status code, headers hash, response body]
Here's an example that returns a HTML string:
[200, { "Content-Type" => "text/html" }, ["<h2>Hello <em>World</em>!</h2>"]]
With this goal in mind, let's create a basic web server. Follow along with the below instructions.
Let's create a file called config.ru
. Files that are used by Rack end with
.ru
instead of .rb
because they're normally loaded with a command called
rackup
. It's a way to indicate to other developers that this is our server
definition file.
Add this code to the config.ru
file:
require 'rack'
class App
def call(env)
[200, { "Content-Type" => "text/html" }, ["<h2>Hello <em>World</em>!</h2>"]]
end
end
run App.new
When we run this code, Rack will essentially run in a loop in the background
waiting for a request to come in. When it receives a request, it will call
the #call
method and pass in data about the request, so we can send back the
appropriate response.
Run this code from the command line:
rackup config.ru
Rack will print out something like:
[2021-07-19 16:38:10] INFO WEBrick 1.4.2
[2021-07-19 16:38:10] INFO ruby 2.6.3 (2019-04-16) [universal.x86_64-darwin20]
[2021-07-19 16:38:10] INFO WEBrick::HTTPServer#start: pid=34006 port=9292
WEBrick is a Ruby library that provides a simple HTTP server. Rack needs a web server to to handle connections, and WEBrick is the default since it's included with Ruby. Later, we'll be replacing this with another more powerful Ruby server, Thin.
Try visiting http://localhost:9292
in your browser. This will send a GET
request to your Rack server, and you should see the HTML response of
Hello World
appear!
Let's deconstruct this URL a little bit though. The URL is
http://localhost:9292/
. The protocol is http
. That makes sense, but the
domain is localhost:9292
. What's going on there?
localhost
is normally where a domain name like google.com
goes. In this
case, since you are running the server on your computer, localhost
refers to
the internal address of your computer.
The last part of that URL is the :9292
section. This the "port number" of your
server. You may want to run multiple servers on one computer (for example, one
for React and one for Sinatra) and having different ports allows them to be
running simultaneously without conflicting.
The path, or resource, that you are requesting is /
. This is effectively like
saying the home or default path. You should be able to go to
http://localhost:9292/
and see Hello World
printed out by your web server!
Feel free to change config.ru
to add changes to your web server. If you make
changes to config.ru
you'll have to shut down the server (control + c
) and
re-start it to see the changes.
We can also access different information about the request object by using
the env
argument that is passed into the call
method. Try adding a
binding.pry
to the #call
method:
require 'rack'
require 'pry'
class App
def call(env)
binding.pry
[200, { "Content-Type" => "text/html" }, ["<h2>Hello <em>World</em>!</h2>"]]
end
end
run App.new
Then, stop (control + c
) and restart the server (rackup config.ru
), and
refresh the browser to make another request to the server. You should hit your
binding.pry
breakpoint, where you can explore the env
hash with all the data
about the request:
env["REQUEST_METHOD"]
# => "GET"
env["PATH_INFO"]
# => "/"
From here, it's not too much of a leap to see how we could make our server more dynamic and set it up to send back different responses based on the path.
For example:
require 'rack'
require 'pry'
class App
def call(env)
path = env["PATH_INFO"]
if path == "/"
[200, { "Content-Type" => "text/html" }, ["<h2>Hello <em>World</em>!</h2>"]]
elsif path == "/potato"
[200, { "Content-Type" => "text/html" }, ["<p>Boil 'em, mash 'em, stick 'em in a stew</p>"]
else
[404, { "Content-Type" => "text/html" }, ["Page not found"]]
end
end
end
run App.new
Try restarting the server, and make requests in the browser to see the response change based on the path:
This conditional logic based on the path (and also the HTTP verb, as we'll see later) is known as routing, and it's is basically what web servers do all day long. Rails, Sinatra, any web programming framework you can name: one of their key features is to simplify and standardize how routing works so we can focus on working with data and generating responses.
Rack is a simple, low-level tool for writing servers in Ruby. Since it's such a low-level tool, it can be challenging to build more complex applications with. In the next lesson, we'll learn how to use Sinatra to help with some common tasks when building a web server.