数据系统数据编码格式

程序通常使用(至少)两种不同的表示形式处理数据:在内存中,数据保存在对象,结构,列表,数组,哈希表,树等中。对这些数据结构进行了优化,以实现CPU的有效访问和操

本篇文章给大家谈谈数据系统数据编码格式,以及对应的知识点,文章可能有点长,但是希望大家可以阅读完,增长自己的知识,最重要的是希望对各位有所帮助,可以解决了您的问题,不要忘了收藏本站喔。

由于这是一个常见问题,因此有许多不同的库和编码格式可供选择。让我们简单概述一下。

语言特定的格式

许多编程语言都内置支持将内存中的对象编码为字节序列。例如,Java有java.io.Serialized,Ruby有Marshal,Python有pickle,等等。还存在许多第三方库,例如Kryo for Java。

这些编码库非常方便,因此它们允许使用最少的额外代码来存储和恢复内存中的对象。但他们也存在一些严重的问题:

通常,编码与特定的编程语言相关,因此很难通过另一种语言读取数据。如果您想以这种编码存储或传输数据,它可能会让您长期使用当前的编程语言,并阻止您的系统与其他组织(可能使用不同语言)的系统集成。为了恢复相同对象类型的数据,解码过程需要能够实例化任意类。这通常是安全问题的根源:如果攻击者可以接触您的应用程序,以便他们可以解码任意字节序列,他们就可以实例化任意类,这反过来通常允许他们做一些可怕的事情,例如远程执行任意代码。在这些库中,版本控制数据通常是事后才想到的:由于它们的目标是快速轻松地对数据进行编码,因此它们常常忽略向前和向后兼容性的不便。效率(编码或解码所花费的CPU 时间以及编码结构的大小)通常也是事后才考虑的。例如,Java 的内置序列化因其糟糕的性能和臃肿的编码而臭名昭著。由于这些原因,除了非常短暂的目的之外,使用当前编程语言的内置编码通常是一个坏主意。

JSOM,XML以及二进制变量

当转向可由多种编程语言编写和读取的标准化编码时,JSON 和XML 是明显的竞争者。它们广为人知,得到广泛支持,但几乎同样不受欢迎。 XML 常常因过于冗长和不必要的复杂而受到批评。 JSON 的流行主要是由于它在Web 浏览器中的内置支持(由于是JavaScript 的子集)及其相对于XML 的简单性。 CSV 是另一种流行的、独立于语言的格式,尽管功能较少。

JSON、XML 和CSV 是文本格式,因此在某种程度上是人类可读的(尽管它们的语法是争论的热门话题)。除了表面的语法问题外,它们还存在一些微妙的问题:

关于数字编码有很多含糊之处。在XML 和CSV 中,您无法区分数字和恰好由数字组成的字符串(除非您引用外部架构)。 JSON 区分字符串和数字,但不区分整数和浮点数,并且不指定精度。当处理大量数据时,这是一个问题。例如,大于2^53 的整数无法用IEEE 754 双精度浮点数精确表示,因此当用使用浮点数的语言(例如JavaScript)解析时,此类数字将变得不准确。 Twitter 上有一个大于2^53 的数字示例,它使用64 位数字来标识每条推文。 Twitter API 返回的JSON 包含两次Twitter ID,一次作为JSON 数字,一次作为十进制字符串,以说明JavaScript 应用程序未正确解析该数字的事实。 JSON 和XML 对Unicode 字符串(即人类可读的文本)有很好的支持,但它们不支持二进制字符串(没有字符编码的字节序列)。二进制字符串是一项有用的功能,因此可以通过使用Base64 将二进制数据编码为文本来解决此限制。然后使用模式来指示该值应解释为Base64 编码。这是可行的,但有点hacky,并且数据大小增加了33%。 XML 和JSON 都有可选的架构支持。这些模式语言非常强大,因此学习和实现起来很复杂。 XML 模式的使用相当普遍,但许多基于JSON 的工具不使用模式。由于数据(例如数字和二进制字符串)的正确解释取决于模式中的信息,因此不使用XML/JSON 模式的应用程序可能需要对相应的编码/解码逻辑进行硬编码。 CSV 没有任何模式,因此由应用程序来定义每行和每列的含义。如果应用程序更改添加了新行或新列,则必须手动处理更改。 CSV 也是一种不明确的格式(如果值包含逗号或换行符会发生什么?)。尽管它们的转义规则被正式指定,但并非所有解析器都正确实现它们。尽管存在上述问题,JSOM、XML 和CSV 足以满足大多数用途。它们可能会继续流行,特别是作为数据交换格式(即用于将数据从一个组织发送到另一个组织)。在这种情况下,只要人们就格式达成一致,格式有多漂亮或多有效通常并不重要。因为让不同的组织就任何事情达成一致的难度超过了大多数其他问题。

