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
- It adds http header's info
- It represents characters(a CRLF) between header and body
- 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
- It's a setting for the interface which Rack provides
- It retuns three values, status, header, body
- 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)