美文网首页
protobuf 3 教程

protobuf 3 教程

作者: dhz120 | 来源:发表于2022-03-26 16:11 被阅读0次

    简介

    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)
          })
    

    相关文章

      网友评论

          本文标题:protobuf 3 教程

          本文链接:https://www.haomeiwen.com/subject/ewrsjrtx.html