Make your own HTTP server in ruby
Tags: webdev, ruby, rails
Published: 2023-11-22 18:45:16

Foundational stuff is too much overlooked in our Software Engineering.  I mean, I know few people who want to dive into protocols and how servers work. Whereas I think these are key and thrilling understandings of how things work. 

In this article, we will dive into how to build our own HTTP server with Ruby. 

## HTTP

We are not going to dive deep into each corner of the protocol. But at least having the definition is important: HTTP is a stateless protocol of the Application layer based on TCP.

In this article, we will focus on HTTP 1.1.

## From TCP server

So this is dumb, but what is the role of a server? Get the request from the clients and give them an appropriate response. HTTP messages follow a structure. The request has the verb and the path they want to access and, finally, the protocol version.

So first thing, our server will need to accept the connection as we said before, HTTP is based upon TCP. So we will need to open a TCP socket on our machine. 

In Ruby, nothing is that simple we can use the socket library. And since Ruby loves OOP we will wrap everything in a class. 

```ruby
# http_server.rb
require 'socket'
class HttpServer
  def initialize(port)
    @server = TCPServer.new port
  end
end
```

Then we will need to accept the connection as we said and keep the connection open to any client in our HttpServer class let's define an accept_connection method.

```ruby
def accept_connection 
  while session = @server.accept
  end
end
```

## To HTTP Server

Now that we accept connection over TCP, we can analyze the message we receive. First, we can see that the message we receive is an HTTP request.

And this is much simpler than we think. The sockets are Streams of data you can write and read from them as data come.

So we are going to read from our stream with the gets function. 

Then we will need to do something really important in all HTTP servers: parsing the HTTP request to know what to answer to know how to respond to the client.

```ruby
def accept_connection
  while session = @server.accept
    request = session.gets
    verb,path,protocol  = request.split(' ')
    if protocol === 'HTTP/1.1'
      session.print response_hello_world
    else 
      session.print 'Connection Refuse'
    end
  end
end
```

For the response, we can define something like this : 
```ruby
def response_hello_world
 <<-HTTPRESPONSE
  HTTP/1.1 200
  Content-Type: text/html

  Hello World
  HTTPRESPONSE
end
```
Let's build an HTTP client to see how our server behaves. 

## HTTP Client

As you can imagine, we will need a TCP socket again. We must connect to the already opened TCP socket for our Web server and then send an HTTP request.

```ruby
# tcp_client.rb
require 'socket'
server = TCPSocket.new 'localhost', 5678
request = <<-HTTPMSQ
GET /test HTTP/1.1
HTTPMSQ
server.puts request
while line = server.gets
  puts line
end
server.close
```

Here we put a correct HTTP request with the method, the header and the protocol. But if we did not, as we have seen before, we would end up with a `Connection refuse` as defined in `accept_connection`.



## Routing and Controllers

So what we did was pretty simple, and we added only one path and case. Now what would happen if we want to take a different path?

We can define a route class that will take care of reading the path and routing to the correct resources.

```ruby
class Router

  def initialize(path)
    @path = path
  end

  def route
    if path === '/test'
      "hello tester"
    elsif path === '/world'
      "hello world"
    end
  end
end
```

Then we can define a builder for the HTTP response : 
```ruby
class HttpResponse

  def self.build(response)
    <<-HTTPRESPONSE
HTTP/1.1 200
Content-Type: text/html

#{response}
    HTTPRESPONSE
  end

end
```
Let's change a bit our `accept_connection` method : 
```ruby
  def accept_connection
    while session = @server.accept
      request = session.gets

      verb,path,protocol  = request.split(' ')

      if protocol === "HTTP/1.1"
        response = Router.new(path).route
        http_response = HttpResponse.build(response)
        session.print http_response
      else
        session.print 'Connection Refuse'
      end
    
      session.close
    end
  end
```


But We could even do something more complicate, as within Rails with ActiveController and ActionPack.

## Even Further

This is a bit of, but we could even do something more complicated with CRUD routes and controllers. This is inspired by this article: https://tommaso.pavese.me/2016/07/26/a-rack-application-from-scratch-part-2-routes-and-controllers/ and how ActionPack and ActionController works inside Rails.

```ruby
class TestController
  def index
    "Hello Test"
  end
end

class Router

  def initialize(path)
    @path = path
  end

  def camelize(string)
    string = string.sub(/^[a-z\d]*/) { |match| match.capitalize! || match }
    string.gsub!(/(_)([a-z\d]*)/i) do
      word = $2
      substituted = word.capitalize! || word
      substituted
    end
    string
  end
  
  def constantize(camel_cased_word)
    Object.const_get(camel_cased_word)
  end

  def route
    controller_name = camelize(@path.split('/')[1]) << "Controller"
    controller = constantize(controller_name)
    controller.new.send('index')
  end
end
```

## Conclusion

It's pretty cool to stroll in the foundational stuff of the Web and understand in a concrete way what is going on. There is so much more to understand only with. Just the Headers or Cookies, for example. 

But at least now we know how a Web Server can serve the response from our web application. Of course, this is a simplistic version, and it lacks many things. 

We also did not use Rack to interface our web application and web servers. But I wanted to keep it as straightforward as possible to understand the bare bones of Web servers.