Python套接字缓冲

24

假设我想使用标准的socket模块从套接字中读取一行:

def read_line(s):
    ret = ''

    while True:
        c = s.recv(1)

        if c == '\n' or c == '':
            break
        else:
            ret += c

    return ret

s.recv(1)会发生什么?每次都会发出系统调用吗?我想无论如何都应该添加一些缓冲:

为了最好地匹配硬件和网络实际情况,bufsize的值应该是一个相对较小的2的幂,例如4096。

http://docs.python.org/library/socket.html#socket.socket.recv

但是编写高效且线程安全的缓冲似乎并不容易。如果我使用 file.readline() 呢?

# does this work well, is it efficiently buffered?
s.makefile().readline()

它每次都会发出系统调用吗?这有什么关系吗? - S.Lott
8
因为系统调用的速度较慢,所以最好获取大块数据(如果可用),然后进行处理。我知道Python并不特别快,也许这并不是很重要。但文档建议无论如何最好读取大块数据。 - Bastien Léonard
8
请注意,使用+=构建字符串潜在地具有二次时间复杂度,而构建列表并在最后使用str.join则始终具有线性时间复杂度。 - Mike Graham
@MikeGraham 我知道这很挑剔,但增加列表的时间将是平均线性的,除非支持该数组填满并分配和复制新数组。所以是线性时间,除了偶尔会有的小问题。 - Miquel
1
@Miquel,我描述的操作将是线性的。添加一个项目通常是恒定的(平均而言也是如此)。偶尔添加一个项目是线性的,但这些是分散在这样一种方式,以至于添加n个项目是线性的。 - Mike Graham
当从套接字流中读取数据时,建议使用str.join而不是+=。但是,您可以使用bytearray使其更易读(但比str.join函数稍慢)。两者都比+=更好的选项。 - Chen A.
3个回答

30

如果您关心性能并完全控制套接字(例如没有将其传递给库),那么尝试在Python中实现自己的缓冲区 - Python字符串查找和字符串分割等操作可能非常快。

def linesplit(socket):
    buffer = socket.recv(4096)
    buffering = True
    while buffering:
        if "\n" in buffer:
            (line, buffer) = buffer.split("\n", 1)
            yield line + "\n"
        else:
            more = socket.recv(4096)
            if not more:
                buffering = False
            else:
                buffer += more
    if buffer:
        yield buffer

如果您期望负载由不太大的行组成,那么这应该运行得相当快,并且避免不必要地跳过太多函数调用层次。我很想知道这与使用file.readline()或socket.recv(1)相比如何。


1
buffer.split("\n", 1) is not very fast if the buffer is large due to the buffer part in the tuple, it is better to use a for line in buffer.split("\n"): yield line + "\n" - MortenB

21

调用recv()时直接使用C库函数处理。

它会阻塞等待套接字有数据。实际上,它只是让recv()系统调用阻塞。

file.readline()是一个高效的缓冲实现。它不是线程安全的,因为它假设它是唯一读取文件的对象。(例如通过缓冲即将到来的输入.)

如果您使用文件对象,那么每次调用read()方法并传入一个正数参数时,底层代码都将仅recv()请求的数据量,除非该数据已经被缓冲。

如果已经缓冲,则如下:

  • 您已调用了readline(),即读取了完整的缓冲区

  • 行的结尾在缓冲区的结尾之前

从而留下数据在缓冲区中。否则,缓冲区通常不会过度填满。

问题的目标不清楚。如果您需要在读取之前查看是否有可用数据,可以使用select()或使用s.setblocking(False)将套接字设置为非阻塞模式。然后,如果没有等待的数据,则读取将返回空而不是阻塞。

您是否正在使用多个线程读取一个文件或套接字?我会将单个线程放在读取套接字并将已接收的项目馈入队列以供其他线程处理。

建议参考Python Socket模块源代码进行系统调用的C源代码


我其实不知道为什么问关于线程安全的问题,因为在我当前的项目中并不需要它。事实上,我想用Python重写一个Java程序。在Java中很容易获得缓冲读取,我想知道Python的socket模块是否提供相同的缓冲(事实上,我想知道为什么有人不想要缓冲而直接调用系统调用)。 - Bastien Léonard
1
realines() 不是实时的。因此,对于像 SMTP 这样的交互式 TCP 服务来说是无用的,不过 readline 似乎可以工作。 - Jasen

8
def buffered_readlines(pull_next_chunk, buf_size=4096):
  """
  pull_next_chunk is callable that should accept one positional argument max_len,
  i.e. socket.recv or file().read and returns string of up to max_len long or
  empty one when nothing left to read.

  >>> for line in buffered_readlines(socket.recv, 16384):
  ...   print line
    ...
  >>> # the following code won't read whole file into memory
  ... # before splitting it into lines like .readlines method
  ... # of file does. Also it won't block until FIFO-file is closed
  ...
  >>> for line in buffered_readlines(open('huge_file').read):
  ...   # process it on per-line basis
        ...
  >>>
  """
  chunks = []
  while True:
    chunk = pull_next_chunk(buf_size)
    if not chunk:
      if chunks:
        yield ''.join(chunks)
      break
    if not '\n' in chunk:
      chunks.append(chunk)
      continue
    chunk = chunk.split('\n')
    if chunks:
      yield ''.join(chunks + [chunk[0]])
    else:
      yield chunk[0]
    for line in chunk[1:-1]:
      yield line
    if chunk[-1]:
      chunks = [chunk[-1]]
    else:
      chunks = []

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