当前位置:   article > 正文

Mac端安装protobuf及其简单使用_macos protogen使用

macos protogen使用

1.protobuf简单介绍

protobuf是Google开发出来的一个语言无关、平台无关的数据序列化工具,在rpc或tcp通信等很多场景都可以使用。通俗来讲,如果客户端和服务端使用的是不同的语言,那么在服务端定义一个数据结构,通过protobuf转化为字节流,再传送到客户端解码,就可以得到对应的数据结构。这就是protobuf神奇的地方。并且,它的通信效率极高,“一条消息数据,用protobuf序列化后的大小是json的10分之一,xml格式的20分之一,是二进制序列化的10分之一”。

2.为什么使用protobuf

在一些场景下,数据需要在不同的平台,不同的程序中进行传输和使用,例如某个消息是用C++程序产生的,而另一个程序是用java写的,当前者产生一个消息数据时,需要在不同的语言编写的不同的程序中进行操作,如何将消息发送并在各个程序中使用呢?这就需要设计一种消息格式,常用的就有json和xml,protobuf出现的则较晚。

2.1 protobuf优点

  • protobuf 的主要有点是简单,快。
  • protobuf将数据序列化为二进制之后,占用的空间相当小,基本仅保留了数据部分,而xml和json会附带消息结构在数据中。
  • protobuf使用起来很方便,只需要反序列化就可以了,而不需要xml和json那样层层解析。

2.2 protobuf与json的比较

虽然Json用起来的确很方便,但相对于protobuf数据量更大些。无论是移动端还是PC端应用,为用户省点流量还是很有必要的,减少数据传输量不仅可以节约带宽而且可以更快地得到响应,提升用户体验。

2.2.1 protobuf相比json的优点

  • 跟Json相比protobuf性能更高,更加规范
  • 编解码速度快,数据体积小
  • 使用统一的规范,不用再担心大小写不同导致解析失败等问题

2.2.2 protobuf相比json的劣势

  • 改动协议字段,需要重新生成文件。
  • 数据没有可读性

3.protobuf安装

Mac下面除了用dmg、pkg来安装软件外,比较方便的还有用brew命令进行安装 , 它能帮助安装其他所需要的依赖,从而减少不必要的麻烦。

3.1 安装最新版本的protoc

3.1.1 安装brew

ruby -e "$(curl -fsSL  https://raw.githubusercontent.com/Homebrew/install/master/install)"

 
 
 
 
  • 1
  • 1

3.1.2 使用brew安装protobuf

使用brew命令进行protobuf安装默认安装最新的版本

brew install protobuf

 
 
 
 
  • 1
  • 1

3.1.3 查看protoc版本

lcc@lcc ~$ protoc --version
libprotoc 3.6.0

 
 
 
 
  • 1
  • 2
  • 1
  • 2

3.2 安装指定版本的protobuf

如果已经安装过protobuf,现在需要使用指定版本的protobuf,可以现将已经安装的版本卸载。

3.2.1 下载已安装版本

$ brew uninstall protobuf
Uninstalling /usr/local/Cellar/protobuf/3.6.0... (256 files, 17.2MB)
$ protoc --version
-bash: /usr/local/bin/protoc: No such file or directory

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

3.2.2 查看protobuf版本

MacBook-Pro:$ brew search protobuf
==> Formulae
protobuf ✔          protobuf-c          protobuf-swift      protobuf@3.1        protobuf@3.6        swift-protobuf

 
 
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

3.2.3 安装指定版本

$ brew reinstall protobuf@3.1

 
 
 
 
  • 1
  • 1

3.2.4 自制版本覆盖安装

如果已经尝试从源安装协议缓冲区版本,则可以在终端中键入以下内容以使源代码被自制软件版本覆盖

$ brew link --force --overwrite protobuf@2.5
Linking /usr/local/Cellar/protobuf@2.5/2.5.0... 14 symlinks created
  • 1
  • 2

If you need to have this software first in your PATH instead consider running:
echo ‘export PATH="/usr/local/opt/protobuf@2.5/bin:$PATH"’ >> ~/.bash_profile
$ protoc --version
libprotoc 2.5.0

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.3 编译安装

如果想要自己编译指定版本的protobuf,并进行安装,可以参考https://blog.csdn.net/qq_21383435/article/details/81035852

4.在golang中对protobuf的简单使用

