如何可靠地从TCP套接字读取n个字节?

时间:2019-04-24 08:45:32

标签: python sockets

上下文:

通常二进制协议定义给定大小的 frames 。如果所有内容都已在单个缓冲区中接收到,则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个字节而又没有重新分配的风险?

参考文献:

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

3 个答案:

答案 0 :(得分:1)

您可以使用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

答案 1 :(得分: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 :(得分:0)

解决方案是使用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