【问题标题】:How to use select with Python ssl socket buffering?如何将 select 与 Python ssl 套接字缓冲一起使用?
【发布时间】:2023-03-31 10:46:01
【问题描述】:

我的问题类似于 python - How select.select() works? 。但是,那里的解决方案对我不起作用,因为我没有打开()我的文件。相反,它是一个套接字。我在documentation 中找不到任何方法将其设置为无缓冲。

我有一个 glib 主循环(它使用 select),我在其中注册了要读取的套接字。因为 socket.recv() 需要我指定一个接收缓冲区大小,所以读取的字节数少于套接字读取的字节数并不少见。只要内核缓冲它们就可以了; select 仍会将套接字标记为“准备读取”。但显然 Python 也有一个缓冲区。对于大文件,在数据流的末尾附近,recv() 将读取其中的一部分,其余部分将由 Python 缓冲,并且 select 不再在我的套接字上触发,直到发送新数据。此时,“丢失”的数据在新数据之前被接收;没有数据丢失。

我的问题是:我该如何解决这个问题?有没有办法在套接字上禁用 Python 的缓冲区?如果没有,有没有办法检查缓冲区是否为空,所以我可以确保在回调之前我不会从回调中返回?

编辑:

如评论中所述,Python 不会向套接字添加额外的缓冲区,因此这不是问题所在。我无法为该问题创建一个最小示例。但是,似乎它可能与使用 ssl 套接字有关。我忘记了我使用了加密连接;禁用加密似乎可以解决这个问题,但我不能接受。所以上面的问题仍然存在,注意缓冲区可能是在 ssl 模块中实现的。

显示问题的示例代码:

#!/usr/bin/python

import glib
import socket
import ssl

def cb (fd, cond):
    print ('data: %s' % repr (s.read (1)))
    return True

s = ssl.wrap_socket (socket.create_connection (('localhost', 1234)))
glib.io_add_watch (s.fileno (), glib.IO_IN, cb)
glib.MainLoop ().run ()

然后运行一个服务器

openssl s_server -accept 1234 -key file.key -cert file.crt

运行 python 程序将建立连接。发送多于一个字节的数据将使程序只打印第一个字节;当发送更多字节时,首先读取剩余的块,然后是第一个新字节,然后再次等待。这很容易理解:只要ssl缓冲区有数据,新字节就不会从内核缓冲区中读取,所以select会继续报。

【问题讨论】:

  • 我刚刚检查了Modules/socketmodule.c, lines from 2485 下的代码,其中实现了sock_recv,我可以向您保证python 做任何缓冲。它会创建一个恰好大小的缓冲区,大小与您使用recv 指定的大小相同,并且对系统调用recv 的调用会跟踪剩余字节并要求 个字节,而不是更多(见this loop
  • 我发现只有在使用 ssl 套接字时才会发生这种情况,并添加了显示问题的示例代码。
  • 我的解决方案是增加缓冲区大小。我的代码中有recv(4096),但数据实际上是4726字节。将其更改为 recv(64 * 1024) 解决了缓冲与 select() 的问题。
  • @Lekensteyn:我认为这是一种解决方法,而不是解决方案。 ;-) 如果您事先知道数据的大小,它会起作用,但看起来非常脆弱。如果您的数据包大小稍后更改并且您忘记更新它,您将很难弄清楚为什么它不再起作用。由于问题和解决方案现在已知(并且很简单),我建议使用它。
  • @Lekensteyn:我说的是docs.python.org/2.7/library/ssl.html。我刚刚提交了一个添加它的错误报告,感谢您的建议。

标签: python sockets select ssl buffer


【解决方案1】:

查看 ssl 源代码,我发现了一个未记录的函数,它可以执行我想要的操作:pending()。可以这样使用:

#!/usr/bin/python

import glib
import socket
import ssl

def cb(fd, cond):
    print('data: %s' % repr(s.read(1)))
    while(s.pending()):
        print('more data: %s' % repr(s.read(1)))
    return True

s = ssl.wrap_socket (socket.create_connection(('localhost', 1234)))
glib.io_add_watch(s.fileno(), glib.IO_IN, cb)
glib.MainLoop().run()

这样就解决了问题。

【讨论】:

  • ...为什么要在函数名和参数之间放置空格? Python 有一个著名的官方 风格指南(参见PEP8)。当您的代码应该被其他人阅读时(例如,在此答案中),您应该至少遵循它。
  • 我不知道这个风格指南,所以它不像你想象的那么有名(我的 n=1 样本说 :-P)另外,“官方”部分意味着它是编写 for Python 代码时必须使用的样式,以包含在官方发行版中。如果没有,你可以做任何你喜欢的事情。我同意使用通用风格是一个好主意,但它并不像你说的那么强制。
  • PEP8 的范围比标准库更广。例如,“此处提供的指南旨在提高代码的可读性并使其在广泛的 Python 代码中保持一致”。最后一句话是指所有的开源项目。事实上,所有像pylintpychecker 用来做一些静态分析的工具也包括检查PEP8 风格。像 PyCharm 这样的 IDE 在不遵循 PEP8 时会自动显示警告。 PEP8 风格,当你想向python社区展示代码时使用。
  • 当然你是对的,遵循它是一个好主意。我的防御反应是由您指责我无视指导方针引发的,而您至少应该考虑(IMO 更有可能)选项,而我根本不知道它的存在。仅仅声称它是众所周知的并不意味着我真的知道它。
  • @Bakuriu:我已经删除了空格;感谢您提供指向 PEP8 的指针。 :-)
【解决方案2】:

我遇到了同样的问题。 Python docs 现在有关于这一点的注释,可能是为了响应您的错误报告:

相反,由于 SSL 层有自己的框架,SSL 套接字可能仍然有可供读取的数据,而 select() 并不知道它。因此,您应该首先调用 SSLSocket.recv() 以排出任何可能可用的数据,然后仅在必要时阻塞 select() 调用。

如果您想避免阻塞,您注意到的解决方案似乎是在 ssl.SSLSocket 类上使用 pending() 函数。

编辑:我尝试将代码中对select 的简单调用替换为以下内容(socks 是一个可能包括普通和 SSL 套接字的列表)。这个想法是在阻塞select之前首先检查SSL套接字上是否有任何未决字节:

r = [s for s in socks if isinstance(s, ssl.SSLSocket) and s.pending()]
if not r:
    r, _, _ = select.select(socks, [], [], 1.0)

它似乎按预期工作。我的理解是:

  1. select 可以给出 SLL 套接字的误报,但不能给出误报,并且
  2. 只有在先前的调用返回 true 并且从套接字读取(即因为 SSL 帧中的某些字节仍未读取)之后才会出现误报

假设这些都是正确的,我认为上面的代码应该可靠地为所有套接字提供服务,并且没有不必要的等待或阻塞。

【讨论】:

    猜你喜欢
    • 2017-02-10
    • 1970-01-01
    • 2010-10-23
    • 2012-08-09
    • 1970-01-01
    • 2011-01-25
    • 2012-07-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多