unityloverz 发表于 2022-3-28 09:39

protobuf 3 教程

简介


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 需要转换的对象的builder实例   * @param json 需要转换的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)      })
页: [1]
查看完整版本: protobuf 3 教程