【问题标题】:How can I reliably read exactly n bytes from a TCP socket?如何可靠地从 TCP 套接字准确读取 n 个字节?
【发布时间】:2019-09-13 12:08:43
【问题描述】:

上下文:

二进制协议定义给定大小的是很常见的。 struct 模块擅长解析它,前提是所有内容都已在单个缓冲区中接收。

问题:

TCP 套接字是流。从套接字读取不能提供比请求更多的字节,但可以返回更少。所以这段代码不可靠:

def readnbytes(sock, n):
    return sock.recv(n)   # can return less than n bytes

天真的解决方法:

def readnbytes(sock, n):
    buff = b''
    while n > 0:
        b = sock.recv(n)
        buff += b
        if len(b) == 0:
            raise EOFError          # peer socket has received a SH_WR shutdown
        n -= len(b)
    return buff

可能效率不高,因为如果我们要求大量的字节,并且数据如果非常碎片化,我们会反复重新分配一个新的字节缓冲区。

问题:

如何才能可靠地从流套接字准确接收 n 个字节而没有重新分配的风险?

参考资料:

那些其他问题是相关的,确实给出了提示,但没有一个给出简单明了的答案:

【问题讨论】:

标签: python sockets


【解决方案1】:

@Serge 的答案的一个小补充,它返回一个IncompleteReadError(它是EOFError 的一个子类)。这包含一个包含部分读取数据的partial 属性。

import socket
from asyncio import IncompleteReadError
 
def readexactly(sock: socket.socket, num_bytes: int) -> bytes:
    buf = bytearray(num_bytes)
    pos = 0
    while pos < num_bytes:
        n = sock.recv_into(memoryview(buf)[pos:])
        if n == 0:
            raise IncompleteReadError(bytes(buf[:pos]), num_bytes)
        pos += n
    return bytes(buf)

用法:

try:
    print(readexactly(sock, 26))
except IncompleteReadError as e:
    print(f"Only read {len(e.partial)} out of {e.expected} bytes. :(")
    print(e.partial)

仅读取 5 个字节时的示例输出b"ABCDE"

Only read 5 out of 26 bytes. :(
b'ABCDE'

【讨论】:

【解决方案2】:

解决方案是使用recv_intomemoryview。 Python 允许预先分配一个可修改的bytearray,它可以传递给recv_into。但是您不能将数据接收到字节数组的切片中,因为切片将是一个副本。但是memoryview 允许将多个片段接收到同一个bytearray

def readnbyte(sock, n):
    buff = bytearray(n)
    pos = 0
    while pos < n:
        cr = sock.recv_into(memoryview(buff)[pos:])
        if cr == 0:
            raise EOFError
        pos += cr
    return buff

【讨论】:

    【解决方案3】:

    您可以使用socket.makefile() 将套接字包装在类似文件的对象中。然后读取将准确返回请求的数量,除非套接字关闭,它可以返回剩余部分。这是一个例子:

    server.py

    from socket import *
    
    sock = socket()
    sock.bind(('',5000))
    sock.listen(1)
    with sock:
        client,addr = sock.accept()
        with client, client.makefile() as clientfile:
            while True:
                data = clientfile.read(5)
                if not data: break
                print(data)
    

    client.py

    from socket import *
    import time
    
    sock = socket()
    sock.connect(('localhost',5000))
    with sock:
        sock.sendall(b'123')
        time.sleep(.5)
        sock.sendall(b'451234')
        time.sleep(.5)
        sock.sendall(b'51234')
    

    服务器输出

    12345
    12345
    1234
    

    【讨论】:

    • 在哪里记录了clientfile.read(5) 正好读取 5 个字节?在socket 停靠中,它说makefile 将返回一个文件对象。在 python 词汇表中,它说“文件对象”是“暴露面向文件的 API 的对象(使用 read() 或 write() 等方法)”。它还说“他们的接口是在 io 模块中定义的。”在 stdlib io 模块中,它说“IOBase 没有声明 read() 或 write(),因为它们的签名会有所不同”。 io 模块中描述的几个IOBase 子类说read(size=-1) 将读取“up to size bytes”....
    猜你喜欢
    • 2015-09-01
    • 2011-11-22
    • 1970-01-01
    • 1970-01-01
    • 2020-10-20
    • 1970-01-01
    • 2019-04-02
    • 2021-04-09
    • 2018-06-28
    相关资源
    最近更新 更多