写在前面
首先声明这篇文章只是介绍gRPC在Go语言中的使用入门级的文章,不包含多少深入的内容,读者对象是gRPC的初学者,如果你是一位gRPC和Go的资深开发者,请绕行,没必要浪费时间!
正文
没用过RPC都不好意思说自己从事过互联网开发,今天就来讲讲gRPC在Go中的应用。
所谓RPC(remote procedure call远程过程调用),实际上是提供了一套框架机制,使得位于网络中的不同机器上的应用程序之间可以进行通信相互调用,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。通常RPC都是通过反射机制来实现的,本文不做深入分析,待后续文章再深入分析RPC的实现原理。
与其他的RPC框架类似,gRPC在服务端提供一个gRPC Server,客户端的库是gRPC Stub。典型的场景是客户端发送请求,调用服务端的接口,客户端和服务端之间的通信协议是基于HTTP2的,支持双工的流式保序消息,性能比较好,同时也很轻量级。
既然是server/client模型,那么我们直接用restful api不是也可以的吗,为什么还需要RPC(或者gRPC)呢?下面我们就来看看gRPC相对于Restful API到底有哪些优势?gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议。不过gRPC还是有些特有的优势的,如下:
- gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件;
- 另外,通过protobuf可以将数据序列化为二进制编码,这会减少需要传输的数据量,从而提高性能;
- gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式);
接下来我们看看gRPC官网上的描述
A high performance, open-source universal RPC framework.
1. Simple service definition
2. Works across languages and platforms
3. Start quickly and scale
4. Bi-directional streaming and integrated auth
简单易学,快速开始,能够支持多种语言和平台,双向流式通讯、集成认证模块。有谁在使用gRPC呢?还是有不少知名公司在用的。
Who’s using it? Square, NETFLIX, Core OS, carbon3D, CISCO, JUNIPER
更详细的描述和使用场景:
gRPC is a modern open source high performance RPC framework that can run
in any environment. It can efficiently connect services in and across data
centers with pluggable support for load balancing, tracing, health checking
and authentication. It is also applicable in last mile of distributed computing
to connect devices, mobile applications and browsers to backend services.
The main usage scenarios:
1. Efficiently connecting polyglot services in microservices style architecture
2. Connecting mobile devices, browser clients to backend services
3. Generating efficient client libraries
4. Core Features that make it awesome:
a. Idiomatic client libraries in 10 languages
b. Highly efficient on wire and with a simple service definition framework
c. Bi-directional streaming with http/2 based transport
d. Pluggable auth, tracing, load balancing and health checking
支持的开发语言和平台:
Language | Platform | Compiler |
---|---|---|
C/C++ | Linux/Mac | GCC 4.8+ Clang 3.3+ |
C/C++ | Windows 7+ | Visual Studio 2015+ |
C# | Linux/Mac | .NET Core, Mono 4+ |
C# | Windows 7+ | .NET Core, .NET 4.5+ |
Dart | Windows/Linux/Mac | Dart 2.0+ |
Go | Windows/Linux/Mac | Go 1.6+ |
Java | Windows/Linux/Mac | JDK 8 recommended. Gingerbread+ for Android |
Node.js | Windows/Linux/Mac | Node v4+ |
Objective-C | Mac OS X 10.11+/iOS 7.0+ | Xcode 7.2+ |
PHP (Beta) | Linux/Mac | PHP 5.5+ and PHP 7.0+ |
Python | Windows/Linux/Mac | Python 2.7 and Python 3.4+ |
Ruby | Windows/Linux/Mac | Ruby 2.3+ |
主流的开发语言和平台基本都支持了。
官网上写的还是不够详细,我们自己来总结一下吧!首先聊聊使用使用场景:
- 需要对接口进行严格约束的情况,我们不希望客户端给我们传递任意的数据,尤其是考虑到安全性的因素,我们通常需要对接口进行更加严格的约束。这时gRPC就可以通过protobuf来提供严格的接口约束;
- 对于性能有更高的要求时。有时我们的服务需要传递大量的数据,而又希望不影响我们的性能,这个时候也可以考虑gRPC服务,因为通过protobuf我们可以将数据压缩编码转化为二进制格式,通常传递的数据量要小得多,而且通过http2我们可以实现异步的请求,从而大大提高了通信效率;
- 但是,通常我们不会去单独使用gRPC,而是将gRPC作为一个部件进行使用,这是因为在生产环境,我们面对大并发的情况下,需要使用分布式系统来去处理,而gRPC并没有提供分布式系统相关的一些必要组件。而且,真正的线上服务还需要提供包括负载均衡,限流熔断,监控报警,服务注册和发现等必要的组件;
接下来还得简单介绍一下Protobuf,因为gRPC使用protobuf来定义接口。Protobuf是什么?Protobuf实际是一套类似于Json或者XML的数据传输格式和规范,用于不同应用或进程之间进行通信时使用。通信时所传递的信息是通过Protobuf定义的message数据结构进行打包,然后编译成二进制的码流再进行传输或者存储。
Protobuf有如下优点:
- 足够简单;
- 序列化后体积很小,消息大小只需要XML的1/10 ~ 1/3;
- 解析速度快,解析速度比XML快20 ~ 100倍;
- 多语言支持;
- 更好的兼容性,Protobuf设计的一个原则就是要能够很好的支持向下或向上兼容;
使用Protobuf有如下几个步骤:
- 定义消息;
- 初始化消息以及存储传输消息;
- 读取消息并解析;
Protobuf的消息结构是通过一种叫做Protocol Buffer Language的语言进行定义和描述的,实际上Protocol Buffer Language分为两个版本,版本2和版本3,默认不声明的情况下使用的是版本2,目前推荐使用的是版本3。
采用ProtoBuf作为IDL(Interface Definition Language接口定义语言),需要定义service和message,生成客户端和服务端代码。用户自己实现服务端代码中的调用接口,并且利用客户端代码来发起请求到服务端。service代表RPC接口,message代表数据结构(里面可以包括不同类型的成员变量,包括字符串、数字、数组、字典等)。message中成员变量后面的数字代表进行二进制编码时候的提示信息,1~15表示热变量,会用较少的字节来编码。默认所有变量都是可选的(optional),repeated则表示数组。service rpc接口只能接受单个message 参数,返回单个message。
到此,我们已经把gRPC相关的基础知识介绍完毕,接下来进入实践环节,以Go语言为例。
在Go中使用gRPC之前,要先做一些基础性的准备工作。gRPC需要运行在Go的1.6版本以上的环境中。先查看一下你的Go版本:
$ go version
安装gRPC组件
$ go get -u google.golang.org/grpc
安装Protocol Buffer v3及编译器,最简单的方法就是直接下载已经编译好的二进制文件包,下载地址为:https://github.com/google/protobuf/releases。
- 下载完成后,解压文件;
- 更新环境变量,把protoc程序所在的路径加入到环境变量PATH之中;
安装protoc的插件protoc-gen-go
$ go get -u github.com/golang/protobuf/protoc-gen-go
protoc-gen-go将被安装到GOPATH/bin。必须把$GOPATH/bin路径放到PATH环境变量中。
$ export PATH=$PATH:$GOPATH/bin
gRPC服务和数据结构定义在.proto文件中的,使用protoc来将其编译为Go的代码(.pb.go)。
protoc --go_out=plugins=grpc:./ xxx.proto
基础工作完成就可以实践了,gRPC官方网站有个简单的入门例子,我这里不再重复,有兴趣可以参考https://www.grpc.io/docs/quickstart/go/。
gRPC官网的例子作为入门教程是可以的,但是生产环境下这么简单是不能满足要求的。下面讲述一下生产环境中是如何使用gRPC的,分布式环境,使用consul作为注册中心来注册和发现服务。为了保证数据的安全性,数据需加密传输,使用非对称加密算法,如何生成公钥密钥对以及CA证书不在本文的讨论范围之内,请参考其他相关文档。
简单说一下Consul,Consul是一个服务网格(微服务间的 TCP/IP,负责服务之间的网络调用、限流、熔断和监控)解决方案,它是一个一个分布式的,高度可用的系统,而且开发使用都很简便。它提供了一个功能齐全的控制平面,主要特点是:服务发现、健康检查、键值存储、安全服务通信、多数据中心。Consul 的主要功能有服务发现、健康检查、KV存储、安全服务沟通和多数据中心。我们这里使用它的服务注册和服务发现的功能。
闲言少叙,直接上代码来讲解,直观明了!在项目当中,写了一个叫做优惠券(coupon)的模块来提供相应的功能。
Server端代码
先来定义gRPC的接口和数据结构(coupon.proto):
syntax = "proto3";
package coupon;
service coupon {
rpc GetUserCouponByUserAndOrder (GetUserCouponByUserAndOrderRequest) returns (GetUserCouponByUserAndOrderResponse);
}
message GetUserCouponByUserAndOrderRequest {
string userId = 1;
string orderId = 2;
}
message GetUserCouponByUserAndOrderResponse {
int32 userCouponId = 1;
}
使用protoc编译protobuf文件coupon.proto来生成coupon.pb.go代码。
cd <coupon.proto所在目录>
protoc --go_out=plugins=grpc:./ coupon.proto
coupon.pb.go(部分代码)
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: coupon.proto
package coupon
import (
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
// ...... 其他省略
server.go中的相关代码(gRPC服务初始化,使用非对称加密算法实现数据的安全传输,并且把gRPC服务注册到Consul中):
package server
import (
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/reflection"
"io/ioutil"
"net"
"time"
"xxxxx/coupon/grpc/coupon"
"git.xxxx.net/xxxx/go-common/logger"
// 其他省略
)
func ServerInit() error {
logger.Info("gRPC server init")
// 读取并解析公钥私钥对
cert, err := tls.LoadX509KeyPair("server.pem", "server.key")
if err != nil {
logger.Fatalf("tls.LoadX509KeyPair err: %v", err)
}
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("ca.pem")
if err != nil {
logger.Fatalf("ioutil.ReadFile err: %v", err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
log.Fatalf("error occurred when certPool.AppendCertsFromPEM")
}
c := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
})
// 日志
grpclog.SetLoggerV2(logger.GetGrpcLogger())
s := grpc.NewServer(
grpc.Creds(c),
)
coupon.RegisterCouponServer(s, &couponService.Service{})
reflection.Register(s)
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", 9503))
if err != nil {
logger.Fatal("fail to listen on port 9503")
}
go func() {
logger.Info("gRPC server running in goroutine")
// register into consul
err = consul.Register("coupon", "127.0.0.1", 9503, "127.0.0.1:8500", time.Second * 10, 15)
if err != nil {
panic(err)
}
if err := s.Serve(listener); err != nil {
logger.Panic("gRPC server init failed,", err)
}
}()
return err
}
coupon_grpc.go中实现远程服务。
package coupon
import (
"context"
"xxxxxx/coupon/grpc/coupon"
"xxxxxx/coupon/models/admin"
)
type Service struct{}
func (s *Service) GetUserCouponByUserAndOrder(ctx context.Context, req *coupon.GetUserCouponByUserAndOrderRequest) (res *coupon.GetUserCouponByUserAndOrderResponse, err error) {
re := coupon.GetUserCouponByUserAndOrderResponse{}
userCoupon, err := admin.GetUserCouponByUserAndOrder(req.OrderId, req.UserId)
if err != nil {
return res, err
}
re.UserCouponId = int32(userCoupon.Id)
return &re, err
}
main.go中的相关代码:
func init() {
// init gRPC server
err := server.ServerInit()
if err != nil {
logger.Fatal("init gRPC server failed", err)
}
}
插一段来讲讲Go语言中的init函数。go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。
- init函数是用于程序执行前做包的初始化的函数;
- 每个包可以拥有多个init函数;
- 包的每个源文件也可以拥有多个init函数;
- 同一个包中多个init函数的执行顺序go语言没有明确的定义;
- 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序;
- init函数不能被其他函数调用,而是在main函数执行之前,自动被调用。
Client端代码
首先是coupon.proto和coupon.pb.go文件,可以直接从server端copy,也可以重新生成。
coupon.proto,与server端一致。
syntax = "proto3";
package coupon;
service coupon {
rpc GetUserCouponByUserAndOrder (GetUserCouponByUserAndOrderRequest) returns (GetUserCouponByUserAndOrderResponse);
}
message GetUserCouponByUserAndOrderRequest {
string userId = 1;
string orderId = 2;
}
message GetUserCouponByUserAndOrderResponse {
int32 userCouponId = 1;
}
coupon.pb.go(部分代码)
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: coupon.proto
package coupon
import (
context "context"
fmt "fmt"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
math "math"
)
// ...... 其他省略
client.go代码,初始化gRPC client,使用非对称加密算法实现数据的安全传输(与server端一致),并且连接注册中心。
package client
import (
"crypto/tls"
"crypto/x509"
"errors"
"xxxxxx/order/grpc/coupon"
"xxxxxx/order/logger"
"io/ioutil"
"google.golang.org/grpc"
"google.golang.org/grpc/balancer/roundrobin"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"
)
var couponServerConn *grpc.ClientConn
var couponClient coupon.CouponClient
func GetNewCouponClient() (coupon.CouponClient, error) {
if !(couponServerConn.GetState() == connectivity.Idle || couponServerConn.GetState() == connectivity.Ready) {
return nil, errors.New("the couponServerConn state is not idle or ready: " + couponServerConn.GetState().String())
}
newClient := coupon.NewCouponClient(couponServerConn)
return newClient, nil
}
func CouponClientInit() error {
var err error
// 读取并解析公钥私钥对
cert, err := tls.LoadX509KeyPair("client.pem", "client.key")
if err != nil {
logger.Fatalf("Load tls.LoadX509KeyPair error: %v", err)
}
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("ca.pem")
if err != nil {
logger.Fatalf("ioutil.ReadFile error: %v", err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
logger.Fatalf("certPool.AppendCertsFromPEM error")
}
c := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: "coupon",
RootCAs: certPool,
})
// set log
grpclog.SetLoggerV2(logger.GetGrpcLogger())
target := "consul://127.0.0.1:8500/coupon?healthy=true&wait=5s"
// 连接到注册中心
couponServerConn, err = grpc.Dial(
target,
grpc.WithBalancerName(roundrobin.Name),
grpc.WithTransportCredentials(c))
if err != nil {
logger.Fatalf("coupon grpc failed to connect to the given target: %v", err)
}
couponClient = coupon.NewCouponClient(couponServerConn)
return err
}
main.go中的相关代码:
import (
grpcClient "xxxxxx/order/grpc/client"
)
func init() {
// ...... 其他代码省略
// init coupon gRPC client
err := grpcClient.CouponClientInit()
if err != nil {
logger.Fatal("init coupon gRPC client failed", err)
}
// ...... 其他代码省略
}
客户端调用远程gRPC服务:
package coupon
import (
"context"
"xxxxxx/order/grpc/client"
"xxxxxx/order/grpc/coupon"
"time"
)
func GetUserCouponByUserAndOrder(userId string, orderId string) (userCouponId int, err error) {
couponClient, err := client.GetNewCouponClient()
if err != nil {
return userCouponId, err
}
ctx := context.Background()
res, err := couponClient.GetUserCouponByUserAndOrder(ctx, &coupon.GetUserCouponByUserAndOrderRequest{
UserId: userId,
OrderId: orderId,
})
if err != nil {
return userCouponId, err
}
return int(res.UserCouponId), nil
}
2019年12月1日星期天 于北京通州家中