ruby http 响应脚本发送大型 gzip 压缩内容

ruby http response script sending large gzipped content

我正在尝试设置一个用 ruby 编写的脚本来打开服务器上的端口 2004。使用 http 调用服务器,然后端口 http://<server>:2004/ 将返回一个 HTTP header + 响应。 从文件中读取响应。 这适用于小内容,但不适用于 50MB。 不知何故,它只是打破了。 顺便说一句,我正在用 SoapUI.

测试这个脚本

这是源代码,我觉得这个很好self-explanatory。 为了更好的阅读,我将响应部分标记为大。

#!/bin/ruby

require 'socket'
require 'timeout'
require 'date'

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

#file="dump.request"

loop {
    Thread.start(server.accept) do |client|
        date = Time.now.strftime("%d-%m-%Y_%H-%M-%S")
        file = "#{date}_mt_dump.txt"
        puts date
        puts "Accepting connection"
        #client = server.accept
        #resp = "OKY|So long and thanks for all the fish!|OKY"
        ticket_id = "1235"


        partial_data = ""
        i = 1024
        firstrun = "yes"
        fd = File.open(file,'w')
        puts "Attempting receive loop"

        puts "Ready to transfer contents to the client"
        f = File.open("output.txt.gz","r")
        puts "Opened file output.txt.gz; size: #{f.size}"
        resp = f.read(f.size)

        headers = ["HTTP/1.1 200 OK",
             "Content-Encoding: gzip",
             "Content-Type: text/xml;charset=UTF-8",
             "Content-Length: #{f.size}\r\n\r\n"].join("\r\n")
        client.puts headers

        #puts all_data.join()
        fd.close unless fd == nil

        puts "Start data transfer"
        client.puts resp
        client.close
        puts "Closed connection"
        puts "\n"
    end
}

我发现您的代码存在许多问题,有些是概念性的,有些是技术性的,但如果没有关于您收到的错误的更多信息,可能无法提供正确的响应。

我最初认为这个问题是由于您在打开 Gzip 文件时没有使用二进制模式标志引起的,因此文件读取停止吃了第一个 EOF 字符并且换行符可能被转换。

需要考虑的一些技术问题:

  1. 你的循环是无限的。您真的应该设置信号陷阱以允许您退出脚本(例如捕获 ^C)。

  2. Zip 文件通常是二进制文件。您应该使用二进制模式打开文件,或者如果您将整个文件加载到内存,则使用 IO.binread 方法。

  3. 您在发送之前将整个文件加载到内存中。这对于小文件来说非常好,但对于大文件来说并不是最好的方法。为每个客户端加载 50MB 的 RAM,同时服务 100 个客户端,意味着 5GB 的 RAM...

考虑到前两个技术点,我会把代码改成这样:

keep_running = true
trap('INT'){ keep_running = false ; raise ::SystemExit}

begin
    while(run) {
        Thread.start(server.accept) do |client|
            date = Time.now.strftime("%d-%m-%Y_%H-%M-%S")
            file = "#{date}_mt_dump.txt"
            puts date
            puts "Accepting connection"
            #client = server.accept
            #resp = "OKY|So long and thanks for all the fish!|OKY"
            ticket_id = "1235"


            partial_data = ""
            i = 1024
            firstrun = "yes"
            fd = File.open(file,'bw')
            puts "Attempting receive loop"

            puts "Ready to transfer contents to the client"
            f = File.open("output.txt.gz","br")
            puts "Opened file output.txt.gz; size: #{f.size}"
            resp = f.read(f.size)

            headers = ["HTTP/1.1 200 OK",
                 "Content-Encoding: gzip",
                 "Content-Type: text/xml;charset=UTF-8",
                 "Content-Length: #{f.size}\r\n\r\n"].join("\r\n")
            client.puts headers

            #puts all_data.join()
            fd.close unless fd == nil

            puts "Start data transfer"
            client.puts resp
            client.close
            puts "Closed connection"
            puts "\n"
        end
    }
rescue => e
    puts e.message
    puts e.backtrace
rescue SystemExit => e
    puts "exiting... please notice that existing threads will be brutally stoped, as we will not wait for them..."
