vlambda博客
学习文章列表

gRPC学习以及实践

       相信大家都听过RPC、HTTP、Socket等协议,他们均可用于业务中来进行数据通信,又根据各自协议的特点,应用场景也比较多样、复杂,那大家是否听过或者了解gRPC呢?用来做什么呢?我们就来了解一下gRPC以及其用途。

介绍

用官方网站1一句话介绍介绍gRPC

A high-performance, open source universal RPC framework.

即:高性能、开源的通用型RPC框架

说起RPC,人们常会和HTTP做对比,两者在底层数据传输时本质基本一致,即全部基于TCP实现安全可靠的连接进行数据通信,但在应用层又有些不同。 

RPC,即Remote Procedure Call(远程过程调用),主要在TCP协议之上进行工作;

HTTP,即HyperText Transfer Protocol(超文本传输协议),主要在HTTP协议之上进行工作。 

从协议上来说,RPC更加高效一些。

gRPC结构图: 

      gRPC基本基于定义服务的思想,指定远程调用的方法,包含方法的入参以及返回数据类型。服务端继承、实现接口并开启监听服务等待客户端请求;客户端保存一份副本,提供与服务端相同的方法。客户端、服务端的语言没有特别限制,只要支持gRPC协议基本可实现客户端、服务端的连接、数据通信。

目前gRPC支持的语言大致有:Golang、Python、Java、PHP、C&C++等。

      gRPC的创建基于Protobuf,进行数据定义、服务接口定义等等,所以在深入了解gRPC前,最好对于protobuf有一定的了解。protobuf有proto2、proto3版本,现在大都基于proto3进行开发,所以大家了解proto32

基于gRPC实现restful接口,主要使用gRPC的一个插件,使得服务端通过一套代码即可对外提供HTTP服务、RPC服务,其架构如下图: 

gRPC学习以及实践

gRPC使用

gRPC的使用通常有如下几步:

  1. 编写Protobuf,定义RPC的接口以及入参、出参,数据类型等

  2. 基于Protobuf编译成项目语言的文件,如go、java等

  3. 实现服务端功能模块,主要实现gRPC的接口

  4. 实现客户端功能

gRPC示例

Demo文件结构

.
├── example
│ ├── service.pb.go // 编译后的rpc文件
│ ├── service.pb.gw.go // 编译后的gateway文件
│ └── service.proto // protobuf文件
├── gw.go // gRPC的gateway服务端
├── gw_client.go // gRPC客户端
└── gw_server.go // gRPC服务端
1. 通过protobuf定义数据结构、类型
syntax = "proto3";

package example;

import "google/api/annotations.proto";

message StringMessage {
    string value = 1;
}

// 定义EchoServer
service EchoService {
// 定义Echo接口,以及参数、返回数据
rpc Echo(StringMessage) returns (StringMessage) {
option (google.api.http) = {
post: "/v1/example/echo" // 定义http服务的请求方法(post)、路由
body: "*"
};
}
}
2. 编译protobuf文件为指定语言

demo主要为go语言,故而将protobuf编译为go语言版本,使用的命令为以下两条:

  • 编译为RPC数据结构、类型、服务


protoc -I/usr/local/include -I. -I$GOPATH/src -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --go_out=plugins=grpc:. service.proto

运行以上命令后,会生成:service.pb.go 文件,server端基于此编写server端服务功能,客户端基于此编写客户端调用逻辑。

  • 编译为Gateway的数据结构、服务转发等


protoc -I/usr/local/include -I. -I$GOPATH/src -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --grpc-gateway_out=logtostderr=true:. service.proto

运行以上命令,会生成:service.pb.gw.go 文件,server端基于此文件,启动HTTP服务,并将HTTP的请求转发到对应的RPC服务中。

  • 编译后的pb.go文件主要内容介绍


...
// 接口参数数据结构、类型
type StringMessage struct {
Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}

...
// 获取gRPC客户端实例
func NewEchoServiceClient(cc *grpc.ClientConn) EchoServiceClient {
return &echoServiceClient{cc}
}
...

