【问题标题】:Ruby readpartial and read_nonblock not throwing EOFErrorRuby readpartial 和 read_nonblock 不抛出 EOFError
【发布时间】:2012-11-08 23:26:52
【问题描述】:

我试图理解和重新创建一个简单的预分叉服务器,就像独角兽一样,服务器在启动时分叉 4 个进程,这些进程都在控制套接字上等待(接受)。

控制套接字@control_socket 绑定到 9799 并生成 4 个等待接受连接的工作人员。每个工人所做的工作如下

def spawn_child
  fork do
    $STDOUT.puts "Forking child #{Process.pid}"
    loop do 
      @client = @control_socket.accept                                        
      loop do                     
        request = gets              

        if request                          
            respond(@inner_app.call(request))                           
        else
            $STDOUT.puts("No Request")
            @client.close                           
        end
      end
    end
  end
end

我使用了一个非常简单的机架应用程序,它只返回一个状态码为 200 的字符串和一个 text/html 的 Content-Type。

我面临的问题是,当我使用gets 而不是readread_partial 或@ 读取传入请求(通过点击“http://localhost:9799”处的url)时,我的服务器正常工作987654328@。当我使用非阻塞读取时,它似乎永远不会抛出 EOFError,根据我的理解,这意味着它没有收到 EOF 状态。

这会导致读取loop 无法完成。这是完成这项工作的代码 sn-p。

# Reads a file using IO.read_nonblock
# Returns end of file when using get but doesn't seem to return 
# while using read_nonblock or readpartial
        # The fact that the method is named gets is just bad naming, please ignore
def gets
  buffer = ""         
  i =0
  loop do
    puts "loop #{i}"
    i += 1
    begin
      buffer << @client.read_nonblock(READ_CHUNK)
      puts "buffer is #{buffer}"
    rescue  Errno::EAGAIN => e
      puts "#{e.message}"
      puts "#{e.backtrace}"
      IO.select([@client])
      retry
    rescue EOFError
      $STDOUT.puts "-" * 50
      puts "request data is #{buffer}"    
      $STDOUT.puts "-" * 50
      break           
    end
  end
  puts "returning buffer"
  buffer
end

但是,如果我使用简单的gets 而不是readread_nonblock,或者如果将IO.select([@client]) 替换为break,则代码可以完美运行。

这是代码工作并返回响应的时间。我打算使用 read_nonblock 的原因是 unicorn 使用了一个等效的 kgio 库,它实现了非阻塞读取。

def gets
  @client.gets
end

接下来粘贴整个代码。

module Server   
  class Prefork
    # line break 
    CRLF  = "\r\n"
    # number of workers process to fork
    CONCURRENCY = 4
    # size of each non_blocking read
    READ_CHUNK = 1024

    $STDOUT = STDOUT
    $STDOUT.sync

    # creates a control socket which listens to port 9799
    def initialize(port = 21)
      @control_socket = TCPServer.new(9799)
      puts "Starting server..."
      trap(:INT) {
        exit
      }
    end

    # Reads a file using IO.read_nonblock
    # Returns end of file when using get but doesn't seem to return 
    # while using read_nonblock or readpartial
    def gets
      buffer = ""         
      i =0
      loop do
        puts "loop #{i}"
        i += 1
        begin
          buffer << @client.read_nonblock(READ_CHUNK)
          puts "buffer is #{buffer}"
        rescue  Errno::EAGAIN => e
          puts "#{e.message}"
          puts "#{e.backtrace}"
          IO.select([@client])
                              retry
        rescue EOFError
          $STDOUT.puts "-" * 50
          puts "request data is #{buffer}"    
          $STDOUT.puts "-" * 50
          break           
        end
      end
      puts "returning buffer"
      buffer
    end

    # responds with the data and closes the connection
    def respond(data)
      puts "request 2 Data is #{data.inspect}"
      status, headers, body = data
      puts "message is #{body}"
      buffer = "HTTP/1.1 #{status}\r\n" \
               "Date: #{Time.now.utc}\r\n" \
               "Status: #{status}\r\n" \
               "Connection: close\r\n"            
      headers.each {|key, value| buffer << "#{key}: #{value}\r\n"}          
      @client.write(buffer << CRLF)
      body.each {|chunk| @client.write(chunk)}            
    ensure 
      $STDOUT.puts "*" * 50
      $STDOUT.puts "Closing..."
      @client.respond_to?(:close) and @client.close
    end

    # The main method which triggers the creation of workers processes
    # The workers processes all wait to accept the socket on the same
    # control socket allowing the kernel to do the load balancing.
    # 
    # Working with a dummy rack app which returns a simple text message
    # hence the config.ru file read.
    def run         
      # copied from unicorn-4.2.1
      # refer unicorn.rb and lib/unicorn/http_server.rb           
      raw_data = File.read("config.ru")           
      app = "::Rack::Builder.new {\n#{raw_data}\n}.to_app"
      @inner_app = eval(app, TOPLEVEL_BINDING)
      child_pids = []
      CONCURRENCY.times do
        child_pids << spawn_child
      end

      trap(:INT) {
        child_pids.each do |cpid|
          begin 
            Process.kill(:INT, cpid)
          rescue Errno::ESRCH
          end
        end

        exit
      }

      loop do
        pid = Process.wait
        puts "Process quit unexpectedly #{pid}"
        child_pids.delete(pid)
        child_pids << spawn_child
      end
    end

    # This is where the real work is done.
    def spawn_child
      fork do
        $STDOUT.puts "Forking child #{Process.pid}"
        loop do 
          @client = @control_socket.accept                                        
          loop do                     
            request = gets              

            if request                          
              respond(@inner_app.call(request))                           
            else
              $STDOUT.puts("No Request")
              @client.close                           
            end
          end
        end
      end
    end
  end
end

p = Server::Prefork.new(9799)
p.run

有人可以向我解释为什么读取会因“read_partial”或“read_nonblock”或“read”而失败。我真的很感激这方面的一些帮助。

谢谢。

【问题讨论】:

  • 您描述的行为与文档 EOFErrorread_nonblock 等所说的相反。 get 应该返回 nilread_nonblock 应该提高 EOFError
  • 如果你只启动一个工人会发生什么?在spawn_child 方法中分配一个实例变量@client 对我来说很奇怪。每个工人不会覆盖该变量吗?或者,fork 是否建立了自己的上下文?

标签: ruby unix nonblocking unicorn preforking


【解决方案1】:

首先我想讲一些基础知识,EOF 表示文件结束,就像当数据源中没有更多数据可以读取时,信号会发送给调用者,例如,打开一个文件并读取整个文件后将收到一个 EOF,或者只是简单地关闭 io 流。

那么这4种方法有几个区别

  • gets 从流中读取一行,在 ruby​​ 中它使用$/ 作为默认的行分隔符,但是你可以传递一个参数作为行分隔符,因为如果客户端和服务器不是同一个操作系统, 行分隔符可能不同,它是一个 block 方法,如果从未遇到行分隔符或 EOF 将阻塞,并在收到 EOF 时返回 nil,因此 gets 永远不会遇到EOFError

  • read(length)从流中读取length字节,这是一个block方法,如果省略length则阻塞直到读取EOF,如果有length则只返回一次读取一定量的数据或遇到EOF,收到EOF时返回空字符串,所以read永远不会遇到EOFError

  • readpartial(maxlen)从流中读取最多maxlen字节,它会读取可用数据并立即返回,有点像read的eager版本,如果数据太大可以使用readpartial代替read 以防止阻塞,但它仍然是一个 block 方法,如果没有立即可用的数据,它会阻塞,readpartial 如果收到 EOF 将引发 EOFError.

  • read_nonblock(maxlen) 有点像readpartial,但就像名字所说的那样,它是一个非阻塞方法,即使没有可用的数据,它也会立即引发Errno::EAGAIN,这意味着现在没有数据,你应该关心这个错误,通常在Errno::EAGAIN救援子句应该首先调用IO.select([conn])以减少不必要的循环,它会阻塞直到conn变得可以读取,然后retryread_nonblock将引发如果收到 EOF,则为 EOFError

现在让我们看看你的例子,因为我看到你正在做的是首先尝试通过“点击 url”来读取数据,这只是一个 HTTP GET 请求,一些文本,如“GET / HTTP/1.1\r\n” , 默认情况下,连接在 HTTP/1.1 中保持活动状态,因此使用 readpartialread_nonblock 将永远不会收到 EOF,除非在您的请求中放入 Connection: close 标头,或者更改您的获取方法如下:

buffer = ""
if m = @client.gets
  buffer << m
  break if m.strip == ""
else
  break
end
buffer

这里不能使用read,因为你不知道请求包的确切长度,使用大长度或者直接省略会导致阻塞。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-02-28
    • 2011-10-02
    • 1970-01-01
    • 2011-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-20
    相关资源
    最近更新 更多