httplib
(至少在 Python 2.7 中)的行为是,它会Content-Length
在添加自己的标题之前检查是否存在现有标题,因此如果您碰巧知道内容的大小,那么您可以添加自己的标题 - 例如:
c.request("POST", "/xpost", s, headers={"Content-Length": len(s.getvalue())})
如果没有这样的头文件,httplib
尝试调用len()
自动填充一个,如果失败,它假定主体必须是一个类似文件的对象并调用os.fstat()
操作系统级别的文件描述符来确定它的大小——它通过以下方式获得这个描述符fileno()
在你给它的文件句柄上调用方法。这适用于真实文件,但由于StringIO
对象不是真实文件,它们不提供fileno()
方法并且操作失败并显示AttributeError
. 此错误由 捕获并静默处理httplib
,它根本无法添加Content-Length
.
如果您确实在使用StringIO
对象,那么您最简单的选择可能是添加您自己的Content-Length
标题,如我在上面的示例中所示。如果这只是一个测试,而您将在现实中使用真实文件,那么httplib
只要os.fstat()
在您的平台上工作,您就可以依靠正确设置标题。如果没有,您可以随时调用os.stat()
文件名并以相同的方式提供您自己的标题。
如果您想同时处理两个真实文件,StringIO
那么您可以随时执行以下操作:
headers = {}
if not hasattr(body, "fileno"):
headers["Content-Length"] = len(body.getvalue())
...但我不建议您添加这种复杂性,除非您需要它。
最后,在 HTTP 级别还有另一个选项是使用分块编码,您不需要提供Content-Length
标头,主体本身被编码为自描述的数据块。然而不幸的是,客户端和服务器(httplib
包括)的许多 HTTP 软件都倾向于假设只有响应会被分块,并且请求总是使用Content-Length
. 我想这个假设是因为请求通常很小,但当然POST
这个PUT
假设并不成立。
假设您确信您的服务器将处理分块请求,您可以尝试这样做 - 为此,您需要使用分块编码构建一个StringIO
对象(或任何其他无法fileno()
阻止httplib
自动Content-Length
插入的对象)放置,并为您自己的Transfer-Encoding
标头提供值chunked
. 如果您的软件旨在与各种服务器一起使用,我个人不建议这样做。
编辑:顺便说一句,如果您使用分块编码,则不得发送Content-Length
标头-请参阅HTTP RFC §4.4 第3 项。当然,对于请求,您不能通过简单地关闭连接来发出正文结束的信号,因为那时您将没有可以接收响应的连接。
作为对分块请求的支持有多差的一个例子,nginx仅在去年年底的1.3.9 版本中将其添加到核心功能中(尽管在此之前有一个插件)。
编辑2:
如果您通读 Wikipedia 文章,您会发现除了简单地发送正确的标头之外,还有更多内容 - 您必须将正文拆分为多个块,并在每个块中发送一个包含该块大小(以十六进制表示)的小标头。这通常会在发送响应时为您完成,但正如我在请求中提到的那样,对它的支持很差。
这是一个围绕类文件对象的包装器示例,它将主体转换为块。我已经修改了你上面的例子来展示如何使用它,虽然"hello world"
身体当然很小,它最终只是一个块。但是,它应该适用于任何大小的物体。它应该适用于任何对象,其read()
方法与 Python 对象的工作方式相同file
。实际上,如果您将标准 Python 文件对象包装在其中之一中,它将阻止httplib
添加 a Content-Length
,因为它不支持len()
或fileno()
.
您仍然需要记住自己添加Transfer-Encoding
标题,如下面的示例所示:
import httplib
import cStringIO
class ChunkedEncodingWrapper(object):
def __init__(self, fileobj, blocksize=8192):
self.fileobj = fileobj
self.blocksize = blocksize
self.current_chunk = ""
self.closed = False
def read(self, size=None):
ret = ""
while size is None or size >= len(self.current_chunk):
ret += self.current_chunk
if size is not None:
size -= len(self.current_chunk)
if self.closed:
self.current_chunk = ""
break
self._get_chunk()
else:
ret += self.current_chunk[:size]
self.current_chunk = self.current_chunk[size:]
return ret
def _get_chunk(self):
if not self.closed:
chunk = self.fileobj.read(self.blocksize)
if chunk:
self.current_chunk = "%x" % (len(chunk),) + "\r\n" + chunk + "\r\n"
else:
self.current_chunk = "0\r\n\r\n"
self.closed = True
s = cStringIO.StringIO("hello world")
w = ChunkedEncodingWrapper(s)
c = httplib.HTTPConnection("xxx.xxx.xxx.xxx")
c.request("POST", "/xpost", w, headers={"Transfer-Encoding": "chunked"})