I Listen, I Promise
Recently, I recieved a comment on one of my previous posts, Creating a Socket Server and Client in Ruby, that asked about the next step towards taking the simple example to make a working web server. Well, you’ve been heard and I have written up code to do just that. It handles retrieval and sending of headers, handling GET requests, and responding with status.
Request and Response
class RequestInfo
attr_accessor :headers, :method, :resource
def initialize
@headers = {}
end
end
class ResponseInfo
attr_accessor :status, :headers, :body
def initialize
@headers = {}
@body = String.new
end
end
These classes are short and are designed simply to contain the information coming in and going out of the webserver. RequestInfo’s method is used to obtain the method of the request (GET, etc..). Resource is the requested file from the connecting client. Finally, headers is a map of key value pairs corresponding with headers.
ResponseInfo consists of status, which is the first line returned containing 200 OK or 404 Not Found; body, the content of what’s actually responded with; and headers, the map of headers of the response.
Populate the Request
request = RequestInfo.new
response = ResponseInfo.new
while((current = s.readline).chomp! != "")
if current[0,3] == "GET"
methodline = current.split
request.method = methodline[0]
request.resource = methodline[1]
elsif current.match ':'
headerline = current.split ':',2
request.headers[headerline[0]] = headerline[1]
end
end
This segment handles populating the RequestInfo object, by going through the sent request line by line from the TCPSocket connection s. It retrieves method, resources, and headers in a single while loop. The headers are retrieved by splitting on the first colon in each line that contains one.
Populate the Respone
begin
File.open("public/#{request.resource}","r") do |file|
while(curline = file.gets)
response.body << curline
end
end
response.status = "HTTP/1.0 200 OK"
response.headers["Content-Type"] = "text/html"
response.headers["Content-Length"] = response.body.length.to_s
rescue => err
puts err
response.status = "HTTP/1.0 404 Not Found"
response.headers["Content-Type"] = "text/html"
response.headers["Content-Length"] = "0"
response.body = ""
end
Within this segment, we read in a file (looking in a folder called public so the webserver.rb itself cannot be retrieved through a request), appending its contents to the response.body. Afterwards, if all is successful, we respond with a 200 OK along with some other headers like Content-Type and Content-Length. Typically, if there’s an issue it’s because the file cannot be found, so returning a 404 is appropriate.
Note: A suggestion for improving this example is reading the MIME type of the read file so the Content-Type is not hardcoded.
Write Back the Response
s.write response.status + "\n"
response.headers.each {|key,value| s.write "#{key}:#{value}\n"}
s.write "\n" + response.body
s.close
This writes to the socket connection s, in order, ResponseInfo’s status, headers, and body. Finally, the connection is closed.
All Together Now
class RequestInfo
attr_accessor :headers, :method, :resource
def initialize
@headers = {}
end
end
class ResponseInfo
attr_accessor :status, :headers, :body
def initialize
@headers = {}
@body = String.new
end
end
require "socket"
serv = TCPServer.new('jstaten.com',7881)
loop do
Thread.start(serv.accept) do |s|
request = RequestInfo.new
response = ResponseInfo.new
while((current = s.readline).chomp! != "")
if current[0,3] == "GET"
methodline = current.split
request.method = methodline[0]
request.resource = methodline[1]
elsif current.match ':'
headerline = current.split ':',2
request.headers[headerline[0]] = headerline[1]
end
end
if (request.resource == "/")
request.resource = "index.html"
end
begin
File.open("public/#{request.resource}","r") do |file|
while(curline = file.gets)
response.body << curline
end
end
response.status = "HTTP/1.0 200 OK"
response.headers["Content-Type"] = "text/html"
response.headers["Content-Length"] = response.body.length.to_s
rescue => err
puts err
response.status = "HTTP/1.0 404 Not Found"
response.headers["Content-Type"] = "text/html"
response.headers["Content-Length"] = "0"
response.body = ""
end
s.write response.status + "\n"
response.headers.each {|key,value| s.write "#{key}:#{value}\n"}
s.write "\n" + response.body
s.close
end
end
end
When the steps are all implemented, you have a working ruby web server. Surely there are improvements to be made upon this creation, but it’s an effective proof-of-concept that can be built off.
I appreciate the feedback from all visitors to my blog, and if you feel that you have any questions or suggestions, feel free to comment away.
Note: Made a few corrections suggested. Thanks!