发布于 2026-01-06 5 阅读
0

用 Ruby 构建一个 30 行代码的 HTTP 服务器

用 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
Enter fullscreen mode Exit fullscreen mode

在这个 TCP 服务器示例中,服务器绑定到端口5678并等待客户端连接。连接建立后,服务器向客户端发送一条消息,然后关闭连接。与第一个客户端通信完毕后,服务器会等待另一个客户端连接并再次发送消息。

# tcp_client.rb
require 'socket'
server = TCPSocket.new 'localhost', 5678

while line = server.gets
  puts line
end

server.close
Enter fullscreen mode Exit fullscreen mode

要连接到我们的服务器,我们需要一个 TCP 客户端。这个示例客户端连接到同一个端口(5678),并使用server.gets该端口接收来自服务器的数据,然后将数据打印出来。当它停止接收数据时,它会关闭与服务器的连接,程序也将退出。

当您启动服务器时,服务器正在运行($ ruby tcp_server.rb),您可以在单独的选项卡中启动客户端以接收服务器的消息。

$ ruby tcp_client.rb
Hello world! The time is 2016-11-23 15:17:11 +0100
$
Enter fullscreen mode Exit fullscreen mode

稍加想象,我们的 TCP 服务器和客户端的工作方式有点像 Web 服务器和浏览器。客户端发送请求,服务器响应,然后连接关闭。这就是请求-响应模式的工作原理,而这正是我们构建 HTTP 服务器所需要的。

在进入正题之前,让我们先来看看 HTTP 请求和响应是什么样子的。

一个基本的 HTTP GET 请求

最基本的HTTP GET 请求不带任何附加标头或请求体的请求行。

GET / HTTP/1.1\r\n
Enter fullscreen mode Exit fullscreen mode

请求线路由四部分组成:

  • 方法标记(GET在本例中为,)
  • 请求 URI(/
  • 协议版本(HTTP/1.1
  • CRLF(回车符\r,后跟换行符\n)表示行的结束

服务器将返回一个HTTP 响应,其内容可能如下所示:

HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n\Hello world!
Enter fullscreen mode Exit fullscreen mode

此回复包含以下内容:

  • 状态行:协议版本(“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
Enter fullscreen mode Exit fullscreen mode

服务器收到请求后,像以前一样,它会session.print向客户端发送一条消息:但这次,响应中不仅包含我们的消息,还添加了状态行、标头和换行符:

  1. 状态行(HTTP 1.1 200\r\n)告诉浏览器HTTP版本为1.1,响应代码为“200”。
  2. 用于指示响应内容类型为 text/html 的标头(Content-Type: text/html\r\n
  3. 换行符(\r\n
  4. 正文:“你好,世界!……”

和之前一样,发送消息后它会关闭连接。我们还没有读取请求,所以目前只是将其打印到控制台。

启动服务器后,在浏览器中打开http://localhost:5678,你应该会看到“Hello world! …”一行,并显示当前时间,就像我们之前从 TCP 客户端收到的那样。🎉

我们最小的 Ruby HTTP 服务器返回我们的

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
Enter fullscreen mode Exit fullscreen mode

在这个例子中,响应代码为“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
Enter fullscreen mode Exit fullscreen mode

为了响应 Rack 应用的反馈,我们将对服务器进行一些更改:

  1. 从返回的三元组中获取状态码、标头和正文app.call
  2. 使用状态码构建状态行
  3. 遍历哈希表的头部,并为哈希表中的每个键值对添加一个头部行。
  4. 打印一个换行符,将状态行和标题与正文分隔开。
  5. 遍历主体并打印每个部分。由于主体数组中只有一个部分,因此它会在关闭会话之前向会话打印“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
# ...
Enter fullscreen mode Exit fullscreen mode

现在打开浏览器,屏幕上会显示一只龙虾,而不是之前打印的无聊字符串。太棒了!

我们运行 Rack::Lobster 的极简 Ruby HTTP 服务器

“翻转!”和“崩溃!”链接分别指向[此处应填写链接]/?flip=left/?flip=crash[此处应填写链接]。然而,点击这些链接后,龙虾并没有翻转,也没有发生任何崩溃。这是因为我们的服务器目前还无法处理查询字符串。还记得request我们之前忽略的那个变量吗?如果我们查看服务器日志,就能看到每个页面的请求字符串。

GET / HTTP/1.1
GET /?flip=left HTTP/1.1
GET /?flip=crash HTTP/1.1
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

为了解析请求并将请求参数发送到 Rack 应用程序,我们将把请求字符串拆分并发送到 Rack 应用程序:

  1. 将请求字符串拆分为方法和完整路径。
  2. 将完整路径拆分为路径和查询语句
  3. 将这些值以Rack 环境哈希的形式传递给我们的应用程序。

例如,类似这样的请求GET /?flip=left HTTP/1.1\r\n会像这样传递给应用程序:

{
  'REQUEST_METHOD' => 'GET',
  'PATH_INFO' => '/',
  'QUERY_STRING' => '?flip=left'
}
Enter fullscreen mode Exit fullscreen mode

重启服务器后,访问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