321 lines
10 KiB
Markdown
321 lines
10 KiB
Markdown
|
# ProtoBuf简明教程
|
|||
|
|
|||
|
## 1.什么是Protobuf
|
|||
|
|
|||
|
Protobuf 是一个无关语言,无关平台的,用于序列化结构 化数据的工具。相较于JSON体积更小,传输更快。
|
|||
|
Protobuf 定义在,proto文件中,在特定语言进行编译时,进行动态编译。
|
|||
|
|
|||
|
1. 序列化:将数据结构转换为字节流,便于网络传输和存储。
|
|||
|
2. 高效性:相比于JSON,Protobuf 序列化后的字节流更小,传输更快。
|
|||
|
3. 兼容性:Protobuf 支持多种语言,使得数据在不同语言之间进行通信和交互更加方便。
|
|||
|
4. 可读性:Protobuf 的定义文件是纯文本
|
|||
|
|
|||
|
## 2.主要应用场景
|
|||
|
|
|||
|
1. 网络通信:Protobuf 适用于网络通信场景,例如在分布式系统中进行数据传输和通信。
|
|||
|
2. 数据存储:Protobuf 可以将数据结构存储在文件或数据库中,使得数据的存储和检索更加高效。
|
|||
|
3. 配置文件:Protobuf 可以用于存储和传输配置信息,例如应用程序的配置参数。
|
|||
|
|
|||
|
## 3.Java中使用
|
|||
|
|
|||
|
创建一个Maven项目其中使用 Java 作为 Client 端,Python 作为 Server 端
|
|||
|
|
|||
|
```xml
|
|||
|
<dependency>
|
|||
|
<groupId>com.google.protobuf</groupId>
|
|||
|
<artifactId>protobuf-java</artifactId>
|
|||
|
<version>4.27.2</version>
|
|||
|
</dependency>
|
|||
|
```
|
|||
|
|
|||
|
在项目中创建文件夹 `script` 在其下创建 Protobuf 文件 `video_info.proto`
|
|||
|
|
|||
|
其中内容如下
|
|||
|
|
|||
|
```protobuf
|
|||
|
syntax = "proto3";
|
|||
|
|
|||
|
message VideoFeature {
|
|||
|
optional int32 author_gender = 1 ;
|
|||
|
optional int64 channel_id = 2;
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
然后导入对应的 `protoc` 工程文件,[下载对应版本的文件](https://github.com/protocolbuffers/protobuf/releases/tag/v27.2)
|
|||
|
并解压到`script`目录下
|
|||
|
|
|||
|
然后创建一个生成脚本 `build_pb.sh`内容如下
|
|||
|
|
|||
|
```sh
|
|||
|
#!/bin/bash
|
|||
|
SRC_DIR="."
|
|||
|
JAVA_DST_DIR="../src/main/java"
|
|||
|
PYTHON_DST_DIR="../src/main/python"
|
|||
|
|
|||
|
./protoc-27.2-osx-aarch_64/bin/protoc -I=$SRC_DIR --java_out=$JAVA_DST_DIR $SRC_DIR/AllTypes.proto
|
|||
|
./protoc-27.2-osx-aarch_64/bin/protoc -I=$SRC_DIR --python_out=$PYTHON_DST_DIR $SRC_DIR/AllTypes.proto
|
|||
|
```
|
|||
|
|
|||
|
其中最后面的文件名是上面创建的`proto`文件的名称,运行sh脚本
|
|||
|
|
|||
|
能够生成两个文件 `VideoInfo.java` `video_info_pb2.py`
|
|||
|
|
|||
|
然后我们创建两个运行文件 `Client.java` `Server.py`,其中内容如下
|
|||
|
|
|||
|
```java
|
|||
|
import java.io.DataOutputStream;
|
|||
|
import java.io.IOException;
|
|||
|
import java.net.Socket;
|
|||
|
|
|||
|
/**
|
|||
|
* ClassName: Client
|
|||
|
* Package: com.yovinchen.protobuf
|
|||
|
*
|
|||
|
* @author yovinchen
|
|||
|
* @since 2024/7/25 上午9:33
|
|||
|
*/
|
|||
|
|
|||
|
public class Client {
|
|||
|
|
|||
|
public static byte[] msg;
|
|||
|
|
|||
|
static {
|
|||
|
VideoInfo.VideoFeature feature = VideoInfo.VideoFeature.newBuilder()
|
|||
|
.setAuthorGender(123)
|
|||
|
.setChannelId(321)
|
|||
|
.build();
|
|||
|
msg = feature.toByteArray();
|
|||
|
|
|||
|
// msg = "测试字符串".getBytes();
|
|||
|
// msg = "{\"author_gender\":123,\"channel_id\":321}".getBytes();
|
|||
|
}
|
|||
|
|
|||
|
public static void main(String[] args) throws IOException {
|
|||
|
System.out.println("客户端启动...");
|
|||
|
// 创建一个流套接字并将其连接到指定主机上的指定端口号
|
|||
|
Socket socket = new Socket("localhost", 8001);
|
|||
|
// 向服务器端发送数据
|
|||
|
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
|||
|
out.write(msg);
|
|||
|
out.close();
|
|||
|
socket.close();
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
```python
|
|||
|
import socket
|
|||
|
|
|||
|
import video_info_pb2
|
|||
|
|
|||
|
|
|||
|
def parse(buf):
|
|||
|
try:
|
|||
|
video_feature = video_info_pb2.VideoFeature()
|
|||
|
video_feature.ParseFromString(buf)
|
|||
|
return video_feature
|
|||
|
except Exception:
|
|||
|
return "暂时不支持转换"
|
|||
|
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
print("Server is starting")
|
|||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|||
|
sock.bind(('localhost', 8001)) # 配置soket,绑定IP地址和端口号
|
|||
|
sock.listen(5) # 设置最大允许连接数
|
|||
|
while True: # 循环轮询socket状态,等待访问
|
|||
|
connection, address = sock.accept()
|
|||
|
buf = connection.recv(1024)
|
|||
|
print(f"原始数据:{buf}")
|
|||
|
print(f"数据长度:{len(buf)}")
|
|||
|
|
|||
|
print(parse(buf))
|
|||
|
connection.close()
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
然后先运行 `Server.py` 然后再运行 `Client.java`,然后就能在Server看到原始数据以及解析出来的数据。
|
|||
|
|
|||
|
解除`msg`中的注释测试`Json`进行数据传输的数据长度
|
|||
|
|
|||
|
![image-20240729103528251](https://lsky.hhdxw.top/imghub/2024/07/image-202407291722220767.png)
|
|||
|
|
|||
|
## 4.测试
|
|||
|
|
|||
|
```protobuf
|
|||
|
syntax = "proto3";
|
|||
|
|
|||
|
// 定义一个消息,该消息包含所有基本的数据类型。
|
|||
|
message AllTypes {
|
|||
|
// 布尔类型
|
|||
|
bool bool_field = 1; // 布尔值
|
|||
|
|
|||
|
// 字符串类型
|
|||
|
string string_field = 2; // UTF-8 编码的字符串
|
|||
|
|
|||
|
// 字节流类型
|
|||
|
bytes bytes_field = 3; // 原始字节流
|
|||
|
|
|||
|
// 整数类型
|
|||
|
int32 int32_field = 4; // 32位有符号整数
|
|||
|
int64 int64_field = 5; // 64位有符号整数
|
|||
|
uint32 uint32_field = 6; // 32位无符号整数
|
|||
|
uint64 uint64_field = 7; // 64位无符号整数
|
|||
|
sint32 sint32_field = 8; // 32位有符号整数,使用 zigzag 编码
|
|||
|
sint64 sint64_field = 9; // 64位有符号整数,使用 zigzag 编码
|
|||
|
|
|||
|
// 浮点数类型
|
|||
|
float float_field = 14; // 单精度浮点数
|
|||
|
double double_field = 15; // 双精度浮点数
|
|||
|
|
|||
|
// 固定宽度整数类型
|
|||
|
fixed32 fixed32_field = 10; // 32位无符号整数,小端存储
|
|||
|
fixed64 fixed64_field = 11; // 64位无符号整数,小端存储
|
|||
|
sfixed32 sfixed32_field = 12; // 32位有符号整数,小端存储
|
|||
|
sfixed64 sfixed64_field = 13; // 64位有符号整数,小端存储
|
|||
|
|
|||
|
// 重复字段类型
|
|||
|
repeated int32 repeated_int32_field = 31; // 可以包含多个元素的 int32 字段
|
|||
|
|
|||
|
// 映射字段类型
|
|||
|
map<int32, string> map_int32_string_field = 32; // 键为 int32,值为 string 的映射
|
|||
|
|
|||
|
// 枚举类型
|
|||
|
EnumType enum_field = 33; // 枚举类型字段
|
|||
|
|
|||
|
// 嵌套消息类型
|
|||
|
MessageType nested_message_field = 34; // 另一个消息类型的字段
|
|||
|
|
|||
|
// 嵌套的消息类型定义
|
|||
|
message MessageType {
|
|||
|
string nested_string_field = 1; // 嵌套消息中的字符串字段
|
|||
|
}
|
|||
|
|
|||
|
// 枚举类型定义
|
|||
|
enum EnumType {
|
|||
|
ENUM_VALUE_0 = 0; // 枚举值 0
|
|||
|
ENUM_VALUE_1 = 1; // 枚举值 1
|
|||
|
ENUM_VALUE_2 = 2; // 枚举值 2
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 以下是用于包装基本类型的特殊消息类型,它们允许携带额外的元数据,如 null 值。
|
|||
|
message BoolValue {bool value = 1;} // 包装布尔值
|
|||
|
message StringValue {string value = 1;} // 包装字符串值
|
|||
|
message BytesValue {bytes value = 1;} // 包装字节流值
|
|||
|
message Int32Value {int32 value = 1;} // 包装 32 位整数值
|
|||
|
message Int64Value {int64 value = 1;} // 包装 64 位整数值
|
|||
|
message UInt32Value {uint32 value = 1;} // 包装无符号 32 位整数值
|
|||
|
message UInt64Value {uint64 value = 1;} // 包装无符号 64 位整数值
|
|||
|
message SInt32Value {sint32 value = 1;} // 包装 zigzag 编码的 32 位整数值
|
|||
|
message SInt64Value {sint64 value = 1;} // 包装 zigzag 编码的 64 位整数值
|
|||
|
message Fixed32Value {fixed32 value = 1;} // 包装小端存储的 32 位整数值
|
|||
|
message Fixed64Value {fixed64 value = 1;} // 包装小端存储的 64 位整数值
|
|||
|
message SFixed32Value {sfixed32 value = 1;} // 包装小端存储的 32 位有符号整数值
|
|||
|
message SFixed64Value {sfixed64 value = 1;} // 包装小端存储的 64 位有符号整数值
|
|||
|
message FloatValue {float value = 1;} // 包装单精度浮点数值
|
|||
|
message DoubleValue {double value = 1;} // 包装双精度浮点数值
|
|||
|
```
|
|||
|
|
|||
|
服务端和接收端`AllTypesClient.java` `AllTypeServer.py`
|
|||
|
|
|||
|
```java
|
|||
|
import com.google.protobuf.ByteString;
|
|||
|
|
|||
|
import java.io.DataOutputStream;
|
|||
|
import java.io.IOException;
|
|||
|
import java.net.Socket;
|
|||
|
|
|||
|
/**
|
|||
|
* ClassName: Client
|
|||
|
* Package: com.yovinchen.protobuf
|
|||
|
*
|
|||
|
* @author yovinchen
|
|||
|
* @since 2024/7/25 上午9:33
|
|||
|
*/
|
|||
|
public class AllTypesClient {
|
|||
|
|
|||
|
public static void main(String[] args) throws IOException {
|
|||
|
System.out.println("客户端启动...");
|
|||
|
|
|||
|
// 创建一个 AllTypes 消息实例
|
|||
|
AllTypesOuterClass.AllTypes.Builder builder = AllTypesOuterClass.AllTypes.newBuilder();
|
|||
|
builder.setBoolField(true);
|
|||
|
builder.setStringField("测试字符串");
|
|||
|
builder.setBytesField(ByteString.copyFromUtf8("字节流"));
|
|||
|
builder.setInt32Field(123);
|
|||
|
builder.setInt64Field(123L);
|
|||
|
builder.setUint32Field(456);
|
|||
|
builder.setUint64Field(456L);
|
|||
|
builder.setSint32Field(-123);
|
|||
|
builder.setSint64Field(-123L);
|
|||
|
builder.setFixed32Field(123);
|
|||
|
builder.setFixed64Field(123L);
|
|||
|
builder.setSfixed32Field(-123);
|
|||
|
builder.setSfixed64Field(-123L);
|
|||
|
builder.setFloatField(123.45f);
|
|||
|
builder.setDoubleField(123.45);
|
|||
|
builder.addRepeatedInt32Field(1);
|
|||
|
builder.addRepeatedInt32Field(2);
|
|||
|
builder.putMapInt32StringField(1, "value1");
|
|||
|
builder.putMapInt32StringField(2, "value2");
|
|||
|
builder.setEnumField(AllTypesOuterClass.AllTypes.EnumType.ENUM_VALUE_1);
|
|||
|
builder.setNestedMessageField(AllTypesOuterClass.AllTypes.MessageType.newBuilder()
|
|||
|
.setNestedStringField("嵌套字符串")
|
|||
|
.build());
|
|||
|
|
|||
|
// 构建消息
|
|||
|
AllTypesOuterClass.AllTypes allTypesMsg = builder.build();
|
|||
|
|
|||
|
// 创建一个流套接字并将其连接到指定主机上的指定端口号
|
|||
|
Socket socket = new Socket("localhost", 8001);
|
|||
|
|
|||
|
// 向服务器端发送数据
|
|||
|
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
|||
|
out.write(allTypesMsg.toByteArray());
|
|||
|
|
|||
|
out.close();
|
|||
|
socket.close();
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
```python
|
|||
|
import socket
|
|||
|
|
|||
|
import AllTypes_pb2
|
|||
|
|
|||
|
|
|||
|
def parse(buf):
|
|||
|
try:
|
|||
|
all_types_msg = AllTypes_pb2.AllTypes() # 创建 AllTypes 消息实例
|
|||
|
all_types_msg.ParseFromString(buf) # 从字节流中解析消息
|
|||
|
return all_types_msg # 返回解析后的消息实例
|
|||
|
except Exception as e:
|
|||
|
print(f"Error parsing message: {e}")
|
|||
|
return None # 如果解析失败,返回 None 或者自定义的错误信息
|
|||
|
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
print("Server is starting")
|
|||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|||
|
sock.bind(('localhost', 8001))
|
|||
|
sock.listen(5)
|
|||
|
|
|||
|
while True:
|
|||
|
connection, address = sock.accept()
|
|||
|
buf = connection.recv(1024)
|
|||
|
print(f"原始数据: {buf}")
|
|||
|
print(f"数据长度:{len(buf)}")
|
|||
|
|
|||
|
parsed_msg = parse(buf)
|
|||
|
if parsed_msg is not None:
|
|||
|
print(parsed_msg) # 输出解析后的消息
|
|||
|
else:
|
|||
|
print("无法解析消息")
|
|||
|
|
|||
|
connection.close()
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
![image-20240729103332905](https://lsky.hhdxw.top/imghub/2024/07/image-202407291722220780.png)
|