protobuf的使用方法是将数据结构写入到.proto文件中,使用protoc编译器编译(间接使用了插件)得到一个新的go包,里面包含go中可以使用的数据结构和一些辅助方法。

4.1 定义.proto文件

下面是定义的test.proto文件的内容

    package example;
enum FOO { X = 17; };

message Test {
  required string label = 1;
  optional int32 type = 2 [default=77];
  repeated int64 reps = 3;
  optional group OptionalGroup = 4 {
    required string RequiredField = 5;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

message是protobuf语法中的一个关键字,相当于golang编程语言中的结构体struct,相当于C++中的类。

4.2 编译

运行命令protoc --go_out=. *.proto 命令,将会生成 test.pb.go 文件。将test.pb.go文件放入golang源代码中的example文件夹(对应上面package)中,作为example包使用。

4.3 在golang代码中使用protobuf中定义的内容

    package main
import (
    "log"

    "github.com/golang/protobuf/proto"
    "example"
)

func main() {
    test := &example.Test {          //对protobuf中定义的结构体,安装protobuf的语法进行初始化
        Label: proto.String("hello"),
        Type:  proto.Int32(17),
        Reps:  []int64{1, 2, 3},       //protobuf定义中的repeated相当于常用编程语言中都有的数组/切片类型
        Optionalgroup: &example.Test_OptionalGroup {
            RequiredField: proto.String("good bye"),
        },
    }
    data, err := proto.Marshal(test)				//将test结果中的数据转为[]byte字节切片
    if err != nil {
        log.Fatal("marshaling error: ", err)
    }
    newTest := &example.Test{}
    err = proto.Unmarshal(data, newTest)    //data切片中的二进制数据映射到newTest结构体中的具体字段中
    if err != nil {
        log.Fatal("unmarshaling error: ", err)
    }
    // Now test and newTest contain the same data.
    if test.GetLabel() != newTest.GetLabel() {     //GetLabel()方法是生成的.pb.go文件中获取protobuf定义结构体中每个字段内容的GetFieldName()方法
        log.Fatalf("data mismatch %q != %q", test.GetLabel(), newTest.GetLabel())
    }
    //test.GetOptionalgroup().GetRequiredField()
    //etc
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

4.4 对应关系说明

message Test对应为结构体struct 结构,其属性字段有了对应的GetFieldName方法,在go中可以使用test.GetLabel()、test.GetType()获取test对象的属性,这些GetXxx方法是通过定义的.proto文件自动生成的方法。
OptionalGroup对应为Test struct中的内嵌struct。
proto文件中repeated属性对应golang中的切片slice结构。
test.Reset()可以使其所有属性置为0值,也是通过定义的.proto文件自动生成的方法。
使用Marshal和Unmarshal可以轻松地对结构体中进行响应的编码和解码。

这些只是一些特性,想要仔细研究可以查看github上的wiki:https://github.com/golang/protobuf

更新protobuf最新文件

注意:在原来protobuf文件的基础上,如果要添加新的字段,在本地生成.pb.go文件后,需要将更新后的protobuf文件合并到线上分支,然后使用下面的命令拉取最新的protobuf文件到本地项目的分支中,以供本地分支使用最新的protobuf文件。dep是依赖管理工具,会自动将依赖拉取到vendor指定的目录下,这样就可以通过下面的命令更新vendor中的protobuf文件。

dep ensure -v -update proto

 
 
 
 
  • 1
  • 1

5. protobuf使用规则

以下内容整理自https://developers.google.com/protocol-buffers/docs/proto3?hl=zh-cn#simple,如有不当还望海涵。

假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:

syntax = "proto3";
  • 1

message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。

注意:
文件的第一行指定正在使用的版本是基于proto3语法定义的内容:如果没有指定,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。

5.1 消息定义

5.1.1 指定字段类型

在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)、其他消息类型或者嵌套结构体。

5.1.2 分配标识号

正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识,表示该字段的标示号/表示符。这些标识符是用来在消息的二进制格式中识别对应的字段的,一旦开始使用就不能够再改变。

注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。

5.1.3 指定字段规则

所指定的消息(message)字段修饰符必须是如下之一:

  • required:一个格式良好的消息一定要(至少)含有1个这种字段,表示该值是必须要设置的;
  • optional:每个消息中可以包含0个或多个optional类型的字段。
  • repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留,表示该值可以重复,相当于java中的List,golang中的切片。

由于一些历史原因,基本数值类型的repeated的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项[packed=true]来保证更高效的编码。如:

repeated int32 samples = 4 [packed=true];

 
 
 
 
  • 1
  • 1

注意:
required是永久性的,在将一个字段标识为required的时候,应该特别小心。如果在某些情况下不想写入或者发送一个required的字段,将原始该字段修饰符更改为optional可能会遇到问题——旧版本的使用者会认为不含该字段的消息是不完整的,从而可能会无目的的拒绝解析。在这种情况下,应该考虑编写特别针对于应用程序的、自定义的消息校验函数。

Google的一些工程师得出了一个结论:***使用required弊多于利;他们更愿意使用optional和repeated而不是required。***当然,这个观点并不具有普遍性。

5.1.4 添加更多消息类型

在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用。例如,如果想定义与SearchResponse消息类型对应的回复消息格式的话,可以将它添加到相同的.proto文件中,如:

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 1
  • 2
  • 3
  • 4
  • 5

message SearchResponse {

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

5.1.5 添加注释

向.proto文件添加注释,可以使用C/C++/java/golang风格的双斜杠(//) 语法格式,如:

message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}

5.1.6 .proto文件生成了什么

当用protobuf的编译器来运行.proto文件时,编译器将生成所选择语言(如go、java、c)的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取(Get)、设置(Set)字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。

对go来说,编译器会为每个消息(message)类型生成了一个.pd.go文件。
对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
对于Objective-C来说,编译器会为每个消息类型生成了一个pbobjc.h文件和pbobjcm文件,.proto文件中的每一个消息有一个对应的类。

5.1.7 标量数值类型

一个标量消息字段可以自动生成的访问类中定义的类型。

更多“序列化消息时各种类型如何编码”的信息请看这里

5.1.8 Optional的字段和默认值

消息描述中的一个元素可以被标记为“可选的”(optional)。一个格式良好的消息可以包含0个或一个optional的元素。当解析消息时,如果它不包含optional的元素值,那么解析出来的对象中的对应字段就被置为默认值。默认值可以在消息描述文件中指定。例如,要为 SearchRequest消息的result_per_page字段指定默认值10,在定义消息格式时如下所示:

optional int32 result_per_page = 3 [default = 10];
如果没有为optional的元素指定默认值,就会使用与特定类型相关的默认值:对string来说,默认值是空字符串。对bool来说,默认值是false。对数值类型来说,默认值是0。对枚举来说,默认值是枚举类型定义中的第一个值。

5.1.9 枚举

当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。

其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)就可以了。一个enum类型的字段只能用指定的常量集中的一个值作为其值(如果尝试指定不同的值,解析器就会把它当作一个未知的字段来对待)。

在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段:

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

如果给枚举常量定义别名, 需要设置allow_alias option 为 true, 否则 protocol编译器会产生错误信息。

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}
enum EnumNotAllowingAlias {
  UNKNOWN = 0;
  STARTED = 1;
  // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。

如上例所示,可以在 一个消息定义的内部或外部定义枚举——这些枚举可以在.proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同的消息中使用它——采用MessageType.EnumType的语法格式。

当对一个使用了枚举的.proto文件运行protoc命令编译的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。

5.2 使用

message支持嵌套使用,作为另一message中的字段类型

message SearchResponse {
    repeated Result results = 1;
}
  • 1
  • 2
  • 3

message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

5.2.1 导入定义(import)

可以使用import语句导入使用其它描述文件中声明的类型

import “others.proto”;
protoc编译器会在-I / --proto_path参数指定的目录中查找导入的文件,如果没有指定该参数,默认在当前目录中查找。

5.2.2 Message嵌套

例子:

message SearchResponse {
    message Result {
        string url = 1;
        string title = 2;
        repeated string snippets = 3;
    }
    repeated Result results = 1;
}

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

内部声明的message类型名称只可在内部直接使用,在外部引用需要前置父级message名称,如Parent.Type。

message SomeOtherMessage {
    SearchResponse.Result result = 1;
}

 
 
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

支持多层嵌套:

message Outer {                // Level 0
    message MiddleAA {         // Level 1
        message Inner {        // Level 2
            int64 ival = 1;
            bool  booly = 2;
        }
    }
    message MiddleBB {         // Level 1
        message Inner {        // Level 2
            int32 ival = 1;
            bool  booly = 2;
        }
    }
}

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

5.2.3 Map类型

proto3 支持 map 类型声明:

map<key_type, value_type> map_field = N;

 
 
 
 
  • 1
  • 1

例如:

message Project {...}
map<string, Project> projects = 1;

 
 
 
 
  • 1
  • 2
  • 1
  • 2

键、值类型可以是内置的标量类型,也可以是自定义message类型。
字段不支持repeated属性,不要依赖map类型的字段顺序。

5.3 包(Packages)

在.proto文件中使用package声明包名,避免命名冲突。

syntax = "proto3";
package foo.bar;
message Open {
	...
}

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

在其他的消息格式定义中可以使用包名+消息名的方式来使用类型,如:

message Foo {
    ...
    foo.bar.Open open = 1;
    ...
}

 
 
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

在不同的语言中,包名定义对编译后生成的代码的影响不同:

C++ 中:对应C++命名空间,例如Open会在命名空间foo::bar中
Java 中:package会作为Java包名,除非指定了option jave_package选项
Python 中:package被忽略
Go 中:默认使用package名作为包名,除非指定了option go_package选项
JavaNano 中:同Java
C# 中:package会转换为驼峰式命名空间,如Foo.Bar,除非指定了option csharp_namespace选项

5.4 定义服务(Service)

如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器会根据所选择的不同语言生成服务接口代码。

例如,想要定义一个RPC服务并具有一个方法,该方法接收SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:

service SearchService {
rpc Search (SearchRequest) returns (SearchResponse) {}
}
生成的接口代码作为客户端与服务端的约定,服务端必须实现定义的所有接口方法,客户端直接调用同名方法向服务端发起请求。

比较蛋疼的是即便业务上不需要参数也必须指定一个请求消息,一般会定义一个空message。

5.5 选项(Options)

在定义.proto文件时可以标注一系列的options。Options并不改变整个文件声明的含义,但却可以影响特定环境下处理方式。完整的可用选项可以查google。

一些选项是文件级别的,意味着它可以作用于顶层作用域,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,可以用在消息定义的内部。当然有些选项可以作用在字段、enum类型、enum值、服务类型及服务方法中。

但是到目前为止,并没有一种有效的选项能作用于这些类型。

以下是一些常用的选择:

java_package (file option):指定生成java类所在的包,如果在.proto文件中没有明确的声明java_package,会使用默认包名。不需要生成java代码时不起作用
java_outer_classname (file option):指定生成Java类的名称,如果在.proto文件中没有明确声明java_outer_classname,生成的class名称将会根据.proto文件的名称采用驼峰式的命名方式进行生成。如(foo_bar.proto生成的java类名为FooBar.java),不需要生成java代码时不起任何作用
objc_class_prefix (file option): 指定Objective-C类前缀,会前置在所有类和枚举类型名之前。没有默认值,应该使用3-5个大写字母。注意所有2个字母的前缀是Apple保留的。

5.6 基本规范

描述文件以.proto做为文件后缀,除结构定义外的语句以分号结尾

结构定义包括:message、service、enum

rpc方法定义结尾的分号可有可无

Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式

message SongServerRequest {
required string song_name = 1;
}
Enums类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式
enum Foo {
FIRST_VALUE = 1;
SECOND_VALUE = 2;
}
Service与rpc方法名统一采用驼峰式命名

message对应golang中的struct,编译生成go代码后,字段名会转换为驼峰式

5.7 编译

通过定义好的.proto文件生成Java, Python, C++, Go, Ruby, JavaNano, Objective-C, or C# 代码,需要安装编译器protoc。

参考Github项目 google/protobuf 安装编译器,Go语言需要同时安装一个特殊的插件:golang/protobuf。

运行命令:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

 
 
 
 
  • 1
  • 1

这里只做参考就好,具体语言的编译实例请参考官方详细文档。

参考

如何在go中使用protobuf:https://segmentfault.com/a/1190000010477733
编译gRPC Go版本使用的 ProtoBuffer 文件:https://studygolang.com/articles/3217
protobuf 归纳:https://blog.csdn.net/mynameislu/article/details/78645880
Protobuf 学习笔记:https://studygolang.com/articles/13258
golang使用protobuf:
https://segmentfault.com/a/1190000009277748
Protocol Buffers:
https://developers.google.com/protocol-buffers/docs/proto3?hl=zh-cn#simple
[译]Protobuf 语法指南:https://colobu.com/2015/01/07/Protobuf-language-guide/

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/807676
推荐阅读
相关标签
  

闽ICP备14008679号