end

至于我更一般的指示:

  1. 您的代码为每个连接打开一个新线程。虽然这对于少量并发连接没有问题,但如果您有大量并发连接,您的脚本可能会停止运行。单独的上下文切换(在线程之间移动)可能会造成 DoS 情况。

    我建议您使用具有线程池的 Reactor 模式。另一种选择是分叉几个监听同一个 TCPSocket 的进程。

  2. 您不从套接字读取数据,也不解析 HTTP 请求 - 这意味着有人可能会通过连续发送来填满您永远不会清空的系统缓冲区数据。

    最好从套接字中读取信息,或者清空它的缓冲区,并断开任何格式错误的恶意连接。

    此外,当响应在请求之前到达时,大多数浏览器都不太高兴...

  3. 您不会捕获任何异常,也不会打印任何错误消息。这意味着您的脚本可能会抛出一个异常,从而破坏一切。例如,如果您的 'server' 将到达其进程的 'open file limit',则 accept 方法将抛出一个异常,该异常将关闭整个脚本,包括现有连接。

我不确定您为什么不使用可用于 Ruby 的众多 HTTP 服务器之一 - 无论是内置 WEBrick(不要用于生产)还是本机 Ruby 社区宝石,例如 Iodine.

这是一个使用 Iodine 的简短示例,它有一个使用 Ruby 编写的易于使用的 Http 服务器(无需编译任何东西):

require 'iodine/http'

# cache the file, since it's the only response ever sent
file_data = IO.binread "output.txt.gz"

Iodine.on_http do |request, response|
        begin
            # set any headers
            response['content-type'] = 'text/xml;charset=UTF-8'
            response['content-encoding'] = 'gzip'
            response << file_data
            true
        rescue => e
            Iodine.error e
            false
        end
    end
end

#if in irb:
exit

或者,如果你坚持自己编写 HTTP 服务器,你至少可以使用一个可用的 IO 反应器,例如 Iodine(我为 Plezi), 帮助你处理线程池和 IO 管理(你也可以使用 EventMachine,但我不太喜欢 - 再一次,我有偏见,因为我写了 Iodine Library):

require 'iodine'
require 'stringio'

class MiniServer < Iodine::Protocol

    # cache the file, since it's the only data sent,
    # and make it available to all the connections.
    def self.data
        @data ||= IO.binread 'output.txt.gz'
    end

    # The on_opne callback is called when a connection is established.
    # We'll use it to initialize the HTTP request's headers Hash.
    def on_open
     @headers = {}
    end

    # the on_message callback is called when data is sent from the client to the socket.
    def on_message input
        input = StringIO.new input
        l = nil
        headers = @headers # easy access
        # loop the lines and parse the HTTP request.
        while (l = input.gets)
            unless l.match /^[\r]?\n/
                if l.include? ':'
                    l = l.strip.downcase.split(':', 2)
                    headers[l[0]] = l[1]
                else
                    headers[:method], headers[:query], headers[:version] = l.strip.split(/[\s]+/, 3)
                    headers[:request_start] = Time.now
                end
                next
            end
            # keep the connection alive if the HTTP version is 1.1 or if the connection is requested to be kept alive
            keep_alive = (headers['connection'].to_s.match(/keep/i) || headers[:version].match(/1\.1/)) && true
            # refuse any file uploads or forms. make sure the request is a GET request
            return close if headers['content-length'] || headers['content-type'] || headers[:method].to_s.match(/get/i).nil?
            # all is well, send the file.
            write ["HTTP/1.1 200 OK",
                    "Connection: #{keep_alive ? 'keep-alive' : 'close'}",
                     "Content-Encoding: gzip",
                     "Content-Type: text/xml;charset=UTF-8",
                     "Content-Length: #{self.class.data.bytesize}\r\n\r\n"].join("\r\n")
            write self.class.data
            return close unless keep_alive

            # reset the headers, in case another request comes in
            headers.clear
        end
    end

end

Iodine.protocol = MiniServer
# # if running within a larget application, consider:
# Iodine.force_start!
# # Server starts automatically when the script ends.
# # on irb, use `exit`:
exit

祝你好运!