在Python中封装套接字数据的正确方法?

1 投票
1 回答
1789 浏览
提问于 2025-04-17 18:11

我正在开发一个应用程序,它可以通过套接字(socket)发送和接收数据。现在我想知道,怎样用一个“结束”标签来封装数据是最有效的方式。比如,我有两个函数用来在套接字连接中读取和写入数据:

def sockWrite(conn, data):
    data = data + ":::END"
    conn.write(data)

def sockRead(conn):
    data = ""
    recvdata = conn.read()
    while recvdata:
        data = data + recvdata
        if data.endswith(':::END'):
            data = data[:len(data)-6]
            break
        recvdata = conn.read()
    if data == "":
        print 'SOCKR: No data')
    else:
        print 'SOCKR: %s', data)
    return data

我基本上是在写入数据时加上“:::END”,因为一次写入可能会有多次读取。所以读取会一直循环,直到遇到“:::END”。

但这样就会出现问题,如果数据变量里面恰好有“:::END”这个字符串,而它又出现在某次读取的末尾,那就会搞混了。

有没有什么好的方法可以在尽量少增加带宽的情况下封装数据呢?我曾考虑过使用pickle或json,但担心这样会增加很多带宽,因为我觉得它们会把二进制数据转换成ASCII格式。这样理解对吗?

谢谢,
Ben

1 个回答

1

首先,你真的需要优化这个吗?

通常情况下,你发送的消息比较小。比如说,从一个512字节的消息中减少60字节,其实没什么意义,因为你忽略了以太网、IP和TCP等协议的开销,还有网络延迟对带宽的影响。

另一方面,如果你发送的是超大的消息,通常也没必要在同一个连接上发送多条消息。

看看常见的互联网协议,比如HTTP、IMAP等等。大多数都使用以行分隔的、易于阅读和调试的纯文本格式。HTTP可以用二进制发送“剩下的消息”,但发送完后就会关闭连接。

99%的情况下,这样就足够了。如果你觉得在你的情况下不够好,我还是建议你先写一个文本版本的协议,然后在调试和工作正常后再添加一个可选的二进制版本(然后测试一下是否真的有区别)。


同时,你的代码有两个问题。

首先,正如你所认识到的,如果你用":::END"作为分隔符,而你的消息中可能包含这个字符串,那么就会产生歧义。通常解决这个问题的方法是使用某种形式的转义或引用。举个简单的例子:

def sockWrite(conn, data):
    data = data.replace(':', r'\:') + ":::END"
    conn.write(data)

在读取时,你只需去掉分隔符,然后对消息进行replace('r\:', ':')处理。(当然,单纯为了使用一个6字节的':::END'分隔符而转义每个冒号是浪费的——你不如直接使用一个未转义的冒号作为分隔符,或者写一个更复杂的转义机制。)

第二,你说得对,“一次写入可能会导致多次读取”——但同样也可能出现一次读取对应多次写入的情况。你可能会读取到这条消息的一半,再加上下一条消息的一半。这意味着你不能仅仅使用endswith;你需要使用partitionsplit,并编写能够处理多条消息的代码,同时还要编写代码来存储部分消息,直到下次进入read循环。


接下来,针对你的具体问题:

有没有合适的方法来封装数据,同时尽量减少带宽的增加?

当然,有至少三种合适的方法:分隔符、前缀或自我分隔格式。

你已经找到了第一种方法。它的问题在于:除非有某个字符串在你的数据中绝对不会出现(例如,在可读的UTF-8文本中'\0'),否则你选的分隔符总是需要转义。

像JSON这样的自我分隔格式是最简单的解决方案。当最后一个打开的括号或大括号关闭时,消息就结束了,可以开始下一条消息。

另外,你可以在每条消息前加一个包含长度的头部。这是许多底层协议(比如TCP)所采用的做法。最简单的格式之一是netstring,其中头部只是以普通的十进制字符串表示的字节长度,后面跟着一个冒号。netstring协议使用逗号作为分隔符,这样可以增加一些错误检查。


我曾考虑过pickle或json,但担心这会显著增加带宽,因为我认为它们会将二进制数据转换为ASCII。

pickle有二进制和文本格式。正如文档所解释的,如果你使用协议23HIGHEST_PROTOCOL,你会得到一个相对高效的二进制格式。

而JSON则只处理字符串、数字、数组和字典。你必须手动将任何二进制数据转换为字符串(或字符串数组、数字数组等),然后才能进行JSON编码,接着在另一端再反向处理。常见的两种方法是base-64和十六进制,它们分别会将数据大小增加25%和100%,但如果你真的需要,还有更高效的方法。

当然,JSON协议本身使用的字符比严格必要的要多,像那些引号、逗号等等,以及你给任何字段起的名字都是以未压缩的UTF-8发送的。如果这真的是个问题,你可以用BSONProtocol BuffersXDR或其他更“节省空间”的序列化格式来替代JSON。

同时,pickle并不是自我分隔的。你必须先将消息分开,才能进行反序列化。而JSON自我分隔的,但你不能直接使用json.loads,除非你先将消息分开;你需要编写更复杂的代码。最简单的解决方案是不断调用raw_decode,直到你得到一个对象。

撰写回答