Python Requests拆分TCP数据包

6

我正在尝试使用Python脚本进行HTTP POST请求。

在bash中使用curl尝试时,一切正常。但是使用Python,无论是使用requests还是urllib3库,我都会从API得到错误响应。POST请求包含头信息和作为json的请求体。

当我使用Wireshark拦截数据包时,发现curl请求(正常工作)是长度为374个字节的单个数据包。而Python请求(requestsurllib3没有区别)被分成了两个不同的数据包,长度分别为253和144个字节。

enter image description here

Wireshark可以轻松地重新组装它们,并且它们似乎都包含完整的头信息和POST主体。但是我尝试连接的API会回复“处理请求时出错”,并没有提供太多帮助。

由于253个字节不能是TCP数据包的限制,这种行为的原因是什么?有没有办法解决这个问题?

编辑:

bash:

curl 'http://localhost/test.php' -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36' -H 'Content-Type: application/json' -d '{"key1":"value1","key2":"value2","key3":"value3"}'

Python:

import requests, json

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Content-Type": "application/json"}

data = {"key1":"value1", "key2":"value2", "key3":"value3"}

r=requests.post("http://localhost/test.php", headers=headers, data=json.dumps(data))

1
你能否添加一下生成这些结果的代码示例? - FunkySayu
抱歉,我已经在下面添加了我的代码,但没有包含敏感信息! - Christian H
3个回答

3
TCP是数据流而不是一系列消息。将数据流分段成数据包对于发送方和接收方的解释都应该没有关联。如果接收方基于数据包的分段行为有所不同,那么接收方就是有问题的。
虽然我见过这样的有问题的系统,但我见过更多由于其他原因(如错误的用户代理、缺少接受标头或类似原因)而不喜欢请求的系统。在得出必须是数据流分段的结论之前,建议您先检查这一点。
至于curl和requests为什么表现不同:可能是curl首先构造完整的请求(头和正文)并发送它,而requests首先构造头并发送它,然后发送正文,即执行两个写操作,这可能会导致两个数据包。

由于我对服务端API没有影响力,你有任何建议如何在Python中实现与Bash相同的结果吗? - Christian H
1
@ChristianH:鉴于我甚至不确定问题是什么(请参见我的答案),很难提出任何建议。我通常的做法是手动创建完整的请求进行测试,直接使用套接字将其发送到服务器,然后尝试调整发送和请求,直到找出服务器不喜欢的真正问题,例如特定的标头(或缺少标头)或数据流的分段。 - Steffen Ullrich

2

虽然这可能与您遇到的问题无关,但有一种方法可以强制将发送的数据合并为一个数据包进行多次发送,即在套接字上使用TCP_CORK选项(尽管这取决于平台)。

首先创建适配器:

from requests.packages.urllib3.connection import HTTPConnection

class HTTPAdapterWithSocketOptions(requests.adapters.HTTPAdapter):
    def __init__(self, *args, **kwargs):
        self.socket_options = kwargs.pop("socket_options", None)
        super(HTTPAdapterWithSocketOptions, self).__init__(*args, **kwargs)

    def init_poolmanager(self, *args, **kwargs):
        if self.socket_options is not None:
            kwargs["socket_options"] = self.socket_options
        super(HTTPAdapterWithSocketOptions, self).init_poolmanager(*args, **kwargs)

然后将其用于您想要发送的请求:
s = requests.Session()
options = HTTPConnection.default_socket_options + [ (socket.IPPROTO_TCP, socket.TCP_CORK, 1)]
adapter = HTTPAdapterWithSocketOptions(socket_options=options)
s.mount("http://", adapter)

1
很遗憾,正如@Steffen Ullrich所解释的那样(尽管它们声称是行业标准),确实存在一些非常破碎的系统无法处理分段的TCP帧。由于我的应用程序/脚本相对孤立且自包含,我使用了更简单的解决方法,即基于@Roeften的答案将TCP_CORK应用于所有连接。 警告:仅在不会破坏依赖于requests的任何其他功能的情况下,此解决方法才有意义。
requests.packages.urllib3.connection.HTTPConnection.default_socket_options = [(6,3,1)]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接