二进制编码

对于仅在组织内部使用的数据,使用每个人都接受的编码格式的压力较小。例如,您可以选择更紧凑或解析速度更快的格式。对于较小的数据集,增益可以忽略不计,但一旦达到TB,数据格式的选择就会产生很大的影响。

JSON 比XML 简洁,但与二进制格式相比,两者仍然占用大量空间。这也导致了JSON(如MessagePack、BSON、BJSON、UBJSON、BISON 和Smile)和XML(如WBXML 和Fast Infoset)的大量二进制编码的发展。这些格式已在各个领域得到采用,但没有一种格式像JSON 和XML 的文本版本那样被广泛采用。

其中一些格式扩展了数据类型集(例如,区分整数和浮点数,或添加对二进制字符串的支持),但在其他方面,它们不会更改JSON/XML 数据模型。特别是,由于它们没有指定模式,因此所有对象字段名称都需要包含在编码数据中。也就是说,在下面示例中的JSON 文档的二进制编码中,它们需要在某处包含字符串userName、favoriteNumber 和interest。

我们以MessagePack(JSON的二进制编码)为例。上图显示了如果您想使用MessagePack 对JSON 文档进行编码,您将获得的文档中的字节序列。前几个字节如下:

第一个字节0x83 表示一个对象包含三个字段。高4位0x80表示一个对象,低4位0x30表示三个字段。 (以防万一您想知道,如果一个对象具有超过15 个字段,因此字段数量无法容纳在4 位中,那么我们会得到不同的类型引用,并且字段数量会被编码为2 或4 个字符。节日)。第二个字节0xa8表示后面跟着一个字符串(0xa0),长度为8个字节(0x08)。接下来的八个字节是ASCII码形式的字段名userName,长度已经在前面标明了,所以不需要使用任何标记来指示字符串的结束位置。接下来的七个字节是前缀0xa6,以及六字母字符串值Martin 的编码,依此类推。使用MessagePack 进行编码演示

二进制编码总共有66 个字节长,比上图中文本JSON 的81 个字节要少一些。 JSON的所有二进制编码方法都与此类似。但尚不清楚如此小的减少(可能是更快的数据解析)是否值得以失去可读性为代价。

下面我们将看到一种仅使用32 个字节对上图中相同内容进行编码的更好方法。

Thrift和Protocol Buffers

Apache Thrift 和Protocol Buffers (protobuf) 是基于相同原理的二进制编码库。 Protocol Buffers 最初由Google 开发,Thrift 最初由Facebook 开发。两者均于2007 年8 月开源。

Thrift 和protobuf 都需要数据编码模式。要对上图中的JSON 数据进行编码,在Thrift 中,您将使用Thrift 接口定义语言(IDL) 来描述架构,如下所示:

protobuf中的相等模式定义如下,看起来很相似:

Thrift和protocol都会使用编码生成工具将上述模式定义转换为各种形式的编程语言的类。您的应用程序调用这些生成的代码来对架构中的每条内容进行编码或解码。

用这种模式编码的数据是什么样的?令人困惑的是,Thrift 有两种不同的二进制编码格式,称为BinaryProtocol 和CompactProtocol。我们先看一下BinaryProtocol。该格式之前的文本JSON的编码大小为59字节。如下图:

与Message类似,每个字段在需要时都有类型注释(指示是否是字符串、整数、列表等)和长度指示(字符串的长度、列表中的项目数)。与之前类似,数据中出现的字符串(“Martin”、“Daydream”、“Hacker”)也被编码为ASCII(或更准确地说是UTF-8)。

与Message相比,最大的区别是没有字段名(userName、favoriteNumber、interest)。相反,编码数据包含字段标记,即数字(1、2 和3)。这些是架构定义中出现的数字。字段标签就像字段的别名,它们是一种紧凑的方式来指示我们想要哪个字段,而无需拼写出字段名称。

