首页 > 解决方案 > 数据包被完全发送 somtimes 和 somtimes 没有完全发送

问题描述

@Grismar 建议我为以下问题创建新主题:

我用模块编写了一个服务器和客户端。对于socket多连接,我使用选择器模块而不是threador fork()

场景:我要生成一个海量的字符串并发送给客户端。当然根据一个字符串是由客户端生成的。实际上,客户端发送查询,服务器生成结果并发送给客户端。向服务器发送查询没有问题。

因为我有大量的字符串,所以我决定将我的字符串拆分成块,例如:

if sys.getsizeof(search_result_string) > 1024: #131072:
    if sys.getsizeof(search_result_string) % 1024 == 0:
        chunks = int(sys.getsizeof(search_result_string) / 1024 )
    else:
        chunks = int(sys.getsizeof(search_result_string) / 1024) + 1
for chunk in range(chunks):
    packets.append(search_result_string[:1024])
    search_result_string = search_result_string[1024:]

所以,我有数据包列表。然后:

conn.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1000000)
for chunk in packets:
    conn.sendall(bytes(chunk,'utf-8'))

有时我在客户端没有任何问题,有时我收到以下错误:

Traceback (most recent call last):
  File "./multiconn-client.py", line 116, in <module>
    service_connection(key, mask)
  File "./multiconn-client.py", line 89, in service_connection
    target_string += recv_data.decode('utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd9 in position 42242: unexpected end of data

在我的客户处,我使用了以下回调:

def service_connection(key, mask):
    buff = 10000
    sock = key.fileobj
    data = key.data
    target_string = str()
    if mask & selectors.EVENT_READ:
        buff = sock.getsockopt(SOL_SOCKET,SO_RCVBUF)
        recv_data = sock.recv( 128*1024 |buff)
        if recv_data:
            buff = sock.getsockopt(SOL_SOCKET,SO_RCVBUF)
            data.recv_total += len(recv_data)
        target_string += recv_data.decode('utf-8')
        print(target_string)
        if not recv_data: #or data.recv_total == data.msg_total:
            print("closing connection", data.connid)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if not data.outb and data.messages:
            data.outb = data.messages.pop(0)
        if data.outb:
            print("sending", repr(data.outb), "to connection", data.connid)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]

顺便说一句,我使用 TCP 套接字。并在 localhost 中进行测试。
我每次运行都使用相同的字符串。

问题是,为什么有时一切都很好,有时字符串没有完全发送。

标签: pythonsocketsnetwork-programming

解决方案


正在发生的事情是您的数据正在被操作系统分块(除了您正在做的事情)。当操作系统这样做时,它可能会将您的数据拆分到 UTF-8 编码序列的中间。换句话说,考虑这个代码块:

foo = '\xce\xdd\xff'       # three non-ascii characters
print(len(foo))            # => 3
bar = foo.encode('utf-8')
print(bar)                 # => b'\xc3\x8e\xc3\x9d\xc3\xbf'
bar[:3].decode()           # =>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 in position 2: unexpected end of data

发生了什么: 0x7f 以上的字符被编码为两个 UTF8 字节。但是,如果两字节序列在中间被截断,则无法解码字符。

因此,为了轻松解决您的问题,首先接收所有数据(作为字节字符串),然后将整个字节字符串解码为一个单元。

这带来了另一个相关的问题:您不需要创建自己的数据块。TCP 将为您做到这一点。正如您所见,TCP 无论如何都不会保留您的消息边界。因此,您最好的选择是正确“构建”您的数据。

也就是说,取出字符串的一部分(如果不是数百兆字节,则取出所有字符串),并将其编码为 UTF-8。取结果字节缓冲区的长度。以二进制数据的形式发送包含该长度的固定长度大小的字段(使用struct模块创建)。在接收端,首先接收定长大小字段。这让您知道实际需要接收多少字节的字符串数据。接收所有这些字节,然后立即解码整个缓冲区。

换句话说,忽略错误处理,发送方:

import struct
import socket
...
str_to_send = "blah blah\xce"
bytes_to_send = str_to_send.encode('utf-8')
len_bytes = len(bytes_to_send)
sock.send(struct.pack("!I", len_bytes)         # Send 4-byte size header
sock.send(bytes_to_send)                       # Let TCP handle chunking bytes

接收方:

len_bytes = sock.recv(4)                       # Receive 4-byte size header
len_bytes = struct.unpack("!I")[0]             # Convert to number (unpack returns a list)

bytes_sent = b''
while len(bytes_sent) < len_bytes:
    buf = sock.recv(1024)          # Note, may not always receive 1024 (but typically will)
    if not buf:
        print("Unexpected EOF!")
        sys.exit(1)
    bytes_sent += buf
str_sent = bytes_sent.decode('utf-8')

最后一句话:socket.send保证发送整个缓冲区(尽管通常会发送)。并且socket.recv不保证接收到参数中指定的字节数。因此,健壮的 TCP 发送/接收代码需要适应这些警告。


推荐阅读