Zeroth:你真的需要优化这个吗?
通常你发送相对较小的消息。当您查看忽略了多少以太网、IP 和 TCP 开销以及占用带宽的 RTT 时,从 512 字节的消息中削减 60 字节通常是愚蠢的。
另一方面,当您发送大量消息时,通常不需要在同一连接上发送多条消息。
查看常见的 Internet 协议,如 HTTP、IMAP 等。它们中的大多数使用行分隔、人类可读、易于调试的纯文本。HTTP 可以以二进制形式发送“消息的其余部分”,但是在完成发送后关闭套接字。
99% 的时间,这已经足够了。如果您认为它在您的情况下不够好,我仍然会编写协议的文本版本,然后在您调试完所有内容并正常工作后添加一个可选的二进制版本(然后测试它是否真的有所作为)。
同时,您的代码存在两个问题。
首先,如您所知,如果您将":::END"
其用作分隔符,并且您的消息可以在其数据中包含该字符串,那么您就会有歧义。解决这个问题的常用方法是某种形式的转义或引用。举一个非常简单的例子:
def sockWrite(conn, data):
data = data.replace(':', r'\:') + ":::END"
conn.write(data)
现在在阅读方面,您只需取消分隔符,然后replace('r\:', ':')
在消息上。(当然,为了使用 6 字节的':::END'
分隔符而对每个冒号进行转义是很浪费的——你不妨只使用未转义的冒号作为分隔符,或者编写更复杂的转义机制。)
其次,“单次写入可能会发生多次读取”是对的,但单次读取可能会发生多次写入也是正确的。您可以阅读此消息的一半,以及下一条消息的一半。这意味着您不能只使用endswith
; 您必须使用partition
or之类的东西split
,并编写可以处理多条消息的代码,还必须编写可以存储部分消息的代码,直到下一次read
循环。
同时,针对您的具体问题:
是否有适当的方法以尽可能少的带宽添加来封装数据?
当然,至少有三种合适的方式:定界符、前缀或自定界格式。
你已经找到了第一个。它的问题是:除非有一些字符串永远不可能出现在您的数据中(例如,'\0'
在人类可读的 UTF-8 文本中),否则您可以选择不需要转义的分隔符。
像 JSON 这样的自定界格式是最简单的解决方案。当最后一个打开的大括号/括号关闭时,消息就结束了,是下一个的时候了。
或者,您可以为每条消息添加包含长度的标头作为前缀。这就是许多低级协议(如 TCP)所做的。最简单的格式之一是netstring,其中标头只是以字节为单位的长度,表示为正常的 base-10 字符串,后跟冒号。netstring 协议还使用逗号作为分隔符,这增加了一些错误检查。
我曾考虑过pickle或json,但担心会增加大量带宽,因为我相信它们会将二进制数据转换为ASCII
pickle
具有二进制和文本格式。正如文档所解释的,如果您使用协议2
、、3
或HIGHEST_PROTOCOL
,您将获得相当有效的二进制格式。
另一方面,JSON 只处理字符串、数字、数组和字典。您必须先将任何二进制数据手动呈现为字符串(或字符串或数字的数组,或其他),然后才能对其进行 JSON 编码,然后在另一端进行反转。两种常见的方法是 base-64 和 hex,它们分别增加了 25% 和 100% 的数据大小,但如果你真的需要,还有更有效的方法可以做到这一点。
当然,JSON 协议本身使用的字符比严格必要的要多,所有这些引号和逗号等等,以及您为任何字段提供的任何名称都将作为未压缩的 UTF-8 发送。如果确实存在问题,您始终可以将 JSON 替换为BSON、Protocol Buffers、XDR或其他不太“浪费”的序列化格式。
同时,pickle
不是自定界的。您必须先将消息分开,然后才能解开它们。JSON是json.loads
自定界的,但除非先将消息分开,否则不能只使用;你将不得不写一些更复杂的东西。最简单的方法是重复调用raw_decode
缓冲区,直到你得到一个对象。