使用 TCPServer 在 Ruby 中创建简单的 HTTP 服务器

10

为了完成学校作业,我正在尝试使用Ruby和sockets库创建一个简单的HTTP服务器。

目前,我可以让它回应任何连接并简单地打印“hello”:

require 'socket'

server = TCPServer.open 2000
puts "Listening on port 2000"

loop {
  client = server.accept()
  resp = "Hello?"
  headers = ["HTTP/1.1 200 OK",
             "Date: Tue, 14 Dec 2010 10:48:45 GMT",
             "Server: Ruby",
             "Content-Type: text/html; charset=iso-8859-1",
             "Content-Length: #{resp.length}\r\n\r\n"].join("\r\n")
  client.puts headers
  client.puts resp
  client.close
}

这个代码按照预期工作。但是,当我要求服务器告诉我刚刚连接的用户时

puts "Client: #{client.addr[2]}"

当我使用Chromium(浏览器)连接到localhost:2000/时(仅一次),我收到以下信息:

Client: 127.0.0.1
Client: 127.0.0.1
Client: 127.0.0.1
Client: 127.0.0.1

我认为这是Chromium请求辅助文件,例如favicon.ico,而不是我的脚本执行了一些奇怪的操作,因此我想调查一下传入的请求。我用以下代码替换了resp = "Hello?"行:

resp = client.read()

我重新启动了服务器。在Chromium中重新发送请求后,它没有立即返回,而是一直卡住了。同时,在我的服务器输出中,我得到了Client: 127.0.0.1。我按下了Chromium中的"停止"按钮,然后服务器崩溃了。

server.rb:16:in `write': Broken pipe (Errno::EPIPE)
    from server.rb:16:in `puts'
    from server.rb:16:in `block in <main>'
    from server.rb:6:in `loop'
    from server.rb:6:in `<main>'

显然,我做错了什么,因为期望的行为是将传入的请求作为响应发送回去。

我漏掉了什么?


你发现为什么它会打印四次“Client: 127.0.0.1”了吗? - Ricardo de Cillo
不,但几乎可以确定是因为Chrome正在请求辅助文件,例如favicon.ico。如果您输出HTTP请求的第一行,就可以看到确切的请求内容。 - Austin Hyde
1个回答

21

我对Chrome和四个连接并不了解,但我会尝试回答你如何正确读取请求的问题。

首先,IO#read 在这种情况下无法正常工作。根据文档,没有任何参数的 read 会一直读取直到遇到EOF,但是这里没有这样的情况。套接字是一个无限流,您将无法使用该方法读取整个消息,因为套接字没有“完整”的消息。您可以使用带有整数的读取,例如read(100)之类的内容,但是那也将在某个时候被阻塞。

基本上,读取套接字与读取文件非常不同。套接字是异步更新的,完全独立于您尝试读取它的时间。如果您请求10个字节,此代码段中可能仅有5个字节可用。使用阻塞 IO,read(10)调用将挂起并等待直到再有5个字节可用或连接关闭。这意味着,如果您不断地尝试读取10个字节的数据包,在某些时候,它仍然会挂起。另一种读取套接字的方式是使用非阻塞IO,但在您的情况下并不是非常重要,而且它本身也是一个很长的话题。

下面是一个使用阻塞IO访问数据的示例:

loop {
  client = server.accept

  while line = client.gets
    puts line.chomp
    break if line =~ /^\s*$/
  end

  # rest of loop ...
}

gets 方法会尝试从套接字读取数据,直到遇到换行符为止。对于 HTTP 请求,这一点 一定会 发生,因此即使整个消息分批传输,gets 应该返回输出的单行内容。调用 line.chomp 会将末尾的换行符去掉。如果读取的行为空,则表示 HTTP 头已经传输完成,可以安全地跳出循环(当然也可以将其放在 while 条件中)。请求将被转储到启动服务器的控制台上。如果你真的想将其发送回浏览器,思路是一样的,只需要以不同的方式处理行:

loop {
  client = server.accept

  lines = []
  while line = client.gets and line !~ /^\s*$/
    lines << line.chomp
  end

  resp = lines.join("<br />")
  headers = ["http/1.1 200 ok",
            "date: tue, 14 dec 2010 10:48:45 gmt",
            "server: ruby",
            "content-type: text/html; charset=iso-8859-1",
            "content-length: #{resp.length}\r\n\r\n"].join("\r\n")
  client.puts headers          # send the time to the client
  client.puts resp
  client.close
}

关于“broken pipe”的错误,是由于浏览器在read正在尝试访问数据时强制中断连接造成的。


感谢您的精彩回答。这正是我所需要的信息。 - Sid
1
我承认我对HTTP规范的细节不是很熟悉,@I_do_python。我尝试了一个简单的测试,使用curl -X POST http://localhost:2000,看起来工作正常。你能给我一个出错的例子,并且提供一个建议,让我在我的回答中修复它吗? - Andrew Radev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接