recv() 函数太慢

Posted

技术标签:

【中文标题】recv() 函数太慢【英文标题】:recv() function too slow 【发布时间】:2013-11-13 12:08:27 【问题描述】:

嗨,我是 Python 的新手。我使用 pygame 模块编写了一个简单的 LAN 游戏(对我来说并不简单)。

问题出在这里 - 我有两台电脑(一台旧英特尔 Atom 上网本,另一台英特尔 i5 NTB)。我想达到至少 5 FPS(上网本正在减慢 NTB,但不是那么多,现在我有大约 1.5 FPS),但是在主循环中调用 recv() 函数两次总共需要大约 0.5 秒机器。 wifi 信号很强,路由器是 300Mbit/s,它发送一个大约 500 个字符的短字符串。如您所见,我使用 time.clock() 来测量时间。

这是“服务器”代码的一部分,我通常在 i5 NTB 上运行:

while 1:
    start = time.clock()
    messagelen = c.recv(4)      #length of the following message (fixed 4 character)
    if " " in messagelen:
        messagelen = messagelen.replace(" ","")
    message = cPickle.loads(c.recv(int(messagelen))) #list of the arrows, other player position and changes in the game map
    arrowsmod = message[0]
    modtankposan = message[1]
    removelistmod = message[2]
    for i in removelistmod:
        try:
             randopos.remove(i)
        except ValueError:
            randopossv.remove(i)

    print time.clock()-start


    tosendlist=[]
    if len(arrows) == 0:  #if there are no arrows it appends only an empty list
        tosendlist.append([])
    else:
        tosendlist.append(arrows)
    tosendlist.append([zeltankpos, 360-angle])
    if len(removelist) == 0:   #if there are no changes of the map it appends only an empty list
        tosendlist.append([])
    else:
        tosendlist.append(removelist)
        removelist=[]
    tosend=cPickle.dumps(tosendlist)
    tosendlen = str(len(tosend))
    while len(tosendlen)<4:
        tosendlen+=" "
    c.sendall(tosendlen)   #sends the length to client
    c.sendall(tosend)      #sends the actual message(dumped list of lists) to client

    ...something else which takes <0,05 sec on the NTB

这是“客户端”游戏代码的一部分(只是将开头倒转 - 发送/接收部分):

while 1:
    tosendlist=[]
    if len(arrows) == 0:  #if there are no arrows it appends only an empty list
        tosendlist.append([])
    else:
        tosendlist.append(arrows)
    tosendlist.append([zeltankpos, 360-angle])
    if len(removelist) == 0:   #if there are no changes of the map it appends only an empty list
        tosendlist.append([])
    else:
        tosendlist.append(removelist)
        removelist=[]
    tosend=cPickle.dumps(tosendlist)
    tosendlen = str(len(tosend))
    while len(tosendlen)<4:
        tosendlen+=" "
    s.sendall(tosendlen)   #sends the length to server
    s.sendall(tosend)      #sends the actual message(dumped list of lists) to server

    start = time.clock()
    messagelen = s.recv(4)      #length of the following message (fixed 4 character)
    if " " in messagelen:
        messagelen = messagelen.replace(" ","")
    message = cPickle.loads(s.recv(int(messagelen))) #list of the arrows, other player position and changes in the game map
    arrowsmod = message[0]
    modtankposan = message[1]
    removelistmod = message[2]
    for i in removelistmod:
        try:
             randopos.remove(i)
        except ValueError:
            randopossv.remove(i)

    print time.clock()-start
    ... rest which takes on the old netbook <0,17 sec

当我在 i5 NTB 上的一台机器(没有插座模块)上运行单人游戏版本时,地图左上角有 50 FPS,右下角有 25 FPS( 1000x1000 像素地图包含 5x5 像素方块,我认为它会因为更大的坐标而变慢,但我不敢相信这么多。顺便说一句,在地图右下角作为 LAN 游戏运行时,recv 大约需要相同的时间) 在 Atom 上网本上它有 4-8 FPS。

那你能告诉我,为什么它这么慢吗?计算机不同步,一个更快,另一个更慢,但不可能是他们在等待对方,这将是最大 0,17 秒的延迟,对吧?再加上长时间的 recv 调用只会在速度更快的计算机上? 我也不完全知道发送/接收功能是如何工作的。奇怪的是 sendall 实际上不需要时间,而接收需要 0.5 秒。也许森多 正在尝试在后台发送,而程序的其余部分继续前进。

【问题讨论】:

【参考方案1】:

当我遇到同样的问题时,我最终来到这里,python 中的 socket recv 看起来超级慢。对我来说(几天后)的解决方法是按照以下方式做一些事情:

  recv_buffer = 2048 # ? guess & check
  ...
  rx_buffer_temp = self._socket.recv(recv_buffer)
  rx_buffer_temp_length = len(rx_buffer_temp)
  recv_buffer = max(recv_buffer, rx_buffer_temp_length)  # keep to the max needed/found

它的要点设置为尝试接收最接近实际预期的字节数。

【讨论】:

【参考方案2】:

