目录
gRPC相关介绍
什么是gPRC
gPRC的优点
gPRC的缺点
gPRC定位
协议缓冲区(Protocol Buffers)
四种调用方式
gRPC开发三大步骤
第一步:定义和编写proto服务文件
第二步:proto文件转化为gRPC代码
第三步:调用gRPC代码
详细开发细节
Server端-Java
Client端-Java
Client端-Golang
步骤一:准备插件
步骤二:准备proto服务描述文件
步骤三:基于proto文件生成gRPC代码
步骤四:编写客户端代码调用gRPC
Client端-Python
步骤一:安装grpc和相关工具:
步骤二:忽略
步骤三:基于proto文件生成gRPC代码
步骤四:编写客户端代码调用gRPC
避坑指南
Java
Golang
Python
总结
gRPC相关介绍
什么是gPRC
基于http2+protocol buffer技术,简单说就是编码压缩+缓存,追求高效率。
gPRC的优点
1、高性能,毕竟是rpc技术,这个优点是我们选择gRPC的主要出发点。
2、报文可在stream上双向流传输,这个是http协议做不到的,用到双向流的场景首选gRPC。
gPRC的缺点
1、开发步骤很复杂! 这个是被gRPC劝退的主要原因,开发过程一不小心就踩坑。
2、目前支持的不充分。简单说就是太新了,像浏览器啥的支持的不好,导致开发调试没那么方便。
gPRC定位
gPRC是CNCF里唯一的传输协议项目,它的定位就是云原生时代分布式、微服务集群的核心底层通信服务。基于这个定位,我们就有必须学习它的理由。
协议缓冲区(Protocol Buffers)
这个是gRPC协议的核心模块,.proto文件、protoc工具、protobuf插件等跟它有关的名词充斥着整个gRPC开发过程。
Protocol Buffers是google推出的一种序列化的协议,它的定位就像json、xml、对象二进制流等一样,是用于对象传输过程中编码和解码的。
它比json还要精简,主要体现在编码时丢弃了一些不必要的内容,解码的时候抛弃了全扫描的方式。
四种调用方式
1、同步函数式,一来一回
rpc SayHello(HelloRequest) returns (HelloResponse);
2、 客户端流式,多来一回
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
3、服务端流式,一来多回
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
4、双向流,多来多回
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
gRPC开发三大步骤
第一步:定义和编写proto服务文件
像定义API一样,直接编写一个.proto后缀的服务描述文件,如下:
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.jingtao.library";
option java_outer_classname = "BookServiceProto";
option objc_class_prefix = "HLW";
package mybook;
// The greeting service definition.
service BookService {
// Sends a greeting
rpc check (RequestData) returns (Book) {}
}
// The request message containing the user's name.
message RequestData {
string name = 1;
}
message Book {
string name = 1;
string auther = 2;
int32 price = 3;
}
syntax定义协议版本
option部分是跟着语言耦合的部分
package定义了服务的包名
service定义了服务的函数
message定义了服务函数需要打的入参、出参等对象
第二步:proto文件转化为gRPC代码
该步骤是把上面的.proto文件通过工具或插件生成可以被程序直接调用的gRPC代码,因为要被程序调用,所以这一步是与语言紧耦合的,不同语言转换的方式各不相同。
第三步:调用gRPC代码
到了第三步就相对轻松了,第二步生成的gRPC文件已经以接口的方式将服务封装好了,我们要干的只是import它,然后调用它。
详细开发细节
Server端-Java
java语言.proto文件转化为gRPC有三种方式
方式一:独立转换
在操作系统中安装转换可视化或者命令工具,例如protoc(下载地址https://github.com/protocolbuffers/protobuf/releases),在操作系统层做好转换后将转换后的代码copy到代码项目中。
方式二:开发工具插件转化
以IDEA为例,转换的插件有GenProtoBuf、Protocol Buffer,而且都是免费的。
方式三:依赖转换
直接在项目中引入相关依赖插件,利用它们生成代码。例如java中的protobuf-java-util
我采用的是方式三,pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>BookFactory</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<protoc.version>3.12.0</protoc.version>
<grpc.version>1.56.1</grpc.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java-util -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.grpc/grpc-all -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
<version>1.56.1</version>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
<outputDirectory>${project.basedir}/src/main/java/</outputDirectory>
<clearOutputDirectory>false</clearOutputDirectory>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
双击Plugin中protobuf的compile生成服务bean,通过custom生成可直接被上层调用的gRPC。
服务端引用并实现grpc代码
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
<version>1.56.1</version>
</dependency>
服务端代码如下:
package com.jingtao.library;
import io.grpc.Grpc;
import io.grpc.InsecureServerCredentials;
import io.grpc.Server;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
public class BookServer {
private static final Logger logger = Logger.getLogger(BookServer.class.getName());
private Server server;
int port = 8088;
private void start() throws IOException {
server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
.addService(new BookImpl())
.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");
try {
BookServer.this.stop();
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
System.err.println("*** server shut down");
}
});
}
private void stop() throws InterruptedException {
if (server != null) {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
}
}
static class BookImpl extends BookServiceGrpc.BookServiceImplBase {
@Override
public void check(RequestData req, StreamObserver<Book> responseObserver) {
Book reply = Book.newBuilder().setName(req.getName()).setAuther("Shakespeare").setPrice(35).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
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 BookServer server = new BookServer();
server.start();
server.blockUntilShutdown();
}
}
Client端-Java
Java客户端前面部分跟服务端一样,只有最后一步不同:
package com.jingtao.library;
import io.grpc.*;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class BookClientor {
private static final Logger logger = Logger.getLogger(BookClientor.class.getName());
private final BookServiceGrpc.BookServiceBlockingStub blockingStub;
public BookClientor(Channel channel) {
blockingStub = BookServiceGrpc.newBlockingStub(channel);
}
public void say(String name) {
logger.info("Will try to greet " + name + " ...");
RequestData request = RequestData.newBuilder().setName(name).build();
Book response;
try{
response = blockingStub.check(request);
}catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
logger.info("Invoke say result is : " + response.getName()
+ " " + response.getAuther()
+ " " + response.getPrice());
}
public static void main(String[] args) throws Exception{
String user = "les miserables";
// Access a service running on the local machine on port 50051
String target = "localhost:8088";
ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create())
.build();
try {
BookClientor client = new BookClientor(channel);
client.say(user);
} finally {
// ManagedChannels use resources like threads and TCP connections. To prevent leaking these
// resources the channel should be shut down when it will no longer be used. If it may be used
// again leave it running.
channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
}
}
}
Client端-Golang
步骤一:准备插件
给protoc准备go相关插件和工具
git clone https://github.com/golang/protobuf.git $GOPATH/src/github.com/golang/protobuf
cd $GOPATH/src/
go install github.com/golang/protobuf/protoc-gen-go@latest
这一步的目标是准备好protoc-gen-go,搞定后一定要确保包含protoc-gen-go的bin在path中,可以被protoc用到。
步骤二:准备proto服务描述文件
注意需要显式的指定所生成gRPC代码的包名
option go_package="./;book";
步骤三:基于proto文件生成gRPC代码
准备好proto文件后,执行以下命令生成go的gRPG文件
protoc -I proto/ --go_out=plugins=grpc:proto proto/book_route.proto
步骤四:编写客户端代码调用gRPC
package book
import (
bookgrpc "book-client-go/proto"
"fmt"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func MyClient() {
conn, err := grpc.Dial(":8088", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
// handle error
panic(err)
}
defer conn.Close()
client := bookgrpc.NewBookServiceClient(conn)
req := bookgrpc.RequestData{
Name: "world",
}
reply, err := client.Check(context.Background(), &req)
if err != nil {
fmt.Println("client.bookservice error:", err)
return
}
fmt.Printf("get msg from server:[%v] \n", reply)
}
Client端-Python
步骤一:安装grpc和相关工具:
pip install grpcio
pip install grpcio-tools
pip install protobuf
步骤二:忽略
python不需要单独设置option_package,因为最终生成的gRPC代码在python中放在哪个目录下就引入哪个目录好了。
步骤三:基于proto文件生成gRPC代码
然后执行以下python命令将proto文件转换为gRPC的python代码
python -m grpc_tools.protoc -I ./ --python_out=./ --grpc_python_out=. ./book_route.proto
步骤四:编写客户端代码调用gRPC
import grpc
from book import book_route_pb2,book_route_pb2_grpc
def run():
conn = grpc.insecure_channel('127.0.0.1:8088')
client = book_route_pb2_grpc.BookServiceStub(channel=conn)
reqeust = book_route_pb2.RequestData(name='jingtao')
respnse = client.check(reqeust)
print("received name:", respnse.name)
print("received auther:", respnse.auther)
print("received:", respnse.price)
if __name__ == '__main__':
run()
避坑指南
Java
1、java.lang.NoSuchMethodError: java.nio.ByteBuffer.clear()Ljava/nio/ByteBuffer;
代码全部都编译过并切启动成功的前提下,客户端和服务端进行grpc交互时遇到这个报错
核心是jdk版本与grpc依赖包版本不一致导致的
我用的jdk1.8+protoc3.12.0+grpc1.57.0就会出这个错误,换成jdk1.8+protoc3.12.0+grpc1.56.1后问题解决。
2、UnusedPrivateParameter unused:
通过插件基于proto文件生产的gprc代码中出现了UnusedPrivateParameter编译错误,核心原因是maven导入的依赖版本与实际所需要的依赖版本不一致导致的。
错误配置:
正确配置:
Golang
1、protoc-gen-go: unable to determine Go import path for "book_route.proto"
protoc -I proto/ --go_out=plugins=grpc:proto proto/book_route.proto的时候报错:
原因是我从java项目中直接copy了proto过来,但里面缺少go需要的配置,修改proto后问题解决。
所以gRPC开发过程中proto文件不能直接复制,需要摘选出service和message部分,这是一个比较讨厌的地方。
Python
1、Cannot unpack file C:\Users\MGTV\AppData\Local\Temp\pip-unpack-iee13adu\simple
执行pip安装grpc时报错,原因是因为pip的源有问题。
开始pip安装网络太慢所以用到代理:
pip install grpcio-i http://pypi.mirrors.ustc.edu.cn/pypi/simple/ --trusted-host pypi.mirrors.ustc.edu.cn
把http://mirrors.aliyun.com/pypi/simple/替换成https://pypi.mirrors.ustc.edu.cn/simple/也解决不了,后来直接用了最原始的命令,没有走代理,问题解决
2、生成的_pd2.py缺少request和response的负载对象导致开发过程中编译不过
这个问题是grpc_tools在1.44版本以后由预定义变成了运行中自动生成对象了,不影响正常运行,这样虽然使代码更简洁了,但是可读性确实变差了。
总结
本文用java实现了gRPC服务端,然后分别用java、go、python来调用,只给了“一来一回”同步函数式调用,其实gRPC支持4中调用方式,真实情况比复杂的多。但比起对接gRPC来说,开发环境的准备更复杂,坑也很多,希望通过本篇博文能把gRPC开发所有步骤和需要注意的地方都解释清楚。