常见的一些http web框架流式导出文件操作——django,flask,gin

HTTP协议对文件的处

文件名处理

最近遇到一个flask框架导出超大文件(GB级别)的需求,当我使用Content-Disposition头设置中文文件名的时候,发现仅文件名中的ASCII值被成功传输,非ASCII则被忽略掉了。

溯源flask的send_file源码,发现在werkzeug的send_file中,将download_name进行了编码:

在这里插入图片描述
然后查询HTTP规范,恍然大悟:

HTTP(超文本传输协议)本身并不强制要求文件名必须使用ASCII编码。HTTP协议是用于从Web服务器传输超文本到本地浏览器的传输协议,它支持多种字符编码。

然而,在实际应用中,由于历史原因和兼容性问题,URL通常使用ASCII字符集。这是因为早期的互联网只支持ASCII字符集,而且很多服务器和客户端可能不支持非ASCII字符。因此,当文件名包含非ASCII字符(如中文、日文、阿拉伯文等)时,通常会通过URL编码(百分号编码)的方式来表示这些字符。URL编码会将非ASCII字符转换为百分号(%)后跟两位十六进制数的形式。

例如,如果你有一个名为 “文件名.txt” 的文件,它包含非ASCII的中文字符,那么在URL中可能会表示为:

%E6%96%87%E4%BB%B6%E5%90%8D.txt

在HTTP响应中,如果要指定一个下载文件的名称,通常会在Content-Disposition头部使用filename参数来指定。这个参数的值如果包含非ASCII字符,通常会使用上述的URL编码,或者使用RFC 5987中定义的编码规则来进行编码,以确保文件名在不同的浏览器和操作系统中能够被正确处理。

例如,HTTP响应头可能包含如下字段:

Content-Disposition: attachment; filename="文件名.txt"

或者使用RFC 5987编码:

Content-Disposition: attachment; filename*=UTF-8''%E6%96%87%E4%BB%B6%E5%90%8D.txt

总之,HTTP协议本身不强制ASCII编码的文件名,但是由于历史和兼容性的原因,非ASCII字符在HTTP中通常需要进行特定的编码处理。

鉴于此,需要给download_name参数赋值时,导入from urllib.parse import quote进行编码。

HTTP 流式传输

对于一个超大文件,如果直接设置其Content-Length头进行传输,则会导致服务器内存爆满,最终OOM,哪怕勉强塞到HTTP中也会给网络IO造成极大的负担。(您公司财大气粗,单台服务器的内存按TB来计算请点击右上角退出)

HTTP 流式传输通常是指在 HTTP/1.1 协议中使用 Transfer-Encoding: chunked 头部来实现的。在这种模式下,服务器可以将响应数据分割成多个部分(称为“块”)逐个发送给客户端,而不是一次性发送整个响应体。这种方式允许服务器开始发送响应数据,而不必先计算出整个响应的大小。

HTTP/1.1 流式传输

当你在 HTTP 响应中以流的方式写入内容时,如果你设置了 Transfer-Encoding: chunked,那么即使你写入了 1GB 的数据,HTTP 响应也只会有一个,但是它的 body 会被分割成多个块逐个发送。每个块的开始会有一个表示该块大小的十六进制数字,后面跟着一个 CRLF(回车换行),然后是块内容和另一个 CRLF。当所有数据都发送完毕后,一个大小为 0 的块会被发送,表示响应结束。

例如,流式传输的 HTTP/1.1 响应可能看起来像这样:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

1a
这是第一个块的内容

18
这是第二个块的内容

0

在这个例子中,1a 和 18 是块大小的十六进制表示,后面跟着的是实际的数据块。

流式传输的工作方式

在流式传输中,客户端和服务器之间的连接保持打开状态,直到所有数据都被发送完毕。客户端读取每个数据块,并处理它们,就像它们是连续到达的一样。这种方式对于发送大文件或者实时数据非常有用,因为它允许客户端开始处理数据,而不必等待所有数据都到达。

HTTP/2 流式传输

HTTP/2 也支持流式传输,但它的工作方式与 HTTP/1.1 有所不同。HTTP/2 使用二进制帧来传输数据,其中每个帧都属于一个特定的流。HTTP/2 的流式传输允许多个请求和响应在同一个连接上并行进行,而不会互相阻塞(称为多路复用)。

总结

  1. 在 HTTP/1.1 中,流式传输通常通过 Transfer-Encoding: chunked 实现。
  2. 在 HTTP/2 中,流式传输是通过帧和流的概念实现的。
  3. 流式传输允许服务器发送大量数据,而不必一次性加载到内存中。
  4. 流式传输的 HTTP 响应只有一个,但 body 可以分割成多个块逐个发送。
  5. 流式传输适用于大文件传输和实时数据传输。

另外需要特别注意的是:

Content-Length 和 Transfer-Encoding: chunked 是两种不同的 HTTP 响应头,它们通常不会同时出现在同一个响应中,因为它们代表了两种不同的数据发送方式。

