美文网首页
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