正如Armin Rigo 所说,recv 将在套接字接收到数据包后返回,但数据包不一定需要在调用send 后立即发送。虽然send 立即返回,但操作系统会在内部缓存数据,并且可能会等待一段时间以等待更多数据写入套接字,然后再实际传输它;这称为Nagle's algorithm 并避免通过网络发送大量小数据包。您可以禁用它并更快地将数据包推送到网络;尝试在 sending 套接字上启用TCP_NODELAY 选项(或者如果您的通信是双向的,则两者都启用),通过调用:

sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

这可能会减少 recv 由于没有数据而休眠的时间。

正如***所说:

这个算法与 TCP 延迟确认的交互很糟糕,一个 早期大致同时引入 TCP 的特性 1980年代,但由不同的群体。启用这两种算法后, 对 TCP 连接进行两次连续写入的应用程序, 然后是在数据之后才会完成的读取 从第二次写到已经到达目的地,体验一次 高达 500 毫秒 的恒定延迟,即“ACK 延迟”。为了这 原因,TCP 实现通常为应用程序提供一个 用于禁用 Nagle 算法的接口。这通常称为 TCP_NODELAY 选项。

在您的基准测试中提到了 0.5 秒,所以这可能是一个原因。

【讨论】:

Nagle 的算法不会影响 send() 的时间。 send() 只是写入套接字发送缓冲区然后返回。所有对线路的实际写入、数据包合并等都与应用程序异步进行,不会由计时器测量。 @EJP 你是对的,我在这里的措辞不是很小心——send 函数本身没有延迟,但是与内核将数据推送到线路相关的延迟(以及这里是 Nagle 的算法)可以在recv 一侧看到,并且可能会影响那里的计时器。 另请注意,他在recv 周围有计时器,而不是send。我不知道这是否是 Nagle 的错,但我认为几率足够高,可以试一试并围绕这个主题进行一些实验。 就是你说的!我设置了 NODELAY,recv 时间通常是 【参考方案3】:

是的,send() 或 sendall() 将在后台发生(除非连接现在已经饱和,即已经有太多数据等待发送)。相反,recv() 只有在数据已经到达时才会立即获取数据,但如果没有,它会等待。然后它可能返回它的一小部分。 (我假设 c 是 TCP 套接字,而不是 UDP 套接字。)请注意,您不应假设 recv(N) 返回 N 个字节;你应该写一个这样的函数:

def recvall(c, n):
    data = []
    while n > 0:
        s = c.recv(n)
        if not s: raise EOFError
        data.append(s)
        n -= len(s)
    return ''.join(data)

不管怎样,说到点子上了。问题不在于 recv() 的速度。如果我理解正确的话,有四个操作:

服务器渲染(1/25 秒)

服务器在套接字上发送一些东西,由客户端接收;

客户租户(1/4 秒);

客户端在套接字上发回一些东西。

这几乎需要(0.3 + 2 * network_delay) 秒。没有什么是并行发生的。如果你想要更多的每秒帧数,你需要并行化这四个操作中的一些。例如,让我们合理地假设操作 3 是迄今为止最慢的。这是我们如何使 3 与其他三个操作并行运行的方法。您应该更改客户端,使其接收数据、处理数据并立即向服务器发送应答;只有这样它才会继续渲染它。在这种情况下,这应该足够了,因为执行此渲染需要 1/4 秒,这应该足以让答案到达服务器、服务器进行渲染以及再次发送下一个数据包。

【讨论】:

理想的方法是将一个线程用于网络,另一个用于主游戏循环? 不,不,我描述的 4 个操作发生在不同的物理位置:您不需要线程。 感谢 recvall 提示。您的意思是 1 字节的 buff 大小并不总是一个字符?但是现在它可以工作,否则会出现泡菜错误。另外我忘了提到,当我进行实验并将这些代码行与游戏分开时(+- 我发布的代码相同,但有一个示例发送列表)并且我运行它们时没有这样的延迟。两个 recv 调用需要 0,1 或 0,01 秒,我不完全记得,但可以肯定它远小于 0,5 - 也是@tomasz。那很奇怪。抱歉,我没有得到并行说明。我该怎么做? 现在通信是双向的,客户端和服务器几乎是同一个程序,但首先客户端发送数据,服务器接收,然后服务器发送数据,客户端接收。 (它可以被逆转,但这并不重要吗?)然后两个程序都“渲染”图形并进行游戏处理。然后它重复。 (每台机器的数据长度接收和数据接收总共需要 0.5 秒)所以你能再解释一下吗?我是个菜鸟 如果需要,画一张图:确实应该颠倒过来。大约 1 字节 == 1 个字符:阅读 socket.recv() 的文档以了解更多信息:recv(N) 可以返回最多 N 字节的随机数据量。

以上是关于recv() 函数太慢的主要内容,如果未能解决你的问题,请参考以下文章

linux 下调用recv函数,死循环在recv函数里面,啥原因?

socket 的recv函数返回0应该怎样处理

linux recv函数返回值分析

将超时设置为 recv 函数

recv函数的第四个参数是啥意思

循环时 recv() 函数如何工作?