Grpc介绍
在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
Grpc支持4种模式:
- 单项 RPC,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。
rpc SayHello(HelloRequest) returns (HelloResponse){
}
- 服务端流式 RPC,即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
- 客户端流式 RPC,即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
- 双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}
Grpc优势
- 使用protobuf,独立于语言的协议,支持多语言之间的通讯;数据压缩更高;序列化和反序列化占用系统资源更低;
- 支持HTTP2,主要利用了HTTP2多路复用、头部压缩,并且请求头部可添加信息便于拦截、调用链分析
- 双向流式
- 多语言支持,支持java、python、node.js、ruby等等
Hello World
gprc开发方式与thrift非常类似,都是使用IDL声明接口 总体思路,
- 创建test.proto文件
- mvn install创建生成Java文件
- 创建client和server 项目,实现RPC调用和返回
- 启动client和server项目
创建test.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.test.grpc.hello";
option java_outer_classname = "HelloWorldProto";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
注意参数option java_package 指定生成的JAVA文件路径;
java_outer_classname 最外层类名称;
java_multiple_files 如果有多种生成语言将生成一个压缩包
具体声明方式参加proto语法
创建客户端
package com.test.grpc.hello;
import com.test.grpc.simple.TestPerformanceGrpc;
import com.test.grpc.simple.TestReply;
import com.test.grpc.simple.TestRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 客户端
*
* @author junqiang.xiao@hand-china.com
* @date 2018/4/23
*/
public class GrpcSimpleClient {
private static final Logger logger = Logger.getLogger(GrpcSimpleClient.class.getName());
private final ManagedChannel channel;
private final GreeterGrpc.GreeterBlockingStub blockingStub;
private final TestPerformanceGrpc.TestPerformanceBlockingStub performanceBlockingStub;
/**
* Construct client connecting to HelloWorld server at {@code host:port}.
*/
public GrpcSimpleClient(String host, int port) {
this(ManagedChannelBuilder.forAddress(host, port)
// Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
// needing certificates.
.usePlaintext()
.build());
}
/**
* Construct client for accessing RouteGuide server using the existing channel.
*/
GrpcSimpleClient(ManagedChannel channel) {
this.channel = channel;
blockingStub = GreeterGrpc.newBlockingStub(channel);
performanceBlockingStub = TestPerformanceGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
/**
* Say hello to server.
*/
public void greet(String name) {
logger.info("Will try to greet " + name + " ...");
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
HelloReply response;
try {
response = blockingStub.sayHello(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
logger.info("Greeting: " + response.getMessage());
}
/**
* Greet server. If provided, the first element of {@code args} is the name to use in the
* greeting.
*/
public static void main(String[] args) throws Exception {
GrpcSimpleClient client = new GrpcSimpleClient("localhost", 50051);
try {
/* Access a service running on the local machine on port 50051 */
String user = "world";
if (args.length > 0) {
user = args[0];
/* Use the arg as the name to greet if provided */
}
client.greet(user + ":" + i);
} finally {
client.shutdown();
}
}
}
添加POM依赖
POM依赖如下,添加后执行mvn install 自动产生proto文件
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<grpc.version>1.11.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
<netty.tcnative.version>2.0.7.Final</netty.tcnative.version>
</properties>
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-alts</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-testing</artifactId>
<version>${grpc.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>${netty.tcnative.version}</version>
</dependency>
<dependency>
<groupId>com.google.api.grpc</groupId>
<artifactId>proto-google-common-protos</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.11.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
创建服务端
package com.test.grpc.server;
import com.google.protobuf.util.Timestamps;
import com.test.grpc.simple.*;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* 服务端
*
* @author junqiang.xiao@hand-china.com
* @date 2018/4/24
*/
public class GrpcSimpleServer {
private static final Logger logger = Logger.getLogger(GrpcSimpleServer.class.getName());
private Server server;
private void start() throws IOException {
/* The port on which the server should run */
int port = 50051;
server = ServerBuilder.forPort(port)
.addService(new PerformanceImpl())
.build()
.start();
logger.info("Server started, listening on " + port);
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
// Use stderr here since the logger may have been reset by its JVM shutdown hook.
System.err.println("*** shutting down gRPC server since JVM is shutting down");
GrpcSimpleServer.this.stop();
System.err.println("*** server shut down");
}
});
}
private void stop() {
if (server != null) {
server.shutdown();
}
}
/**
* Await termination on the main thread since the grpc library uses daemon threads.
*/
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
/**
* Main launches the server from the command line.
*/
public static void main(String[] args) throws IOException, InterruptedException {
final GrpcSimpleServer server = new GrpcSimpleServer();
server.start();
server.blockUntilShutdown();
}
static class PerformanceImpl extends TestPerformanceGrpc.TestPerformanceImplBase {
@Override
public void sayHello(TestRequest req, StreamObserver<TestReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setName(req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
}
启动服务端、客户端验证测试结果
经过10W数据单线程测试,该简单模式性能十分差,基本只是测试时使用。
服务端流式RPC
一个 服务器端流式 RPC , 客户端发送请求到服务器,拿到一个流去读取返回的消息序列。 客户端读取返回的流,直到里面没有任何消息。从例子中可以看出,通过在 响应 类型前插入 stream 关键字,可以指定一个服务器端的流方法。
该方式经过测试,性能十分显著
创建proto文件
采用头行分配三个层次进行测试,注意service方法中return 是stream
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.test.grpc.performance";
//option java_outer_classname = "TestGrpcPerformance";
package com.test.grpc.performance;
import "google/protobuf/timestamp.proto";
service TestPerformance {
rpc SayHello (TestRequest) returns (stream TestReply) {
}
}
message TestRequest {
string name = 1;
}
message HeaderReply {
repeated LineReply field1 = 1;
int32 field2 = 2;
google.protobuf.Timestamp field3 = 3;
string field4 = 4;
int32 field5 = 5;
string field6 = 6;
int32 field7 = 7;
string field8 = 8;
int32 field9 = 9;
string field10 = 10;
int32 field11 = 11;
string field12 = 12;
int32 field13 = 13;
string field14 = 14;
int32 field15 = 15;
string field16 = 16;
int32 field17 = 17;
string field18 = 18;
}
message LineReply {
repeated DistributionReply field1 = 1;
int32 field2 = 2;
google.protobuf.Timestamp field3 = 3;
string field4 = 4;
int32 field5 = 5;
string field6 = 6;
int32 field7 = 7;
string field8 = 8;
int32 field9 = 9;
string field10 = 10;
int32 field11 = 11;
string field12 = 12;
int32 field13 = 13;
string field14 = 14;
int32 field15 = 15;
string field16 = 16;
int32 field17 = 17;
string field18 = 18;
}
message DistributionReply {
string field1 = 1;
int32 field2 = 2;
google.protobuf.Timestamp field3 = 3;
string field4 = 4;
int32 field5 = 5;
string field6 = 6;
int32 field7 = 7;
string field8 = 8;
int32 field9 = 9;
string field10 = 10;
int32 field11 = 11;
string field12 = 12;
int32 field13 = 13;
string field14 = 14;
int32 field15 = 15;
string field16 = 16;
int32 field17 = 17;
string field18 = 18;
}
message TestReply {
HeaderReply field1 = 1;
int32 field2 = 2;
google.protobuf.Timestamp field3 = 3;
string name = 4;
}
创建客户端
注意客户端相应方式有两种BlockSub阻塞方式和FutureStu非阻塞方式,本例采用阻塞方式;response采用 Iterator进行接收。
package com.test.grpc.hello;
import com.test.grpc.performance.TestPerformanceGrpc;
import com.test.grpc.performance.TestReply;
import com.test.grpc.performance.TestRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 客户端
*
* @author junqiang.xiao@hand-china.com
* @date 2018/4/23
*/
public class GrpcStreamClient {
private static final Logger logger = Logger.getLogger(GrpcStreamClient.class.getName());
public static final int CALL_TIMES = 100000;
private final ManagedChannel channel;
private final GreeterGrpc.GreeterBlockingStub blockingStub;
private final TestPerformanceGrpc.TestPerformanceBlockingStub performanceBlockingStub;
/**
* Construct client connecting to HelloWorld server at {@code host:port}.
*/
public GrpcStreamClient(String host, int port) {
this(ManagedChannelBuilder.forAddress(host, port)
// Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
// needing certificates.
.usePlaintext()
.build());
}
/**
* Construct client for accessing RouteGuide server using the existing channel.
*/
GrpcStreamClient(ManagedChannel channel) {
this.channel = channel;
blockingStub = GreeterGrpc.newBlockingStub(channel);
performanceBlockingStub = TestPerformanceGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
/**
* Test message to server.
*/
public void buildTestMessage(String name) {
logger.info("Will try to greet " + name + " ...");
TestRequest request = TestRequest.newBuilder().setName(name).build();
Iterator<TestReply> response;
try {
response = performanceBlockingStub.sayHello(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
//logger.info("get response name : "+response.getName());
/*logger.info("get one line: ");
List<LineReply> lineReplyList = response.getField1().getField1List();
for (LineReply lineReply:
lineReplyList) {
logger.info("Distribution: " + lineReply.getField1(1));
logger.info("field3: " + lineReply.getField3());
}*/
}
/**
* Greet server. If provided, the first element of {@code args} is the name to use in the
* greeting.
*/
public static void main(String[] args) throws Exception {
GrpcStreamClient client = new GrpcStreamClient("localhost", 50051);
try {
/* Access a service running on the local machine on port 50051 */
String user = "world";
if (args.length > 0) {
user = args[0];
/* Use the arg as the name to greet if provided */
}
Date beginDate = new Date();
System.out.println("begin:"+beginDate);
for(int i= 0;i<CALL_TIMES;i++) {
client.buildTestMessage(user+":"+i);
}
Date endDate = new Date();
System.out.println("end:"+endDate);
System.out.println("time:"+(endDate.getTime()-beginDate.getTime())/1000+" s");
} finally {
client.shutdown();
}
}
}
创建服务端
package com.test.grpc.server;
import com.google.protobuf.util.Timestamps;
import com.test.grpc.performance.*;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* 服务端
*
* @author junqiang.xiao@hand-china.com
* @date 2018/4/24
*/
public class GrpcStreamServer {
private static final Logger logger = Logger.getLogger(GrpcStreamServer.class.getName());
private Server server;
private void start() throws IOException {
/* The port on which the server should run */
int port = 50051;
server = ServerBuilder.forPort(port)
.addService(new PerformanceImpl())
.build()
.start();
logger.info("Server started, listening on " + port);
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
// Use stderr here since the logger may have been reset by its JVM shutdown hook.
System.err.println("*** shutting down gRPC server since JVM is shutting down");
GrpcStreamServer.this.stop();
System.err.println("*** server shut down");
}
});
}
private void stop() {
if (server != null) {
server.shutdown();
}
}
/**
* Await termination on the main thread since the grpc library uses daemon threads.
*/
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
/**
* Main launches the server from the command line.
*/
public static void main(String[] args) throws IOException, InterruptedException {
final GrpcStreamServer server = new GrpcStreamServer();
server.start();
server.blockUntilShutdown();
}
static class PerformanceImpl extends TestPerformanceGrpc.TestPerformanceImplBase {
@Override
public void sayHello(TestRequest req, StreamObserver<TestReply> responseObserver) {
List<LineReply> lineReplyList = new ArrayList<LineReply>();
for (int i = 0; i < 10; i++) {
List<DistributionReply> distributionReplyList = new ArrayList<DistributionReply>();
DistributionReply distributionReply = null;
for (int j = 0; j < 10; j++) {
distributionReply = DistributionReply.newBuilder().setField1("我是第一列").setField2(2).
setField3(Timestamps.fromMillis(System.currentTimeMillis())).setField4("我是第四列").setField5(5).setField6("我是第六列").
setField7(7).setField8("我是第八列").setField9(9).setField10("我是第十列").
setField11(11).setField12("我是第十二列").setField13(13).setField14("我是第十四列").
setField15(15).setField16("我是第十六列").setField17(17).setField18("我是第十八列").
build();
distributionReplyList.add(distributionReply);
}
LineReply lineReply = LineReply.newBuilder().addAllField1(distributionReplyList).setField2(2).
setField3(Timestamps.fromMillis(System.currentTimeMillis())).setField4("我是第四列").setField5(5).setField6("我是第六列").
setField7(7).setField8("我是第八列").setField9(9).setField10("我是第十列").
setField11(11).setField12("我是第十二列").setField13(13).setField14("我是第十四列").
setField15(15).setField16("我是第十六列").setField17(17).setField18("我是第十八列").build();
lineReplyList.add(lineReply);
}
HeaderReply headerReply = HeaderReply.newBuilder().addAllField1(lineReplyList).setField2(2).setField3(Timestamps.fromMillis(System.currentTimeMillis()))
.setField4("我是第四列").setField5(5).setField6("我是第六列").
setField7(7).setField8("我是第八列").setField9(9).setField10("我是第十列").
setField11(11).setField12("我是第十二列").setField13(13).setField14("我是第十四列").
setField15(15).setField16("我是第十六列").setField17(17).setField18("我是第十八列").build();
TestReply reply = TestReply.newBuilder().setField1(headerReply).setName(req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
}
启动服务端、客户端进行测试
经过10W次数测试,明显该方式执行效率非常高。
官方文档
grpc文档比较齐全,并且git也有比较好的样例
https://github.com/grpc/grpc-java.githttp://doc.oschina.net/grpc
proto语法指南
语法指南在该网页已经有比较好的翻译
我这里列举我个人感觉比较重要的点
分配标识号
在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]( (从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。
指定字段规则
所指定的消息字段修饰符必须是如下之一:
singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。 repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。(相当于我们JAVA 中的List)
在proto3中,repeated的标量域默认情况虾使用packed。
你可以了解更多的pakced属性在Protocol Buffer 编码
默认值
当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:
对于strings,默认是一个空string 对于bytes,默认是一个空的bytes 对于bools,默认是false 对于数值类型,默认是0 对于枚举,默认是第一个定义的枚举值,必须为0; 对于消息类型(message),域没有被设置,确切的消息是根据语言确定的,详见generated code guide
对于可重复域的默认值是空(通常情况下是对应语言中空列表)。
注:对于标量消息域,一旦消息被解析,就无法判断域释放被设置为默认值(例如,例如boolean值是否被设置为false)还是根本没有被设置。你应该在定义你的消息类型时非常注意。例如,比如你不应该定义boolean的默认值false作为任何行为的触发方式。也应该注意如果一个标量消息域被设置为标志位,这个值不应该被序列化传输。
查看generated code guide选择你的语言的默认值的工作细节。
导入定义
在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢? 你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:
import "myproject/other_protos.proto";
默认情况下你只能使用直接导入的.proto文件中的定义. 然而, 有时候你需要移动一个.proto文件到一个新的位置, 可以不直接移动.proto文件, 只需放入一个伪 .proto 文件在老的位置, 然后使用import public转向新的位置。import public 依赖性会通过任意导入包含import public声明的proto文件传递。例如:
// 这是新的proto
// All definitions are moved here
// 这是久的proto
// 这是所有客户端正在导入的包
import public "new.proto";
import "other.proto";
// 客户端proto
import "old.proto";
// 现在你可以使用新久两种包的proto定义了。
通过在编译器命令行参数中使用-I/--proto_pathprotocal 编译器会在指定目录搜索要导入的文件。如果没有给出标志,编译器会搜索编译命令被调用的目录。通常你只要指定proto_path标志为你的工程根目录就好。并且指定好导入的正确名称就好。
更新一个消息类型
如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。
- 不要更改任何已有的字段的数值标识。 如果你增加新的字段,使用旧格式的字段仍然可以被你新产生的代码所解析。你应该记住这些元素的默认值这样你的新代码就可以以适当的方式和旧代码产生的数据交互。相似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉。注意,未被识别的字段会在反序列化的过程中丢弃掉,所以如果消息再被传递给新的代码,新的字段依然是不可用的(这和proto2中的行为是不同的,在proto2中未定义的域依然会随着消息被序列化)
- 非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。
- int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。
- sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
- string和bytes是兼容的——只要bytes是有效的UTF-8编码。
- 嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
- fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
- 枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int类型的字段总会保留他们的