Thrift CompactProtocol 编码在语义上与BinaryProtocol 等效,但如下图所示,它将相同的信息打包到34 个字节中。它通过将字段类型和标记号打包到一个字节中并使用可变长度整数来实现这一点。它没有使用完整的八个字节来表示数字1337,而是将其编码为两个字节,每个字节的最高位用于指示是否有更多字节即将到来。这意味着64 到63 之间的数字被编码为一个字节,8192 到8191 之间的数字被编码为两个字节,依此类推。数字越大,使用的字节越多。

protobuf只有一种二进制编码形式,如下图所示。它的打包方式略有不同,但在其他方面与Thrift 的CompactProtocol 非常相似。上述protobuf编码的JSON文本的大小也是33字节。

这里需要注意的一个细节:在前面显示的模式中,每个字段都被标记为必填或可选,但这对字段的编码方式没有影响(二进制数据中没有任何内容来指示字段是否为必填) 。唯一的区别是required 启用运行时检查,如果未设置该字段,则运行时检查失败,这对于捕获错误很有用。

字段的标签和模式的演化

随着时间的推移,模式不可避免地需要改变。我们称之为模式演化。那么Thrift 和Protocol Buffer 如何在保持向后和向前兼容性的同时处理架构变化呢?

从上面的示例中可以看出,编码记录只是其编码字段的串联。每个字段都由其标签号(示例架构中的数字1、2、3)标识,并用数据类型(例如字符串或整数)进行注释。如果未设置字段值,则会从编码记录中省略该字段值。由此可见,字段标签对于编码数据的含义至关重要。您可以更改架构中的字段名称,因为编码数据永远不会引用字段名称,但您无法更改字段的标记,因为这将使所有现有编码数据无效。

您可以向架构添加新字段,前提是为每个字段指定新的标签号。如果旧代码(不知道您添加的新标签号)尝试读取新代码写入的数据(包括带有标签号的新字段),它将无法识别它并简单地忽略该字段。数据类型注释允许解析器确定需要跳过多少字节。这保持了向前兼容性:旧代码可以读取新代码写入的记录。

向后兼容性怎么样?只要每个字段都有唯一的标签号,新代码总是可以读取旧数据,因为标签号仍然具有相同的含义。唯一的细节是,如果添加新字段,则无法将其设为必填字段。如果您要添加一个字段并将其设为必填字段,如果新代码读取旧代码写入的数据,则检查将失败,因为旧代码不会写入您添加的新字段。因此,为了保持向后兼容性,在初始部署模式之后添加的每个字段都必须是可选的或具有默认值。

删除字段就像添加字段一样。这意味着您只能删除选项字段(必填字段永远无法删除),并且您永远不能再次使用相同的标签编号(因为您可能仍然将该数据写入包含旧标签编号的某处,并且该字段必须是新的代码被忽略)。

数据类型和模式的演化

如何更改字段的数据类型?这是可能的(详细信息请查看文档),但存在值丢失精度或被截断的风险。例如,假设您将32 位整数更改为64 位整数。新代码可以轻松读取旧代码写入的数据,因为解析器可以用零填充任何丢失的位。但是,如果旧代码读取新代码写入的数据,旧代码仍将使用32 位变量来保存该值。如果解码后的64 位值不适合32 位,则会被截断。

protobuf 的一个有趣的细节是它没有列表或数组数据类型,而是具有重复的字段标记(这是除了必需和可选之外的第三个选项)。从上图中可以看到,重复字段的编码是:同一个字段标签只是在一条记录中出现多次。这具有将可选(单值)字段更改为重复(多值)字段的良好效果。新代码读取旧数据将看到一个包含零个或一个元素的列表(取决于该字段是否存在);读取新数据的旧代码只会看到列表的最后一个元素。

Thrift 有一个专用的列表数据类型,用列表元素的数据类型进行参数化。这不允许像protobuf那样从单个值演变为多个值,但它具有支持嵌套列表的优点。

数据系统数据编码格式

Avro

Apache Avro是另一种二进制编码格式,与protobuf和Thrift不同。它于2009 年作为Hadoop 的子项目启动,因为Thrift 不适合Hadoop 的用例。

