用 Ruby 构建一个 30 行代码的 HTTP 服务器
最初发表于blog.appsignal.com/2016/11/23/ruby-magic-building-a-30-line-http-server-in-ruby.html,日期为 2016 年 11 月 23 日。
Web 服务器,以及 HTTP 协议本身,可能看起来难以理解。浏览器如何格式化请求?响应又是如何发送给用户的?在本期 Ruby Magic 节目中,我们将学习如何用 30 行代码构建一个 Ruby HTTP 服务器。完成后,我们的服务器将处理 HTTP GET 请求,我们将使用它来运行一个 Rack 应用。
HTTP 和 TCP 如何协同工作
TCP是一种传输协议,它描述了服务器和客户端如何交换数据。
HTTP是一种请求-响应协议,它专门描述了 Web 服务器如何与 HTTP 客户端或 Web 浏览器交换数据。HTTP 通常使用 TCP 作为其传输协议。本质上,HTTP 服务器是一个“说”HTTP 协议的 TCP 服务器。
# tcp_server.rb
require 'socket'
server = TCPServer.new 5678
while session = server.accept
session.puts "Hello world! The time is #{Time.now}"
session.close
end
在这个 TCP 服务器示例中,服务器绑定到端口5678并等待客户端连接。连接建立后,服务器向客户端发送一条消息,然后关闭连接。与第一个客户端通信完毕后,服务器会等待另一个客户端连接并再次发送消息。
# tcp_client.rb
require 'socket'
server = TCPSocket.new 'localhost', 5678
while line = server.gets
puts line
end
server.close
要连接到我们的服务器,我们需要一个 TCP 客户端。这个示例客户端连接到同一个端口(5678),并使用server.gets该端口接收来自服务器的数据,然后将数据打印出来。当它停止接收数据时,它会关闭与服务器的连接,程序也将退出。
当您启动服务器时,服务器正在运行($ ruby tcp_server.rb),您可以在单独的选项卡中启动客户端以接收服务器的消息。
$ ruby tcp_client.rb
Hello world! The time is 2016-11-23 15:17:11 +0100
$
稍加想象,我们的 TCP 服务器和客户端的工作方式有点像 Web 服务器和浏览器。客户端发送请求,服务器响应,然后连接关闭。这就是请求-响应模式的工作原理,而这正是我们构建 HTTP 服务器所需要的。
在进入正题之前,让我们先来看看 HTTP 请求和响应是什么样子的。
一个基本的 HTTP GET 请求
最基本的HTTP GET 请求是不带任何附加标头或请求体的请求行。
GET / HTTP/1.1\r\n
请求线路由四部分组成:
- 方法标记(
GET在本例中为,) - 请求 URI(
/) - 协议版本(
HTTP/1.1) - CRLF(回车符
\r,后跟换行符\n)表示行的结束
服务器将返回一个HTTP 响应,其内容可能如下所示:
HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n\Hello world!
此回复包含以下内容:
- 状态行:协议版本(“HTTP/1.1”),后跟一个空格,响应状态码(“200”),最后以 CRLF 结尾
\r\n。 - 可选的页眉行。在本例中,只有一个页眉行(“Content-Type: text/html”),但可以有多个(用 CRLF 分隔
\r\n)。 - 使用换行符(或双 CRLF)将状态行和标题与正文分隔开:(
\r\n\r\n) - 正文:“你好,世界!”
一个极简的 Ruby HTTP 服务器
好了,废话不多说。既然我们已经了解了如何在 Ruby 中创建 TCP 服务器,以及一些 HTTP 请求和响应的示例,就可以构建一个最小的 HTTP 服务器了。你会发现,这个 Web 服务器看起来和我们之前讨论的 TCP 服务器非常相似。基本思路是一样的,我们只是使用了 HTTP 协议来格式化消息。另外,因为我们会使用浏览器来发送请求和解析响应,所以这次不需要实现客户端。
# http_server.rb
require 'socket'
server = TCPServer.new 5678
while session = server.accept
request = session.gets
puts request
session.print "HTTP/1.1 200\r\n" # 1
session.print "Content-Type: text/html\r\n" # 2
session.print "\r\n" # 3
session.print "Hello world! The time is #{Time.now}" #4
session.close
end
服务器收到请求后,像以前一样,它会session.print向客户端发送一条消息:但这次,响应中不仅包含我们的消息,还添加了状态行、标头和换行符:
- 状态行(
HTTP 1.1 200\r\n)告诉浏览器HTTP版本为1.1,响应代码为“200”。 - 用于指示响应内容类型为 text/html 的标头(
Content-Type: text/html\r\n) - 换行符(
\r\n) - 正文:“你好,世界!……”
和之前一样,发送消息后它会关闭连接。我们还没有读取请求,所以目前只是将其打印到控制台。
启动服务器后,在浏览器中打开http://localhost:5678,你应该会看到“Hello world! …”一行,并显示当前时间,就像我们之前从 TCP 客户端收到的那样。🎉
Serving a Rack 应用程序
到目前为止,我们的服务器对每个请求都只返回一个响应。为了使其更加实用,我们可以向服务器添加更多响应。我们不直接向服务器添加这些响应,而是使用Rack应用。我们的服务器将解析 HTTP 请求并将其传递给 Rack 应用,然后由 Rack 应用返回一个响应,供服务器发送给客户端。
Rack 是支持 Ruby 的 Web 服务器与大多数 Ruby Web 框架(例如 Rails 和 Sinatra)之间的接口。最简单的 Rack 应用是一个对象,它响应call并返回一个“tiplet”,这是一个包含三个元素的数组:HTTP 响应代码、HTTP 标头哈希和响应体。
app = Proc.new do |env|
['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end
在这个例子中,响应代码为“200”,我们通过标头传递“text/html”作为内容类型,并且正文是一个包含字符串的数组。
为了让我们的服务器能够处理来自此应用程序的响应,我们需要将返回的三元组转换为 HTTP 响应字符串。与之前始终返回静态响应不同,现在我们需要根据 Rack 应用程序返回的三元组构建响应。
# http_server.rb
require 'socket'
app = Proc.new do
['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end
server = TCPServer.new 5678
while session = server.accept
request = session.gets
puts request
# 1
status, headers, body = app.call({})
# 2
session.print "HTTP/1.1 #{status}\r\n"
# 3
headers.each do |key, value|
session.print "#{key}: #{value}\r\n"
end
# 4
session.print "\r\n"
# 5
body.each do |part|
session.print part
end
session.close
end
为了响应 Rack 应用的反馈,我们将对服务器进行一些更改:
- 从返回的三元组中获取状态码、标头和正文
app.call。 - 使用状态码构建状态行
- 遍历哈希表的头部,并为哈希表中的每个键值对添加一个头部行。
- 打印一个换行符,将状态行和标题与正文分隔开。
- 遍历主体并打印每个部分。由于主体数组中只有一个部分,因此它会在关闭会话之前向会话打印“Hello world”消息。
阅读请求
此前,我们的服务器一直忽略这个request变量。因为我们的 Rack 应用始终返回相同的响应,所以没有必要这样做。
Rack::Lobster这是一个随 Rack 一起提供的示例应用程序,它使用请求 URL 参数来运行。从现在开始,我们将使用这个示例应用程序作为测试应用程序,而不是之前用作应用程序的 Proc。
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
app = Rack::Lobster.new
server = TCPServer.new 5678
while session = server.accept
# ...
现在打开浏览器,屏幕上会显示一只龙虾,而不是之前打印的无聊字符串。太棒了!
“翻转!”和“崩溃!”链接分别指向[此处应填写链接]/?flip=left和/?flip=crash[此处应填写链接]。然而,点击这些链接后,龙虾并没有翻转,也没有发生任何崩溃。这是因为我们的服务器目前还无法处理查询字符串。还记得request我们之前忽略的那个变量吗?如果我们查看服务器日志,就能看到每个页面的请求字符串。
GET / HTTP/1.1
GET /?flip=left HTTP/1.1
GET /?flip=crash HTTP/1.1
HTTP 请求字符串包含请求方法(“GET”)、请求路径(`/` /、` /?flip=left/` 和/?flip=crash`/`)以及 HTTP 版本。我们可以利用这些信息来确定需要提供哪些服务。
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
app = Rack::Lobster.new
server = TCPServer.new 5678
while session = server.accept
request = session.gets
puts request
# 1
method, full_path = request.split(' ')
# 2
path, query = full_path.split('?')
# 3
status, headers, body = app.call({
'REQUEST_METHOD' => method,
'PATH_INFO' => path,
'QUERY_STRING' => query
})
session.print "HTTP/1.1 #{status}\r\n"
headers.each do |key, value|
session.print "#{key}: #{value}\r\n"
end
session.print "\r\n"
body.each do |part|
session.print part
end
session.close
end
为了解析请求并将请求参数发送到 Rack 应用程序,我们将把请求字符串拆分并发送到 Rack 应用程序:
- 将请求字符串拆分为方法和完整路径。
- 将完整路径拆分为路径和查询语句
- 将这些值以Rack 环境哈希的形式传递给我们的应用程序。
例如,类似这样的请求GET /?flip=left HTTP/1.1\r\n会像这样传递给应用程序:
{
'REQUEST_METHOD' => 'GET',
'PATH_INFO' => '/',
'QUERY_STRING' => '?flip=left'
}
重启服务器后,访问http://localhost:5678,点击“翻转!”链接即可翻转龙虾;点击“崩溃!”链接即可使我们的 Web 服务器崩溃。
我们只是浅尝辄止地了解了HTTP服务器的实现,我们的服务器只有30行代码,但它解释了基本原理。它接收GET请求,将请求属性传递给Rack应用程序,并将响应发送回浏览器。虽然它不处理请求流和POST请求之类的操作,但理论上我们的服务器也可以用来为其他Rack应用程序提供服务。
以上就是我们关于如何使用 Ruby 构建 HTTP 服务器的简要介绍。如果您想亲自体验一下我们的服务器,代码在这里。如果您想了解更多信息或有任何具体问题,请在@AppSignal联系我们。
如果您喜欢这篇文章,请订阅 Ruby Magic 时事通讯:每月(大致)发送一期 Ruby 相关内容。
文章来源:https://dev.to/appsignal/building-a-30-line-http-server-in-ruby-1p2m

