http上传协议之文件流实现,轻松支持大文件上传
最近在公司进行业务开发时遇到了一些问题,当需要上传一个较大的文件时,经常会遇到内存被大量占用的情况。公司之前使用的web框架是一个老前辈实现的。在实现multipart/form-data类型的post请求解析时, 是将post请求体一次性读到内存中再做解析的,从而导致内存占用过大。而我之前为公司开发的框架是基于apistar这个asgi框架的,而apistar在解析mutilpart时使用的时flask作者编写的flask和django在对待multipart报文解析使用的方案基本是一致的,通过持续的解析请求体,将解析出来的文件内容放入一个工厂类创建的类文件对象中,工厂类在django中返回uploader的子类,在flask中叫作stream_factory。可以使用基于内存的,也可以使用基于临时文件的。
但是apistar作者在借用werkzeug的FormDataParser解析时,却直接将一个BytesIO传入了!而BytesIO中存放的是全量请求体,这势必会全部存在于内存中!那么带来的问题就是,当上传大文件时,内存会被撑爆!代码如下:
class MultiPartCodec(BaseCodec):
media_type = 'multipart/form-data'
def decode(self, bytestring, headers, **options):
try:
content_length = max(0, int(headers['content-length']))
except (KeyError, ValueError, TypeError):
content_length = None
try:
mime_type, mime_options = parse_options_header(headers['content-type'])
except KeyError:
mime_type, mime_options = '', {}
body_file = BytesIO(bytestring)
parser = FormDataParser()
stream, form, files = parser.parse(body_file, mime_type, content_length, mime_options)
return ImmutableMultiDict(chain(form.items(multi=True), files.items(multi=True)))
其实想必这也是不得已的事情,因为apistar支持ASGI协议,这就导致了每次请求IO都是异步的,异步read接口和同步接口调用方式肯定不一样,所以作者想偷懒不自己实现一套异步解析方案,那么只能么做。
作者想偷懒我可以理解,但是公司对我的要求让我感觉鸭梨山大,之前基于s3文件的上传服务是由我开发的,使用的框架也是我依赖apistar开发的star_builder,现在公司要求废弃掉公司之前的文件上传服务(也就是基于老前辈web框架开发的那个),将所有接口全部转移到我开发的服务上来。那么势必要求我一并解决掉大文件上传的问题。所以没有办法,只能为apistar的作者造个轮子接上先用着了。
在我简单了解了multipart/form-data协议之后,实现了一个FileStream类和File类,每个类都返回可异步迭代对象,FileStream迭代File对象,File对象迭代数据,迭代过程实时解析请求体,实时发现文件对象,实时处理得到的文件数据。以这种方式处理上传的文件,对内存不会产生任何压力。
FIleStream的实现如下:
class FileStream(object):
def __init__(self, receive, boundary):
self.receive = receive
self.boundary = boundary
self.body = b""
self.closed = False
def __aiter__(self):
return self
async def __anext__(self):
return await File.from_boundary(self, self.receive, self.boundary)
FileStream支持异步迭代,每次返回一个File对象。同时FIleStream存储已读但未返回到应用层的请求体数据。
File的实现如下:
class File(object):
mime_type_regex = re.compile(b"Content-Type: (.*)")
disposition_regex = re.compile(
rb"Content-Disposition: form-data;"
rb"(?: name=\"(?P<name>[^;]*?)\")?"
rb"(?:; filename\*?=\"?"
rb"(?:(?P<enc>.+?)'"
rb"(?P<lang>\w*)')?"
rb"(?P<filename>[^\"]*)\"?)?")
def __init__(self, stream, receive, boundary, name, filename, mimetype):
self.mimetype = mimetype
self.receive = receive
self.filename = filename
self.name = name
self.stream = stream
self.tmpboundary = b"\r\n--" + boundary
self.boundary_len = len(self.tmpboundary)
self._last = b""
self._size = 0
self.body_iter = self._iter_content()
def __aiter__(self):
return self.body_iter
def __str__(self):
return f"<{self.__class__.__name__} " \
f"name={self.name} " \
f"filename={self.filename} >"
__repr__ = __str__
def iter_content(self):
return self.body_iter
async def _iter_content(self):
stream = self.stream
while True:
# 如果存在read过程中剩下的,则直接返回
if self._last:
yield self._last
continue
index = self.stream.body.find(self.tmpboundary)
if index != -1:
# 找到分隔线,返回分隔线前的数据
# 并将分隔及分隔线后的数据返回给stream
read, stream.body = stream.body[:index], stream.body[index:]
self._size += len(read)
yield read
if self._last:
yield self._last
break