Content-Length 告诉客户端响应体的确切字节数。当服务器知道响应的全部内容大小时,它会在响应头中包含这个字段。客户端使用这个值来确定何时完成接收数据。

Transfer-Encoding: chunked 用于分块传输编码,它允许服务器发送一个由多个大小不定的块组成的响应体,而不需要在开始发送之前知道整个响应的大小。每个块开始之前都有一个表示该块大小的十六进制数字,最后一个大小为零的块表示数据结束。

当你在响应中设置了 Content-Length 头,服务器就假定你会发送一个固定大小的响应体,因此它不会使用分块传输编码。相反,如果你没有设置 Content-Length,并且你的服务器或框架支持分块传输编码,那么它可能会自动添加 Transfer-Encoding: chunked 头,并以分块的方式发送响应体。

在实践中,如果你知道响应体的大小,并且希望客户端能够显示下载进度,你应该设置 Content-Length 头。如果你不知道响应体的大小,或者响应体是动态生成的,那么你应该让服务器使用分块传输编码,这时不应该设置 Content-Length 头。

请注意,如果你手动设置了 Content-Length 头,但实际发送的数据大小与这个头部声明的大小不一致,那么可能会导致客户端出现错误,因为客户端会期待接收到声明的字节数。如果发送的数据少于声明的大小,客户端会等待剩余的数据;如果发送的数据多于声明的大小,客户端可能会忽略额外的数据或者报错。

补充

经过实验,只需要以流的形式写入http writer即可,不需要特别添加Transfer-Encoding: chunked头,如果想在浏览器展示进度条,需要自己计算Content-Length写入http headers即可。

给出一个基于flask的流式Response的实现:

import os.path
import typing as t
from datetime import datetime
from hashlib import sha256
from urllib.parse import quote
from time import time
from flask import stream_with_context, Response, request


def stream_file_hash(path_or_file: t.Iterable[bytes], hash_factory: sha256) -> str:
    __h = hash_factory()

    for line in path_or_file:
        __h.update(line)

    return __h.hexdigest()


def stream_send_file(
        download_name: str,
        bytes_size: int,
        path_or_file: t.Iterable[bytes],
        mimetype: t.Optional[str] = "application/octet-stream",
        as_attachment: bool = True,
        conditional: bool = True,
        etag: t.Optional[str] = None,
        last_modified: t.Optional[t.Union[datetime, int, float]] = None,
        max_age: t.Optional[t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]] = None
):
    response = Response(stream_with_context(path_or_file), mimetype=mimetype)
    if conditional:
        response = response.make_conditional(request_or_environ=request, accept_ranges=True, complete_length=bytes_size)

    if etag:
        response.set_etag(etag)

    if max_age is not None:
        if max_age > 0:
            response.cache_control.no_cache = None
            response.cache_control.public = True

        response.cache_control.max_age = max_age
        response.expires = int(time() + max_age)  # type: ignore

    if last_modified is not None:
        response.last_modified = last_modified  # type: ignore

    response.headers.set(
        "Content-Disposition", "attachment" if as_attachment else "inline", **{"filename": quote(download_name)}
    )

    response.headers.set("Content-Length", bytes_size)
    return response


from flask import Flask

app = Flask(__name__)


def generator():
    with open("elasticsearch-7.16.0-windows-x86_64.zip", "rb") as f:
        for line in f:
            yield line


@app.get("/download")
def download():
    return stream_send_file("elasticsearch-7.16.0-windows-x86_64.zip",
                            os.path.getsize("elasticsearch-7.16.0-windows-x86_64.zip"), generator())


if __name__ == '__main__':
    app.run()

django

以django导出xlsx文件为例,

Django 提供三种方式实现文件下载功能,分别是:HttpResponse、StreamingHttpResponse和FileResponse

三种Response方式对比

  1. HttpResponse 是所有响应过程的核心类,它的底层功能类是HttpResponseBase。
  2. StreamingHttpResponse 是在HttpResponseBase 的基础上进行继承与重写的,它实现流式响应输出(流式响应输出是使用Python的迭代器将数据进行分段处理并传输的),适用于大规模数据响应和文件传输响应。
  3. FileResponse 是在StreamingHttpResponse 的基础上进行继承与重写的,它实现文件的流式响应输出,只适用于文件传输响应。
  4. HttpResponse 实现文件下载存在很大弊端,其工作原理是将文件读取并载入内存,然后输出到浏览器上实现下载功能。如果文件较大,该方法就会占用很多内存。对于下载大文件,Django推荐使用5. StreamingHttpResponse 和FileResponse 方法,这两个方法将下载文件分批写入服务器的本地磁盘,减少对内存的消耗。
  5. StreamingHttpResponse 和FileResponse 的实现原理是相同的,两者都是将下载文件分批写入本地磁盘,实现文件的流式响应输出。
  6. 从适用范围来说,StreamingHttpResponse 的适用范围更为广泛,可支持大规模数据或文件输出,而FileResponse 只支持文件输出。
  7. 从使用方式来说,由于StreamingHttpResponse 支持数据或文件输出,因此在使用时需要设置响应输出类型和方式,而FileResponse只需设置3个参数即可实现文件下载功能。