Avro 还使用模式来指定要编码的数据的结构。它有两种模式语言:一种(Avro IDL)用于人工编辑,另一种(基于JSON)更易于机器阅读。

根据之前JSON 文本的内容,根据Avro IDL,它看起来像这样:

该模式的等效JSON 表示如下:

首先,请注意架构中没有标签号。如果我们使用这种模式对前面的JSON 文本进行编码,Avro 二进制编码只有32 个字节长,这是我们见过的所有编码中最紧凑的。编码字节序列的细分如下图所示。

如果检查字节序列,您会发现没有任何内容标识该字段或其数据类型。编码仅由连接在一起的值组成。字符串只是一个长度前缀,后跟UTF-8 字节,但编码数据中没有任何内容告诉您它是一个字符串。也可以是整数或其他整数。整数使用可变长度编码进行编码(与Thrift 的CompactProtocol 相同)。

要解析二进制数据,您可以按照字段在架构中出现的顺序遍历字段,然后使用架构告诉您每个字段的数据类型。这意味着只有读取数据的代码使用与写入数据的代码完全相同的模式,才能正确解码二进制数据。读取器和写入器之间的架构中的任何不匹配都将意味着解码的数据不正确。

那么,Avro 如何支持模式演化呢?

写入器模式和读取器模式

使用Avro,当应用程序想要对某些数据进行编码(将其写入文件或数据库、通过网络发送等)时,它将使用它所知道的任何模式版本对数据进行编码,例如可以编译到应用程序中。这是写入模式。

当应用程序想要解码某些数据(从文件或数据库读取数据、从网络接收数据等)时,它期望数据处于某种模式,称为读取器模式。这是应用程序代码所依赖的架构- 代码可能是在应用程序的构建过程中从该架构生成的。

Avro 的关键思想是写入器模式和读取器模式不必相同,只需兼容即可。解码(读取)数据时,Avro 库通过并排查看写入器模式和读取器模式并将数据从写入器模式转换为读取器模式来解决差异。 Avro 规范准确定义了该分辨率的工作原理,如下图所示。

模式演化的规则

使用Avro,前向兼容性意味着您可以使用新版本的架构作为写入器,使用旧版本的架构作为读取器。相反,向后兼容性意味着您可以使用新版本的模式作为读取器,使用旧版本作为写入器。

为了保持兼容性,您只能添加或删除具有默认值的字段。 (在我们的Avro 架构中,favoriteNumber 的默认值为null。)。例如,假设您添加一个具有默认值的字段,因此这个新字段存在于新架构中,但不存在于旧架构中。当使用新模式的读取器读取使用旧模式写入的记录时,将为缺失的字段填充默认值。

如果要添加一个没有默认值的字段,新的读取器将无法读取旧写入器写入的数据,从而破坏了向后兼容性。如果要删除没有默认值的字段,旧的读取器将无法读取新写入器写入的数据,从而破坏兼容性。

在某些编程语言中,null 是任何变量可接受的默认值,但在Avro 中并非如此:如果要允许字段为null,则必须使用联合类型。例如,union {null, long, string} 字段;指示该字段可以是数字、字符串或null。仅当null 是null 联合的分支时才可以使用null。这比默认情况下使所有内容都可为空更为冗长,但它通过明确哪些可以为空、哪些不能为空来帮助防止错误。

因此,Avro 没有与Protocol Buffers 和Thrift 相同的可选和必需标签(相反,它具有联合类型和默认值)。

只要Avro 可以转换类型,您就可以更改字段的数据类型。更改字段的名称是可能的,但有点棘手:读取器的模式可以包含字段名称的别名,因此旧写入器的模式字段名称可以与别名进行匹配。这意味着更改字段名称是向后兼容的,但不是向前兼容的。同样,向联合类型添加分支是向后兼容的,但不是向前兼容的。

什么是写入者模式?

现在,我们需要解决一个重要问题:读者如何知道作者对特定数据进行编码的模式?我们不能在每个记录中包含整个模式,因为模式可能比编码数据大得多,从而丢失了二进制编码节省的所有空间。

答案取决于Avro 的使用环境。举几个例子:

