找回密码
 立即注册
查看: 351|回复: 0

protobuf 3 教程

[复制链接]
发表于 2022-3-28 09:39 | 显示全部楼层 |阅读模式
简介


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)      })
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2025-5-9 10:32 , Processed in 0.139241 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表