httpResponse【不推荐】

普通文件导出案例:

def download1(request):
    #服务器上存放文件的路径
    file_path = r"E:\myDjango\file\1.jpg"
    try:
        r = HttpResponse(open(file_path,"rb"))
        print(r)
        r["content_type"]="application/octet-stream"
        r["Content-Disposition"]="attachment;filename=1.jpg"
        return r
    except Exception:
        raise Http404("Download error")

xlsx导出案例:

def make_res1(buffer):
    # ok
    response = HttpResponse(content_type="application/octet-stream")
    response["Content-Disposition"] = 'attachment;filename="122.xlsx"'
    response.write(buffer.getvalue())
    return response

StreamingHttpResponse【推荐】

普通文件导出案例:

def download2(request):
    file_path = r"E:\myDjango\file\2.jpg"
    try:
        r = StreamingHttpResponse(open(file_path,"rb"))
        r["content_type"]="application/octet-stream"
        r["Content-Disposition"]="attachment;filename=2.jpg"
        return r
    except Exception:
        raise Http404("Download error")

使用bytesIo导出文件案例:

def yield_buffer(buffer):
    buffer.seek(0)
    for line in buffer:
        yield line
        
def make_res2(buffer):
    response = StreamingHttpResponse(yield_buffer(buffer))
    response['Content-Type'] = 'application/octet-stream'
    response["Content-Disposition"] = 'attachment;filename="122.xlsx"'
    return response

FileResponse【推荐】

普通文件导出案例:

def download3(request):
    file_path = r"E:\myDjango\file\3.jpg"
    try:
        f = open(file_path,"rb")
        r = FileResponse(f,as_attachment=True,filename="3.jpg")
        return r
    except Exception:
        raise Http404("Download error")

使用bytesIo导出文件案例:

def make_res3(buffer):
    response = FileResponse(yield_buffer(buffer))
    response.write()
    response['Content-Type'] = 'application/octet-stream'
    response["Content-Disposition"] = 'attachment;filename="122.xlsx"'
    return response

drf的Response类

drf封装了django的http形容了一个强大的Response类用于返回各类型的http相应,

但是,当直接把一个file或file_like丢给Response会导致报错,所以Response还是用django的一些流式http返回比较好。

flask

使用flask进行大文件传输,首先需要抛弃掉封装末端的send_file方法~,并且导入原生Response和流上下文处理器:

from flask import Response,stream_with_context

以下给出一个案例:

from flask import Flask, Response, stream_with_context

app = Flask(__name__)

def generate_large_file():
    # 假设这是一个生成大文件内容的函数
    for i in range(1000000):  # 假设我们有100万行数据要发送
        yield f"This is line {i}\n"

@app.route('/download')
def download_file():
    return Response(stream_with_context(generate_large_file()), content_type='text/plain')

if __name__ == '__main__':
    app.run()

此时打开浏览器,并且访问下载接口,你就会发现当前接口的相应体在逐渐增大,而不是之前的等待响应完全返回才嗖的一下全部展示出来:

在这里插入图片描述
在这个例子中,generate_large_file 函数是一个生成器,它逐行生成文件内容。当你访问 /download 路由时,Flask 会自动将响应设置为分块传输模式,并且会逐块发送生成器产生的数据。

如果使用其他框架或者自己实现 HTTP 服务,可能需要手动处理分块传输的逻辑,包括添加头部、格式化每个数据块以及发送终止块。

总结来说,使用 Flask 时,不需要显式地设置 Transfer-Encoding: chunked 头部,只需要返回一个生成器即可。如果在其他环境中工作,可能需要更多的手动设置和实现。

Gin或net/http

Gin 框架中的 http.ResponseWriter 确实允许多次写入响应。在 Gin 中实现流式传输相对直接,因为可以直接写入到 http.ResponseWriter。以下是一个 Gin 的例子:

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "net/http"
)

func main() {
    r := gin.Default()
    r.GET("/download", func(c *gin.Context) {
        // 设置响应头部,以附件形式下载文件
        c.Header("Content-Disposition", "attachment; filename=large_file.txt")
        c.Header("Content-Type", "text/plain")
        // 不设置 Content-Length,使其自动使用分块传输编码

        // 逐步写入数据
        for i := 0; i < 1000000; i++ {
            data := fmt.Sprintf("This is line %d\n", i)
            c.Writer.Write([]byte(data))
            c.Writer.Flush() // 确保每次写入都发送到客户端
        }
    })
    r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

在这个 Gin 示例中,在路由处理函数中直接向 c.Writer 写入数据,并在每次写入后调用 Flush 方法,确保数据被立即发送到客户端。这样就实现了流式传输。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Generalzy

文章对您有帮助,倍感荣幸

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值