包含许多记录的大文件:Avro 的常见用途(尤其是在Hadoop 上下文中)是存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码。在这种情况下,文件的编写者只需在文件的开头包含编写者的模式一次。 Avro 指定一种文件格式(对象容器文件)来执行此操作。具有不同类型写入数据的数据库:在数据库中,不同的记录可能会使用不同写入者的模式在不同的时间点写入- 您不能假设所有记录都具有相同的模式。最简单的解决方案是在每个编码记录的开头包含版本号,并在数据库中保留架构版本列表。读取器可以获取记录,提取版本号,然后从数据库中获取该版本号的写入器模式。使用该写入器的模式,它可以解码剩余的记录。 (例如,Expresso 的工作原理如下。)通过网络连接发送记录:当两个进程通过双向网络连接进行通信时,它们可以在建立连接时协商架构版本,然后在连接的生命周期内使用该架构。这就是Avro RPC 协议的工作原理。无论如何,模式版本数据库都是有用的,因为它充当文档并为您提供检查模式兼容性的机会。至于版本号,您可以使用简单的递增整数,也可以使用模式的哈希值。

动态生成的模式

与protobuf 和Thrift 相比,Avro 方法的优点之一是模式不包含任何标记号。但这为什么重要呢?在模式中保留一些数字有什么问题吗?

不同之处在于Avro 对动态生成的schema 更加友好。例如,假设您有一个关系数据库,您希望将其内容转储到文件中,并且您希望使用二进制格式来避免上述文本格式(JSON、CSV、SQL)的问题。如果您使用Avro,您可以轻松地从关系模式(如我们之前看到的JSON 表示形式)生成Avro 模式,并使用该模式对数据库内容进行编码,然后将其全部转储到Avro 对象容器文件中。您为每个数据库表生成一个读取器架构,并且每一列都成为该记录中的一个字段。数据库中的列名称映射到Avro 中的字段名称。

现在,如果数据库模式发生变化(例如,表中添加一列,删除一列),您只需从更新的数据库模式生成一个新的Avro 模式,并将数据导出到新的Avro 模式中间。数据导出过程不需要任何关注架构更改,它只是在每次运行时执行架构转换。任何读取新数据文件的人都会看到记录的字段已更改,但由于字段是按名称标识的,因此更新后的读取器的架构仍然可以与旧读取器的架构相匹配。

相反,如果您使用Thrift 或Protocol Buffer 来实现此目的,则可能必须手动分配字段标签:每次更改数据库模式时,管理员都必须手动更新从数据库列名称到字段标签的映射。 (也许可以自动执行此操作,但模式生成器必须非常小心,不要分配以前使用的字段标签。)这种动态生成的模式根本不是Thrift 或Protocol Buffer 的设计目的。

代码生成和动态类型化的语言

Thrift 和Protocol Buffer 依赖于代码生成:定义模式后,您可以生成用您选择的编程语言实现该模式的代码。这在Java、C 或C 等静态类型语言中很有用,因为它允许使用内存高效的结构对数据进行解码,并允许在写入访问数据结构时在IDE 中进行类型检查和自动完成。

在JavaScript、Ruby 或Python 等动态类型编程语言中,生成代码没有多大意义,因为没有编译时类型检查器可以满足。这些语言通常不鼓励使用代码生成,否则它们为什么要避免显式编译步骤。此外,对于动态生成的模式(例如从数据库表生成的Avro 模式),代码生成并不是获取数据的必要障碍。

Avro 为静态类型编程语言提供可选的代码生成,但它可以在不生成任何代码的情况下使用。如果您有一个对象容器文件(其中嵌入了编写器的架构),您可以使用Avro 库打开它,并像查看JSON 文件一样查看数据。该文件是自描述的,因为它包含所有必需的元数据。

当与动态类型数据处理语言(例如Apache Pig)结合使用时,此属性特别有用。在Pig 中,您只需打开一些Avro 文件,开始分析它们,然后将派生数据集写入Avro 格式的输出文件,甚至无需考虑架构。

模式的优点

正如我们所见,Protocol Buffers、Thrift 和Avro 都使用schema 来描述二进制编码格式。它们的模式语言比XML Schema 或JSON Schema 简单得多,支持更详细的验证规则(例如“此字段的字符串值必须与此正则表达式匹配”或“此字段的整数值必须在0 到100 之间”)。由于Protocol Buffers、Thrift 和Avro 更易于实现和使用,因此它们已经发展到支持相当广泛的编程语言。

