【问题标题】:Lua read chunked request body if header specified如果指定了标头,Lua 会读取分块的请求正文
【发布时间】:2020-09-17 02:35:38
【问题描述】:

我们使用 Nginx + Lua 并希望根据this 解决方法支持分块上传,这通常是有效的。我的问题是如何像往常一样处理上传请求 - 使用标头、正文、eof:

                local form, err = upload:new(chunk_size)
                if not form then
                    ngx.log(ngx.ERR, "failed to new upload: ", err)
                    ngx.exit(500)
                end

                form:set_timeout(1000) -- 1 sec

                while true do
                    local typ, res, err = form:read()
                    if not typ then
                        ngx.say("failed to read: ", err)
                        return
                    end

                    ngx.say("read: ", cjson.encode({typ, res}))

                    if typ == "eof" then
                        break
                    end
                end

当我将上传标头 -H "Transfer-Encoding: chunked" 分块时,使用该 chunk 脚本。

对不起,如果这是显而易见的事情,但经过几天的谷歌搜索后,我没有看到任何示例。 但我的建议是:

# read headers
ngx.req.get_headers()

#read body:
ngx.req.get_body_data()

然后我不需要form:read() 并遍历表单数组直到eof。感谢任何链接、示例。

卷曲示例:

curl -X PUT localhost:8080/test -F file=@./myfile -H "Transfer-Encoding: chunked"

【问题讨论】:

  • 为了清楚起见——分块传输编码使用 with form-data(我的意思是,首先上传的文件使用 multipart/form-data 编码,然后是请求正文,即表单,是使用分块传输编码进行编码的)还是与原始文件一起使用的分块传输编码(即,没有表单数据)?
  • 现在只是一个带有标题-H "Transfer-Encoding: chunked"的文件请求
  • 因此,根据请求标头,您希望解码分块正文并将其视为上传文件(如果存在transfer-encoding: chunked 标头)或从multipart/form-data 的某些部分提取上传文件(如果content-type: multipart/form-data 存在),我理解正确吗?
  • 我们以 multipart 方式只加载一个文件,并希望按块发送(-H "Transfer-Encoding: chunked")
  • 嗯,我需要再次澄清一下。您的 HTTP 客户端将一个文件作为 multipart/form-data 表单的一部分发送,并且请求正文(即表单本身)使用分块编码进行编码。因此,客户端应发送以下两个标头:content-type: multipart/form-datatransfer-encoding: chunked。对吗?

标签: rest nginx lua chunked-encoding openresty


【解决方案1】:

不幸的是,lua-resty-upload 在后台使用的ngx.req.socket (https://github.com/openresty/lua-nginx-module#ngxreqsocket) 目前不处理正文编码。也就是说,当您从套接字对象中读取时,您会收到原样的请求正文,因此,您需要自己解码。 lua-resty-upload 没有这样做,它需要一个没有任何额外编码的普通表单数据主体。更多解释请见https://github.com/openresty/lua-resty-upload/issues/32#issuecomment-266301684

正如上面链接中提到的,您可以使用由“nginx 的内置请求正文阅读器支持分块编码”支持的ngx.req.read_body/ngx.re.get_body_datangx.re.get_body_data 方法返回一个已经解码的主体。您可以将正文提供给一些 formdata 解析器,该解析器接受正文作为字节字符串,而不是从 cosocket 中读取它(就像lua-resty-upload 那样)。例如,您可以使用lua-resty-multipart-parserhttps://github.com/agentzh/lua-resty-multipart-parser

有一个明显的缺点——请求体需要一次读取到 Lua 字符串,即整个请求体作为 Lua 字符串对象存储在内存中。

理论上,它可以修复。我们可以修改lua-resty-upload 以接受类似套接字的对象而不是硬编码的对象(https://github.com/openresty/lua-resty-upload/blob/v0.10/lib/resty/upload.lua#L60),并编写某种缓冲区,该缓冲区可以从迭代器中延迟读取字节并提供类似套接字的接口。也许我稍后会尝试。


这是使用这两个库的示例。它完全符合您的要求(但请记住,如果请求正文是 chunked–encoded,它会将整个正文读取为字符串)。

# nginx.conf
http {
    server {
        listen 8888;
        location = /upload {
            content_by_lua_block {
                require('upload').handler()
            }
        }
    }
}
-- upload.lua
local upload = require('resty.upload')
local multipart_parser = require('resty.multipart.parser')

local get_header = function(headers, name)
    local header = headers[name]
    if not header then
        return nil
    end
    if type(header) == 'table' then
        return header[1]
    end
    return header
end

local handler = function()
    -- return 405 if HTTP verb is not POST
    if ngx.req.get_method() ~= 'POST' then
        return ngx.exit(ngx.HTTP_NOT_ALLOWED)
    end
    local headers = ngx.req.get_headers()
    local content_type = get_header(headers, 'content-type')
    -- return 400 if the body is not a formdata
    if not content_type or not string.find(content_type, '^multipart/form%-data') then
        return ngx.exit(ngx.HTTP_BAD_REQUEST)
    end
    local transfer_encoding = get_header(headers, 'transfer-encoding')
    if transfer_encoding == 'chunked' then
        -- parse form using `lua-resty-multipart-parser`
        ngx.say('*** chunked')
        -- read the body, chunked encoding will be decoded by nginx
        ngx.req.read_body()
        local body = ngx.req.get_body_data()
        if not body then
            local filename = ngx.req.get_body_file()
            if not filename then
                return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
            end
            -- WARNING
            -- don't use this code in production, file I/O is blocking,
            -- you are going to block nginx event loop at this point!
            local fd = io.open(filename, 'rb')
            if not fd then
                return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
            end
            body = fd:read('*a')
        end
        local parser = multipart_parser.new(body, content_type)
        while true do
            local part = parser:parse_part()
            if not part then
                break
            end
            ngx.say('>>> ', part)
        end
    else
        -- parse form using `lua-resty-upload` (in a streaming fashion)
        ngx.say('*** not chunked')
        local chunk_size = 8 -- for demo purposes only, use 4096 or 8192
        local form = upload:new(chunk_size)
        while true do
            local typ, res = form:read()
            if typ == 'eof' then
                break
            elseif typ == 'body' then
                ngx.say('>>> ', res)
            end
        end
    end
end

return {
    handler = handler
}
$ curl -X POST localhost:8888/upload -F file='binary file content'                             

*** not chunked
>>> binary f
>>> ile cont
>>> ent

如您所见,正文是逐块读取和处理的。

$ curl -X POST localhost:8888/upload -F file='binary file content' -H transfer-encoding:chunked

*** chunked
>>> binary file content

在这里,相反,身体被立即处理。

【讨论】:

  • 它适用于文本文件,但是当我发送二进制数据时出现错误原因无法获取正文:local body = ngx.req.get_body_data(),错误:parser.lua:58: bad argument #1 to 'find' (string expected, got nil)
  • 是的,你是对的。 ngx.req.get_body_data 只返回内存中的缓冲区内容,但如果主体对于缓冲区来说太大,它将被存储在一个临时文件中。在这种情况下,ngx.req.get_body_file 用于获取文件名。我已经更新了示例,现在使用 I/O Lua API 来读取文件。但请注意,这种方法更糟糕——文件 I/O 在大多数情况下是阻塞。也就是说,您将阻止 nginx 事件循环。这可以通过将 I/O 操作卸载到线程池来避免,例如,github.com/tokers/lua-io-nginx-module 但我认为对于这样简单的任务来说它看起来太复杂了
  • 嗨@un.def 我已经成功应用了带有套接字github.com/openresty/lua-nginx-module/blob/… 的分块上传补丁,所以现在我已经用你的代码和那个补丁完全工作了管道
  • 您的意思是您自己解码分块编码,将所有解码的块连接到字符串并将此字符串提供给resty.multipart.parser?顺便说一句,您可以使用 lua-resty-http 中的 get_client_body_reader,支持分块编码:github.com/ledgetech/lua-resty-http/blob/v0.15/lib/resty/…
【解决方案2】:

在上一个答案中我注意到:

我们可以修改 lua-resty-upload 以接受类似套接字的对象而不是硬编码的对象,并编写某种缓冲区来懒惰地从迭代器中读取字节并提供类似套接字的接口。

完成了。我创建了一个名为lua-buffet 的新库。它可以用来创建像常规ngx_lua cosocket 对象一样的对象。尚未实现所有套接字方法,但现在它具有lua-resty-upload 所需的所有方法。它还没有发布,但我很快就会发布第一个版本。

我还 fork 和修改了lua-resty-upload 以添加套接字参数。稍后我将创建 PR 到上游存储库。

在您的情况下,有一个如何处理数据的示例:https://github.com/un-def/lua-buffet/tree/master/examples/resty-chunked-formdata

【讨论】:

    猜你喜欢
    • 2017-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多