python - 数据包被完全发送 somtimes 和 somtimes 没有完全发送
问题描述
@Grismar 建议我为以下问题创建新主题:
我用模块编写了一个服务器和客户端。对于socket
多连接,我使用选择器模块而不是thread
or 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 中进行测试。
我每次运行都使用相同的字符串。
问题是,为什么有时一切都很好,有时字符串没有完全发送。
解决方案
正在发生的事情是您的数据正在被操作系统分块(除了您正在做的事情)。当操作系统这样做时,它可能会将您的数据拆分到 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 发送/接收代码需要适应这些警告。
推荐阅读
- pandas - 如何从列表中读取并将详细信息添加到 pandas 数据框
- php - 从外键表中获取第一张图片
- vb.net - 为什么无法更改 GridView 中编辑和删除按钮的显示?
- java - ANTLR4:获取子令牌的类型
- c++ - 在带有标志选项 -m32 的 gcc-8.2.2 上找不到 std::thread。我正在使用 mingw
- android - “可惜,ROC已经停了。” 当代码从firebase(登录)中检索数据时。NoActionBar 解决方案也不起作用
- architecture - 框架插件架构解决方案
- python - 如何根据开始和结束列表提取文本
- mybatis - MyBatis map-underscore-to-camel-case 不工作
- docker - 如何在 docker 镜像中添加 CA 根证书