DEV Community

Teruo Kunihiro
Teruo Kunihiro

Posted on

Easy ruby Web server

I read Working With Unix Processes. It's a very interesting book because I didn't know about Unix Processes. I've used Web servers in my work with not knowing how it does. So I wanted to write this article to remember what I learn.
In this article, I tried to creat easy web server using ruby.

What is a Web server

It accepts requests from clients(users), then it returns a response. Web server I'm creating now is very simple, it handles the HTTP GET requests correctly.

Launch TCP server

Ruby provides TCPServer class, it's easy to create TCP connection for me.
server.rb

require 'socket'

server = TCPServer.open('0.0.0.0', 5678)

while connection = server.accept
  connection.write "Hello world!!"
  connection.close 
end

Since keepalive is not supported we can close the client connection, immediately after writing the body.

I can launch a server to do ruby server.rb, you can access to curl http://localhost:5678, then it shows Hello world!!
server.rb handle and shuts down your request. I'm ready to make HTTP server because HTTP is on TCP, it's just added some header information.

Getting started on HTTP Server

Although now this communications on TCP layer, I have to create HTTP protocol to archive my goal.
As I said, HTTP needs some header information. So I add some HTTP headers

  head = "HTTP/1.1 200\r\n" \
  "Date: #{Time.now.httpdate}\r\n" \
  "Content-Length: #{body.length.to_s}\r\n" 

Whole of code

server.rb

require 'socket'
require 'time'

server = TCPServer.open('0.0.0.0', 5678)

while connection = server.accept

  body = "Hello world!"

  head = "HTTP/1.1 200\r\n" \
  "Date: #{Time.now.httpdate}\r\n" \
  "Content-Length: #{body.length.to_s}\r\n" 

  # 1
  connection.write head

  # 2
  connection.write "\r\n"

  # 3
  connection.write body

  session.close
  connection.close 
end
  1. It adds http header's info
  2. It represents characters(a CRLF) between header and body
  3. It add body(It'll be shown in browser. normally it's html, json and xml etc)

Then, finally we can see the response in your browser. It returns 200 with "Hello world" response if you access localhost:5678
And I could understand that HTTP is just added Header information on the TCP server.

Real world

This can catch only GET and response 200 always. It doesn't look like be a real Web server.
Then the next step I want to control status, header, and body as I want. At first, I install Rack it is a famous middleware to connect Web server and application(Like Rails).
Rack provides the just interface

The code will be like this

require 'socket'
require 'time'
require 'rack/utils'

server = TCPServer.open('0.0.0.0', 5678)

# 1
app = Proc.new do |env|
  body = "Hello world!"
  ['200', {'Content-Type' => 'text/html', "Content-Length" => body.length.to_s}, ["Hello world!"]]
end

while connection = server.accept
  # 2
  status, headers, body = app.call({})

  head = "HTTP/1.1 200\r\n" \
  "Date: #{Time.now.httpdate}\r\n" \
  "Status: #{Rack::Utils::HTTP_STATUS_CODES[status]}\r\n" 

  # 3
  headers.each do |k,v|
    head << "#{k}: #{v}\r\n"
  end

  connection.write "#{head}\r\n"

  # 3
  body.each do |part| 
    connection.write part
  end

  body.close if body.respond_to?(:close)

  connection.close 
end
  1. It's a setting for the interface which Rack provides
  2. It retuns three values, status, header, body
  3. It writes body and headeers. Since body and header will be array, each is used in this code

Reading request

You can get request data using connection.get.
connection.get is like this GET / HTTP/1.1. Then I can extract request path and method etc from client request.

# 1
method, full_path = request.split(' ')
# 2
path = full_path.split('?')

And in Proc.new I can route the process by each path.

app = Proc.new do |env|
  req = Rack::Request.new(env)
  case req.path
  when "/"
    body = "Hello world!"
    [200, {'Content-Type' => 'text/html', "Content-Length" => body.length.to_s}, [body]]
  when /^\/name\/(.*)/
    body = "Hello, #{$1}!"
    [200, {'Content-Type' => 'text/html', "Content-Length" => body.length.to_s}, [body]]
  else 
    [404, {"Content-Type" => "text/html"}, ["Ah!!!"]]
  end
end

Finally, it can do routing, Method and query parameters are easy to control following like this codes

The whole of code

require 'socket'
require 'time'
require 'rack'
require 'rack/utils'

# app = Rack::Lobster.new
server = TCPServer.open('0.0.0.0', 5678)

app = Proc.new do |env|
  req = Rack::Request.new(env)
  case req.path
  when "/"
    body = "Hello world!"
    [200, {'Content-Type' => 'text/html', "Content-Length" => body.length.to_s}, [body]]
  when /^\/name\/(.*)/
    body = "Hello, #{$1}!"
    [200, {'Content-Type' => 'text/html', "Content-Length" => body.length.to_s}, [body]]
  else 
    [404, {"Content-Type" => "text/html"}, ["Ah!!!"]]
  end
end

while connection = server.accept
  request = connection.gets
  # 1
  method, full_path = request.split(' ')
  # 2
  path = full_path.split('?')

  # 1
  status, headers, body = app.call({
    'REQUEST_METHOD' => method,
    'PATH_INFO' => path
  })

  head = "HTTP/1.1 200\r\n" \
  "Date: #{Time.now.httpdate}\r\n" \
  "Status: #{Rack::Utils::HTTP_STATUS_CODES[status]}\r\n" 

  # 1
  headers.each do |k,v|
    head << "#{k}: #{v}\r\n"
  end

  connection.write "#{head}\r\n"

  body.each do |part| 
    connection.write part
  end

  body.close if body.respond_to?(:close)

  connection.close 
end

Next => Easy prefork webserver(if I have a freeeeeee time)

Top comments (0)