这些代码所基于的想法绝不是新的。例如,它们与ASN.1 有很多共同点,ASN.1 是一种于1984 年首次标准化的模式定义语言。它用于定义各种网络协议及其应用程序。例如,二进制编码(DER) 仍用于对SSL 证书(X.509) 进行编码。 ASN.1 支持使用标签号进行模式演化,类似于protobuf 和Thrift。然而,它也非常复杂且文档很少,因此ASN.1 可能不适合新应用程序。

许多数据系统还为其数据实现一些专有的二进制编码。例如,大多数关系数据库都有一个网络协议,您可以通过该协议向数据库发送查询并获取响应。这些协议通常特定于特定数据库,数据库供应商提供驱动程序(例如,使用ODBC 或JDBC API),将来自数据库网络协议的响应解码为内存中的数据结构。

用户评论

数据系统数据编码格式
在哪跌倒こ就在哪躺下

数据系统这块真是一门深奥的东西啊!数据编码格式的选择的确很有讲究,要是选得不好,可能会导致很多问题。比如中文和英文的编码格式就肯定不一样吧?

    有8位网友表示赞同!

数据系统数据编码格式
走过海棠暮

看来我之前的编码方法不太合适了,需要好好研究一下新的数据编码格式才行。不然的话,将来传输数据时就会出现乱码什么的麻烦事了!

    有10位网友表示赞同!

数据系统数据编码格式
惦着脚尖摘太阳

我是做前端开发的,对数据系统和编码格式的理解确实有点浅。这篇文章讲得详细易懂,受益良多啊,以后遇到这类问题就翻翻这个文章好了!

    有14位网友表示赞同!

数据系统数据编码格式
苏莫晨

个人觉得数据编码格式的选择确实取决于实际需要吧,什么场景用什么格式才合适呢?这篇文章没说得很透彻啊,再多提供一些具体事例就好了。

    有8位网友表示赞同!

数据系统数据编码格式
追忆思域。

对程序员来说,掌握不同的数据编码格式的确很有必要。这能够让我们的代码更简洁高效,也就能更容易地进行数据的传输和处理了。

    有6位网友表示赞同!

数据系统数据编码格式
古巷青灯

这篇文章让我更加了解数据系统中数据编码格式的重要性了!以前只知道编码要转换,没想到还有这么多的类型和适用场景啊!

    有14位网友表示赞同!

数据系统数据编码格式
无望的后半生

数据系统的稳定性和安全性跟选用的编码格式都有关吧?应该多考虑一下这方面的因素才能做出最合理的决定。

    有16位网友表示赞同!

数据系统数据编码格式
来自火星球的我

虽然文章讲解挺详细的,但我觉得还是缺乏一些实战操作经验分享。比如遇到某个具体问题时,应该怎么选择合适的编码格式呢?

    有12位网友表示赞同!

数据系统数据编码格式
致命伤

数据系统这个领域真复杂啊!各种编码格式那么多,让人头脑发热,看不懂的文章总是让人感觉越来越迷茫了。

    有6位网友表示赞同!

数据系统数据编码格式
微信名字

对于一个初学者来说,这篇文章确实很有帮助。可以让我慢慢地理解数据系统中数据编码格式的重要性,以及学习相应的知识和技能

    有5位网友表示赞同!

数据系统数据编码格式
何年何念

虽然这篇文章介绍了好多种常见的编码格式,但我觉得应该详细解释一下各个格式的特点和优缺点,这样能更直观地帮助读者进行选择。

    有18位网友表示赞同!

数据系统数据编码格式
病房

数据系统这个领域确实很需要不断学习和探索。希望作者以后能够再发布一些更深入的文章,分享更多实践经验吧!

    有6位网友表示赞同!

数据系统数据编码格式
关于道别

这种东西真是越看越深奥啊!我一个门外汉,看着这些专业术语就头晕眼花了…

    有5位网友表示赞同!

数据系统数据编码格式
あ浅浅の嘚僾

数据编码格式的选择真的很关键,它直接影响着数据的传输和安全性。希望更多的开发者重视这一方面。

    有20位网友表示赞同!

原创文章,作者:小su,如若转载,请注明出处:https://www.sudun.com/ask/101447.html

(0)
小su's avatar小su
上一篇 2024年8月30日 上午1:53
下一篇 2024年8月30日 上午1:57

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注