|
简介
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协议; 否则使用proto2syntax = "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可以理解为定义一个结构体的意思), 名为resultmessage 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 */@SpringBootApplicationpublic 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 */@RestControllerpublic 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) }) |
|