// EchoServiceServer is the server API for EchoService service.
type EchoServiceServer interface {
Echo(context.Context, *StringMessage) (*StringMessage, error)
}

...
// 注册服务端server功能
func RegisterEchoServiceServer(s *grpc.Server, srv EchoServiceServer) {
s.RegisterService(&_EchoService_serviceDesc, srv)
}
3. 服务端&客户端功能实现

        不管是server的功能,还是client的功能,全部基于 service.pb.go 进行开发实现,也就是说,生成一份pb.go文件,可以提供到服务端和客户端使用,从而能严格的保持客户端、服务端的数据结构类型、方法等的一致,客户端也无需太过关心接口的入参、出参数据以及类型,直接使用pb.go文件即可。

a. server代码示例

package main

import (
pb "./example"
"encoding/json"
"fmt"
"golang.org/x/net/context"
"google.golang.org/grpc"
"log"
"net"
)

const (
port = ":9090"
)

// 实现pb.go中的EchoServiceServer接口
type server struct{}

// 接口的入参、出参结构已经确定,按需实现即可
func (s *server) Echo(ctx context.Context, in *pb.StringMessage) (*pb.StringMessage, error) {
by, _ := json.Marshal(in)
fmt.Println("Receive From Client:", string(by))
return &pb.StringMessage{Value: "Hello " + in.Value}, nil
}
/**********/

func main() {
// 启动server监听
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 向gRPC服务中注册已经实现的服务
s := grpc.NewServer()
pb.RegisterEchoServiceServer(s, &server{})
s.Serve(lis)
}
b. client代码示例
package main

import (
pb "./example"
"golang.org/x/net/context"
"google.golang.org/grpc"
"log"
"os"
)

const (
address = "localhost:9090"
defaultName = "world"
)

func main() {
// 连接到gRPC的服务端
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()

// 基于连接获得gRPC的client实例
c := pb.NewEchoServiceClient(conn)

name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
// 在client实例上调用服务端方法,只需要遵循pb.go中的数据结构进行传参即可
r, err := c.Echo(context.Background(), &pb.StringMessage{Value: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Value)
}
c. gateway代码示例
package main

import (
gw "./example"
"flag"
"github.com/golang/glog"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"golang.org/x/net/context"
"google.golang.org/grpc"
"net/http"
)

var (
echoEndpoint = flag.String("echo_endpoint", "localhost:9090", "endpoint of YourService")
)

func run() error {
// 定义上下文
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

// 获取mux实例
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
// 注册http服务的转发的endpoint
err := gw.RegisterEchoServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint, opts)
if err != nil {
return err
}

// 启动HTTP服务
return http.ListenAndServe(":8080", mux)
}

func main() {
flag.Parse()
defer glog.Flush()

if err := run(); err != nil {
glog.Fatal(err)
}
}

因为HTTP服务需要连接到gRPC的服务中才能对外提供服务,故而在启动项目前需要优先启动gRPC服务端,再启动gateway服务。

项目运行示例
  1. 启动gRPC服务端


    go run gw_server.go
  2. 启动gateway服务


    go run gw.go
  3. 发起RPC请求 运行客户端


    go run gw_client.go

    客户端的运行结果:

     gRPC学习以及实践

    服务端运行结果:

    gRPC学习以及实践

  4. 发起HTTP请求

    使用postman工具发起http的post请求,结果: 


    gRPC服务端结果:

      到此整个gRPC的使用以及示例就基本介绍完毕了,在当今微服务比较流行的情况下,gRPC对于微服务中的使用还是有着比较重要的作用,各个服务之间不必关心对方服务的语言、数据结构、类型等,各个服务间基于proto文件进行数据转换、通信,以及语言特点选择http请求或者rpc请求。

      gRPC也可以结合其他Go框架进行封装合一,如echo、gin框架等,充分利用Go语言框架的优势,对外提供更好的服务。

参考资料:
  1. [gRPC](https://www.grpc.io/)

  2. [Protobuf](https://developers.google.cn/protocol-buffers/docs/proto3)

  3. [gRPC-gateway](https://github.com/grpc-ecosystem/grpc-gateway)