- 了解Protocol Buffers协议
Protocal Buffers是google推出的一种序列化协议,用于结构化的数据序列化、反序列化。
官方解释:Protocol Buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法。可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。 - 为什么要使用protobuf
使用protobuf的原因肯定是为了解决开发中的一些问题,那使用其他的序列化机制会出现什么问题呢?
(1)java默认序列化机制:效率极低,而且还能不能跨语言之间共享数据。
(2)XML常用于与其他项目之间数据传输或者是共享数据,但是编码和解码会造成很大的性能损失。
(3)gson格式也是常见的一种,但是gson在解析的时候非常耗时,而且gson结构非常占内存。
但是我们protobuf是一种灵活的、高效的、自动化的序列化机制,可以有效的解决上面的问题。由于 protobuf是跨语言的,所以用不同的语言序列化对象后,生成一段字节码,之后可以其他任何语言反序列化并自用,大大方便了跨语言的通讯,同时也提高了效率
3.protobuf常见数据类型与Java对照表
- 语法类型讲解,创建user_login.proto文件
syntax = "proto3";//声明版本 syntax=”proto3”,如果没有声明,则默认是proto2.
option java_package = "com.test.proto";//指定生成的类应该放在什么Java包名下.
option java_outer_classname = "MessageUserLogin";//定义应该包含这个文件中所有类的类名,调用时MessageUserLogin.MessageUserLoginRequest 或者 MessageUserLogin.MessageUserLoginResponse.
option java_multiple_files = false;//生成的类是否应该放在一个单独的Java文件里,是否需要將生成的类拆分为多个.默认false,可以不写.
message MessageUserLoginRequest {//message对应java的class
/** 字段编号
在 Protocol Buffers (protobuf) 中,每个字段都必须有一个唯一的标识符,这个标识符是一个整数,称为字段编号(Field Number)。
字段编号用于在序列化和反序列化过程中唯一地识别一个字段,即使在不同版本的协议中字段的顺序发生变化,只要字段编号不变,就可以保证数据的一致性和正确性。
字段编号的范围是1到2^29-1,但是通常建议将字段编号保持在1到1500之间,以避免冲突。字段编号不能重复,否则编译器会报错。
字段编号的选择也会影响序列化后的二进制数据的大小和性能,较小的字段编号会导致更小的二进制数据和更快的序列化/反序列化速度,但这种影响通常可以忽略不计。
在实际开发中,我们通常根据字段的重要性和出现的频率来选择字段编号,例如将重要的、经常使用的字段编号设置得较小。
*/
string a = 1; //string 数据类型 a字段名 1字段编号
int32 b = 2;
int64 c = 3;
bool d = 4;
double e = 5;
float f = 6;
repeated string g = 7; //repeated 对应java的List集合 repeated string g = List<String> g
map<string, string> h = 8;//map 对应java的Map集合
repeated MessageUserLoginResponse i = 9;//集合对象 repeated MessageUserLoginResponse i = List<MessageUserLoginResponse> g
map<string, MessageUserLoginResponse> j = 10;//map对象
MessageUserLoginResponse k = 11;//嵌套对象字段
}
message MessageUserLoginResponse {
string a = 1;
int32 b = 2;
int64 c = 3;
bool d = 4;
double e = 5;
float f = 6;
}
- 如何将.proto文件生成java文件,目前简单介绍一个窗口生成的。idea工具生成后续追加
下载protoc.exe编辑器
下载链接: 下载地址
根据同样的系统选择不同的编译器就ok,我用的windows, 所以选择 :protoc-3.11.0-win64.zip
下载后只用到bin里面的protoc.exe - SpringBoot使用protobuf格式的接口
建立SpringBoot项目,pom.xml需要引用内容如下:
根据自己项目实际情况添加,我的springBoot版本是2.7.17,springBoot版本这里比较重要,
因为后边在模拟Content-Type=application/x-protobuf请求时,是要配置我们服务端能请求类型内容。
Spring Boot 2.6及更高版本中,配置方式是不同的。
根据自己项目实际情况添加
根据自己项目实际情况添加
根据自己项目实际情况添加
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
<!-- Generated by https://start.springboot.io -->
<!-- 优质的 spring/boot/data/security/cloud 框架中文文档尽在 => https://springdoc.cn -->
<groupId>com.game.module</groupId>
<artifactId>GameModule</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>GameModule</name>
<description>GameModule</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--springBoot相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--springBoot相关-->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
<!-- 使用最新的版本或适合你项目的版本 -->
</dependency>
<!--protobuf相关-->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>com.googlecode.protobuf-java-format</groupId>
<artifactId>protobuf-java-format</artifactId>
<version>1.2</version>
</dependency>
<!--protobuf相关-->
<!-- 网络请求依赖 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4</version>
</dependency>
<!-- 网络请求依赖 -->
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.0</version>
</dependency>
<!-- 工具类 -->
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- websocket -->
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<!-- lombok -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.2</version>
<configuration>
<fork>true</fork>
<mainClass>com.game.module.gamemodule.GameModuleApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<!-- 是否替换资源中的属性 -->
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>*</include>
</includes>
</resource>
</resources>
</build>
</project>
编写.proto文件,user_login.proto 内容如下:
syntax = "proto3";
option java_package = "com.boomsecret.protobuf";
option java_outer_classname = "MessageUserLogin";
message MessageUserLoginRequest {
string username = 1;
string password = 2;
}
message MessageUserLoginResponse {
string access_token = 1;
string username = 2;
}
- 生成java代码:
我的做法是将下载好的protoc.exe放在项目里面了,这样我直接在项目路径下cmd进去好找位置,你也可以不放。
然后进入protoc.exe存方位cmd进去执行命令。
示例: .\center\user_login.proto 是你的.proto文件所在路径 --java_out=./ 指定生成文件的路径
大家根据实际情况自行修改,我的是在同一级所以.\user_login.proto
.\protoc.exe .\user_login.proto --java_out=./
强调下你的项目路径就是你.proto里面指定的option java_package = 路径
8.编写protobuf格式的Controller接口:
package com.example.protobuf.demo.controller;
import com.boomsecret.protobuf.MessageUserLogin;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import util.HttpUtils;
import java.net.URI;
import java.util.UUID;
@Controller
public class TestController {
@RequestMapping(value = "/demo/test", produces = "application/x-protobuf")
@ResponseBody
public MessageUserLogin.MessageUserLoginResponse getPersonProto(@RequestBody MessageUserLogin.MessageUserLoginRequest request) {
MessageUserLogin.MessageUserLoginResponse.Builder builder = MessageUserLogin.MessageUserLoginResponse.newBuilder();
builder.setAccessToken(UUID.randomUUID().toString()+"_res");
builder.setUsername(request.getUsername()+"_res");
return builder.build();
}
}
编写测试HttpUtils模拟application/x-protobuf
package util;
import com.google.protobuf.GeneratedMessageV3;
import com.googlecode.protobuf.format.JsonFormat;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.HttpClients;
import java.io.ByteArrayInputStream;
import java.io.IOException;
public class HttpUtils {
public static HttpResponse doPost(HttpPost post, GeneratedMessageV3 message) throws IOException {
HttpClient httpclient = HttpClients.createDefault();
String requestUrl = post.getURI().toString();
ByteArrayInputStream inputStream = new ByteArrayInputStream(message.toByteArray());
InputStreamEntity inputStreamEntity = new InputStreamEntity(inputStream);
post.setEntity(inputStreamEntity);
post.addHeader("Content-Type", "application/x-protobuf");
for (Header header : post.getAllHeaders()) {
System.out.println(header.getName() + ":" + header.getValue());
}
StringBuilder sb = new StringBuilder();
sb.append("curl -XPOST ");
for (Header header : post.getAllHeaders()) {
sb.append(" -H \"").append(header.getName()).append(":").append(header.getValue()).append("\"");
}
String jsonBody = JsonFormat.printToString(message);
jsonBody = jsonBody.replace("\"", "\\\"");
sb.append(" -d \"").append(jsonBody).append("\"");
sb.append(" ").append(requestUrl);
System.out.println(sb.toString());
return httpclient.execute(post);
}
}
编写测试接口
package com.game.module.gamemodule.controller;
import com.game.module.gamemodule.proto.MessageUserLogin;
import com.game.module.gamemodule.utils.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.net.URI;
import java.util.Arrays;
import java.util.UUID;
@Slf4j
@RestController
public class UserLoginController {
@RequestMapping(value = "/demo/test", produces = "application/x-protobuf")
@ResponseBody
public MessageUserLogin.MessageUserLoginResponse getPersonProto(@RequestBody MessageUserLogin.MessageUserLoginRequest request) {
MessageUserLogin.MessageUserLoginResponse.Builder builder = MessageUserLogin.MessageUserLoginResponse.newBuilder();
String token = UUID.randomUUID().toString() + "_res";
builder.setAccessToken(token);
String name = request.getUsername() + "_res";
builder.setUsername(name);
return builder.build();//序列化
}
@RequestMapping(value = "/userTest")
public void test() {
try {
URI uri = new URI("http", null, "localhost", 9101, "/demo/test", "", null);
HttpPost request = new HttpPost(uri);
MessageUserLogin.MessageUserLoginRequest.Builder builder = MessageUserLogin.MessageUserLoginRequest.newBuilder();
builder.setUsername("tom");
builder.setPassword("123456");
log.info("序列化后的内容:{}", Arrays.toString(builder.build().toByteArray()));
HttpResponse response = HttpUtils.doPost(request, builder.build());// builder.build() 序列化
MessageUserLogin.MessageUserLoginResponse messageUserLoginResponse = MessageUserLogin.MessageUserLoginResponse.parseFrom(response.getEntity().getContent());//parseFrom 反序列化
log.info("请求结果反序列化后:{}", messageUserLoginResponse.toString());
} catch (Exception e) {
System.out.println("请求异常");
}
}
}
以debug方式运行SpringBoot项目,并在controller加断点,然后运行测试代码: 访问http://localhost:9101/userTest 接口
此时我们会看到415,表示错误org.springframework.web.HttpMediaTypeNotSupportedException表明服务器不支持你的客户端尝试使用的application/x-protobuf;charset=UTF-8这种内容类型。这通常发生在服务器只处理特定的媒体类型,而application/x-protobuf不在这些类型中时。此时就要配置服务端支持protobuf,就与上面我们的springboot版本配置相关,我的springBoot版本是2.7.17。Spring Boot 2.6及更高版本中,配置方式是不同的。
添加MessageConverterConfig,2.6以上配置
package com.game.module.gamemodule.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
@Configuration
public class MessageConverterConfig {
@Bean
public ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
}
2.6以下配置方式
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new ProtobufHttpMessageConverter());
}
}
再次执行就可以了
可以看到请求过来的数据是正确的,放行后可以看到响应数据也是正确的: