什么可能导致响应体被截断(客户端)?

1 投票
1 回答
3075 浏览
提问于 2025-04-17 18:21


我正在为一个活跃的代码生成器写一个Python语言的插件,这个插件会调用我们的Rest API。在尝试了很多次使用requests库但都失败后,我决定使用更底层的socket和ssl模块,这些模块到目前为止都运行得很好。我用了一种非常简单的方法来解析响应;对于比较短的响应内容,这种方法没问题,但现在我想获取更大的json对象(用户列表)。响应被截断了,像这样(注意:为了简洁,我去掉了一些用户条目):
{"page-start":1,"total":5,"userlist":[{"userid":"jim.morrison","first-name":"Jim","last-name":"Morrison","language":"English","timezone":"(GMT+5:30)CHENNAI,KOLKATA,MUMBAI,NEW DELHI","currency":"US DOLLAR","roles":
在这之后应该还有几个用户,而响应的内容在控制台上是一行显示的。

这是我用来从Rest API服务器请求用户列表的代码:

import socket, ssl, json

host = self.WrmlClientSession.api_host
port = 8443
pem_file = "<pem file>"

url = self.WrmlClientSession.buildURI(host, port, '<root path>')

#Create the header
http_header = 'GET {0} HTTP/1.1\n\n'
req = http_header.format(url)

#Socket configuration and connection execution
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
conn = ssl.wrap_socket(sock, ca_certs = pem_file)
conn.connect((host, port))
conn.send(req)

response = conn.recv()
(headers, body) = response.split("\r\n\r\n")

#Here I would convert the body into a json object, but because the response is 
#cut off, it cannot be properly decoded.  
print(response) 

如果能对此事提供一些见解,我将非常感激!

编辑:我忘了提到我在服务器端调试了响应,一切都很正常。

1 个回答

3

你不能指望只调用一次 recv() 就能获取所有数据,因为TCP连接只能缓存有限的量。而且,你也没有解析任何头部信息来确定你期望的主体大小。你可以使用非阻塞套接字,不断读取直到阻塞,这种方法在大多数情况下可以工作,但并不可靠,而且实践上也不太好,所以我这里不打算详细说明。

HTTP有一些方法可以指示主体的大小,正是为了这个原因。如果你想让你的代码更可靠,正确的做法是使用这些方法。有两种需要注意的情况。首先,如果HTTP响应中有 Content-Length,那么它就表示响应主体的字节数——你需要一直读取,直到读取到这么多字节。第二种情况是,服务器可能会发送使用分块编码的响应——它通过包含一个 Transfer-Encoding 头部来指示,头部的值会包含文本 chunked。我这里不打算深入讲解分块编码,具体可以查看维基百科文章。简单来说,主体包含每个“块”的小头部,指示该块的大小。在这种情况下,你需要不断读取块,直到读取到一个空块,这表示响应结束。当服务器在开始发送时不知道响应主体的大小时,就会使用这种方法,而不是 Content-Length

通常情况下,服务器不会同时使用 Content-Length 和分块编码,但实际上并没有什么能阻止它这样做,所以这也是需要考虑的。如果你只需要与特定的服务器进行交互,那么你可以直接了解它的行为并据此工作,但要注意这样会让你的代码变得不那么通用,也更容易受到未来变化的影响。

注意,在使用这些头部时,你仍然需要在一个循环中读取,因为任何一次读取操作可能返回不完整的数据——TCP的设计是,在读取应用程序开始清空缓冲区之前,它会停止发送数据,所以这是无法规避的。此外,每次读取可能甚至不包含完整的块,因此你需要跟踪当前块的大小和你已经看到的部分。只有在你看到前一个块头部指定的字节数后,才知道要读取下一个块头部。

当然,如果你使用Python的各种HTTP库,就不需要担心这些问题。作为一个曾经实现过相对完整的HTTP/1.1客户端的人,我真的建议你让别人来做这件事——有很多棘手的边界情况需要考虑,而你上面的简单代码在很多情况下都会失败。如果 requests 不适合你,你试过Python的标准库吗?有urlliburllib2 这些更高级的接口,还有httplib 提供的更底层的方法,可能会帮助你解决一些问题。

记住,如果你真的需要修复问题,你总是可以修改这些库的代码(当然,前提是先复制到你的本地库),或者直接导入它们并进行修改。不过,你需要非常清楚这是库中的问题,而不是你使用不当造成的。

如果你真的想实现一个HTTP客户端,那也没问题,但要意识到这比看起来要难得多。

最后补充一下,我一直使用SSL套接字的 read() 方法,而不是 recv()——我希望它们是等效的,但如果你仍然遇到问题,可以尝试一下。

撰写回答