简介
protobuf是继json,xml之后出现的一种新的数据序列化方式。其特点是数据以二进制形式呈现、数据量小、解析效率快、开发简单。特别适合对传输性能要求高的场景(比如:高并发数据传输)。
怎么玩
一、下载protocal buffer 编译器:https://github.com/protocolbuffers/protobuf
# 以linux版本为例,下载编译器
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_64.zip
# 解压
$ unzip ./protoc-3.19.4-linux-x86_64.zip -d protoc
# 将编译器protoc放到/usr/local/bin下,方便后边使用
$ cd /usr/local/bin
$ sudo ls -s 使用绝对路径/protoc/bin/protoc protoc
二、定义消息文件
参考官方指南:https://developers.google.com/protocol-buffers/docs/proto3
下边以myresult.proto
为例,做简单说明
// 指定使用proto3协议; 否则使用proto2
syntax = "proto3";
// 定义消息的命名空间
package pb;
// 导入Any类型
import "google/protobuf/any.proto";
// java_xx表示生成java代码需要的几个属性
// java_package: 指定生成java的包名
option java_package = "com.sy.common.pojo";
// java_outer_classname: 生成java的类型,注意不能与message中定义的名称重名
option java_outer_classname = "MyResp";
// 用message定义一个消息(message可以理解为定义一个结构体的意思), 名为result
message Result {
// 定义一个int类型的变量,变量名为code,
// 赋值为1表示的是这个变量的唯一编号,序列化的时候会用这个编号替代变量名
// 注意:编号1~15,编码时占1字节。16~2047编码时占两个字节。编号19000~19999为保留编号,不能用。
int32 code = 1;
// 定义一个string类型的变量,名为msg, 唯一编号为2
string msg = 2;
// 定义一个Any类型(Any表示泛型,也可以理解为java的Object类型)的变量,名为data,唯一编号为3
// 定义成Any类型的好处时,赋值的时候可以给data赋任意类型的值
google.protobuf.Any data = 3;
}
// 定义一个Student类型的消息
message Student {
int32 id = 1;
string name = 2;
// repeated Book表示 List<Book>的意思
// 定义一个List类型的字段,名为book,编号为3
repeated Book book = 3;
// map<type1, type2>
// 定义一个map列席的字段,名为attr,编号为4
map<string, string> attr = 4;
// 定义一个子类型Book, 其中包含id,name两个字段
message Book {
int32 id = 1;
string name = 2;
}
}
三、根据需要,编译生成指定语言文件后使用。
# 生成java文件, 其中--java_out表示生成的java文件放在什么位置; myresult.proto表示用哪个proto源文件去生成, 可以是一个也可以指定多个
$ protoc --java_out=. myresult.proto
# 生成js文件, 其中--js_out表示生成的js文件放在什么位置(注意需要带上import_style=commonjs,binary:, 要不然前端用的时候会报错);myresult.proto表示用哪个proto源文件去生成, 可以是一个也可以指定多个
$ protoc --js_out=import_style=commonjs,binary:. myresult.proto
# 查看生成的文件:MyResult.java, myresult_pb.js
$ tree
.
├── com
│ └── sy
│ └── common
│ └── pojo
│ └── MyResult.java
├── myresult_pb.js
└── myresult.proto
实践
一、springboot rest api 测试
后端几个注意的地方
在pom文件中添加依赖
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.19.4</version>
</dependency>
<!-- protobuf 和 json 相互转换-->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.19.4</version>
</dependency>
添加protobuf序列化支持
package com.sy.comm.config;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
/**
* ProtoBufConfig class
*
* @author donghuaizhi
* @date 2022/3/15
*/
@SpringBootApplication
public class ProtoBufConfig {
/**
* protobuf 序列化
* @return
*/
@Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
/**
* protobuf 反序列化
*/
@Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter protobufHttpMessageConverter) {
return new RestTemplate(Collections.singletonList(protobufHttpMessageConverter));
}
}
添加protobuf与json相互转换的工具类
package com.sy.comm.util;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import com.google.protobuf.util.JsonFormat;
import java.io.IOException;
import java.util.Arrays;
/**
* ProtoJsonUtils class
*
* @author donghuaizhi
* @date 2022/3/17
*/
public class ProtoJsonUtils {
/**
* 将protobuf对象转换为json字符串
* 注意:不支持any字段
* @param sourceMessage
* @return
* @throws IOException
*/
public static String pb2Json(Message sourceMessage) throws IOException {
return JsonFormat.printer().print(sourceMessage);
}
/**
* 将json字符串转换为protobuf对象
* 注意:不支持any字段
* @param targetBuilder [in] 需要转换的对象的builder实例
* @param json [in] 需要转换的json字符串
* @return 转换后的protobuf对象
* @throws IOException
*/
public static Message json2Pb(Message.Builder targetBuilder, String json) throws IOException {
JsonFormat.parser().merge(json, targetBuilder);
return targetBuilder.build();
}
private final JsonFormat.Printer printer;
private final JsonFormat.Parser parser;
/**
* 空构造
*/
public ProtoJsonUtils() {
printer = JsonFormat.printer();
parser = JsonFormat.parser();
}
/**
* 如果需要转换带Any字段的,需要用这个构造
* @param anyFieldDescriptor
*/
public ProtoJsonUtils(Descriptors.Descriptor... anyFieldDescriptor) {
// 可以为 TypeRegistry 添加多个不同的Descriptor
JsonFormat.TypeRegistry typeRegistry = JsonFormat.TypeRegistry.newBuilder().add(Arrays.asList(anyFieldDescriptor)).build();
// usingTypeRegistry 方法会重新构建一个Printer
printer = JsonFormat.printer().usingTypeRegistry(typeRegistry);
parser = JsonFormat.parser().usingTypeRegistry(typeRegistry);
}
/**
* 将protobuf对象转换为json字符串, 传入anyFieldDescriptor后支持any转换
* @param sourceMessage
* @return
* @throws IOException
*/
public String toJson(Message sourceMessage) throws IOException {
return printer.print(sourceMessage);
}
/**
* 将json字符串转换为protobuf对象, 传入anyFieldDescriptor后支持any转换
* @param targetBuilder
* @param json
* @return
* @throws IOException
*/
public Message toProto(Message.Builder targetBuilder, String json) throws IOException {
parser.merge(json, targetBuilder);
return targetBuilder.build();
}
}
添加测试接口
package com.sy.test.controller;
import com.google.protobuf.Any;
import com.sy.comm.util.ProtoJsonUtils;
import com.sy.common.pojo.MyResp;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
/**
* PbController class
*
* @author donghuaizhi
* @date 2022/3/19
*/
@RestController
public class PbController {
/**
* 不带any类型的测试
* 注意:返回Student时,需要指定produces = "application/x-protobuf", 要不然前端拿到的是json字符串
* @return
*/
@RequestMapping(value = "/pb/get1", produces = "application/x-protobuf")
MyResp.Student get1() {
MyResp.Student student = getStudent();
// protobuf与json互转测试
try {
// protobuf to json
String jStudent = ProtoJsonUtils.pb2Json(student);
System.out.println("-----------student json---------- \n" + jStudent);
// json to protobuf
MyResp.Student pStudent = (MyResp.Student) ProtoJsonUtils.json2Pb(MyResp.Student.newBuilder(), jStudent);
System.out.println("--------student proto-------\n" + pStudent);
} catch (IOException e) {
e.printStackTrace();
}
return student;
}
/**
* 带any类型的测试
* @return
*/
@RequestMapping(value = "/pb/get2", produces = "application/x-protobuf")
MyResp.Result get2() {
MyResp.Student student = getStudent();
MyResp.Result result = MyResp.Result.newBuilder()
.setCode(200)
.setMsg("hello")
.setData(Any.pack(student)) // 设置any类型的数据
.build();
System.out.println("-----result----- \n" + result);
// protobuf与json互转测试
try {
// 注意proto中有any类型时,与json互转,需要指定对应类型的Descriptor, 否则报错
ProtoJsonUtils protoJsonUtils = new ProtoJsonUtils(MyResp.Student.getDescriptor());
// protobuf to json
String jResult = protoJsonUtils.toJson(result);
System.out.println("-----------result json---------- \n" + jResult);
// json to protobuf
MyResp.Result pResult= (MyResp.Result) protoJsonUtils.toProto(MyResp.Result.newBuilder(), jResult);
System.out.println("--------result proto-------\n" + pResult);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
* 接收前端传过来的student对象
* @param student
*/
@PostMapping("/pb/set")
MyResp.Student setStudent(@RequestBody MyResp.Student student) {
System.out.println(student);
return student;
}
/**
* 返回一个学生对象
* @return
*/
private MyResp.Student getStudent() {
MyResp.Student.Book book = MyResp.Student.Book.newBuilder().setId(1).setName("罪与罚").build();
MyResp.Student student = MyResp.Student.newBuilder()
.setId(1234)
.setName("hello")
.addBook(book)
.build();
System.out.println("-------getStudent---------\n" + student);
return student;
}
}
由于protobuf前后端调试工具少,可以直接在后端建立一个单元测试类,模拟前端发http请求,来自己测试:
封装http请求的工具类MyHttpUtils.java
package com.sy.comm.util;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.HttpClients;
import java.io.IOException;
import java.io.InputStream;
/**
* MyHttpUtils class
*
* @author donghuaizhi
* @date 2022/3/26
*/
public class MyHttpUtils {
/**
* 模拟get请求
* @param url
* @return
* @throws IOException
*/
public static InputStream getReq(String url) throws IOException {
return httpRequest(new HttpGet(url)).getContent();
}
/**
* 模拟protobuf的post请求
* @param url
* @param data
* @return
* @throws IOException
*/
public static InputStream pbPostReq(String url, byte[] data) throws IOException {
HttpEntity httpEntity = postReq(url, "application/x-protobuf", new ByteArrayEntity(data));
return httpEntity.getContent();
}
/**
* 模拟post请求
* @param url
* @param contentType
* @param entity
* @return
* @throws IOException
*/
public static HttpEntity postReq(String url, String contentType, HttpEntity entity) throws IOException {
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-Type",contentType);
httpPost.setEntity(entity);
return httpRequest(httpPost);
}
/**
* 模拟http发请求
* @param request
* @return
* @throws IOException
*/
public static HttpEntity httpRequest(HttpUriRequest request) throws IOException {
return HttpClients.createDefault().execute(request).getEntity();
}
}
建立测试类,模拟前端发请求
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class ModifyDataTest {
@Test
public void testGet() throws IOException {
String url = "http://localhost:8080/pb/get1";
InputStream inputStream = MyHttpUtils.getReq(url);
MyResp.Student result = MyResp.Student.parseFrom(inputStream);
Assert.assertEquals(1234, result.getId());
Assert.assertEquals("hello", result.getName());
}
@Test
public void testPost() throws IOException {
String url = "http://localhost:8080/pb/set";
MyResp.Student.Book book = MyResp.Student.Book.newBuilder().setId(1).setName("罪与罚").build();
MyResp.Student student = MyResp.Student.newBuilder()
.setId(1234)
.setName("hello")
.addBook(book)
.build();
InputStream inputStream = MyHttpUtils.pbPostReq(url, student.toByteArray());
MyResp.Student result = MyResp.Student.parseFrom(inputStream);
Assert.assertEquals(student, result);
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) 说明:
SpringBoot启动测试时报错(javax.websocket.server.ServerContainer not available), 经查阅资料,得知SpringBootTest在启动的时候不会启动服务器,所以WebSocket自然会报错,这个时候需要添加选项webEnvironment,以便提供一个测试的web环境。
前端几个注意的地方
发送get请求时要加 responseType: 'arraybuffer'
this.$axios({
method: 'get',
url: 'template/pb/testany',
responseType: 'arraybuffer'
}).then(res => {
console.log(res.data)
const course2 = protos.Result.deserializeBinary(res.data)
// const cc = new proto.Course(course2.getData())
// const course2 = new proto.Course(res.data)
console.log(course2.getCode(), course2.getMsg(), course2.getData().getTypeName(), course2.getData().unpack(proto.Course.deserializeBinary, 'baeldung.Course').toObject())
})
发送post请求时要加headers: { 'Content-Type': 'application/x-protobuf' }
this.$axios({
method: 'post',
url: 'template/pb/setcourse',
headers: {
'Content-Type': 'application/x-protobuf'
},
data: data
}).then(res => {
console.log(res.data)
})
网友评论