Table of Contents
gRPC简介
gRPC是一个高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。 gRPC提供了一种简单的方法来精确地定义服务和为iOS、Android和后台支持服务自动生成可靠性很强的客户端功能库。 客户端充分利用高级流和链接功能,从而有助于节省带宽、降低TCP连接次数、节省CPU使用和电池寿命。
RPC(Remote Procedure Call,远程过程调用)是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络细节的应用程序通信协议。RPC协议构建于TCP或UDP,或者是HTTP上。允许开发者直接调用另一台服务器上的程序,而开发者无需另外的为这个调用过程编写网络通信相关代码,使得开发网络分布式程序在内的应用程序更加容易
RPC采用客户端-服务器端的工作模式,请求程序就是一个客户端,而服务提供程序就是一个服务器端。当执行一个远程过程调用时,客户端程序首先先发送一个带有参数的调用信息到服务端,然后等待服务端响应。在服务端,服务进程保持睡眠状态直到客户端的调用信息到达。当一个调用信息到达时,服务端获得进程参数,计算出结果,并向客户端发送应答信息。然后等待下一个调用。
gRPC特性
- 服务定义简单
- 跨语言和跨平台
- 启动快,伸缩性好
- 支持服务端和客户端双向流
RPC实践
在认识gRPC之前,我们先来看看一个简单的RPC服务端和客户端具体是怎样的。 下面以Go语言的net/rpc包为例实现一个提供乘法和除法运算服务的RPC服务端和RPC客户端。
服务端上的RPC服务可简单理解为一组可被客户端调用的方法,这些方法的形式必须如下:
func (t *T) MethodName(argType T1, replyType *T2) error
- 该方法所属的类型T必须是导出的(即类型名首字母大写)
- 方法是导出的(即方法名首字母大写)
- 方法带有两个参数,T1和T2(T2必须是指针类型,用于写入该方法的返回结果),且T1和T2都是导出类型或内建类型
- 方法返回类型为error
服务端实现:
package main
import (
"net"
"net/http"
"net/rpc"
"log"
"errors"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
l, e := net.Listen("tcp", ":2345")
if e != nil {
log.Fatal("listen error:", e)
}
log.Println("rpcserver listening at 127.0.0.1:2345")
http.Serve(l, nil)
}
上面的代码主要有两部分:
服务的定义和实现:
Arith将作为服务名,该服务包含两个方法可被远程调用,Multiply和Divide。这两个函数都遵循rpc包要求的形式,通过结构体来传送1个或多个函数的参数,然后将函数调用的结果写入用于存放响应内容的结构体中。
服务的监听和处理:
- rpc.Register方法注册Arith的一个实例,使得Arith服务对外可调用
- HandleHTTP方法用于注册一个HTTP handler来处理RPC消息
- net.Listen设置使用的协议及端口,并返回一个Listener对象l用于监听新建立的连接
- http.Serve启动一个http server处理Listener对象l接收到的连接
客户端实现:
package main
import (
"fmt"
"net/rpc"
"log"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
var serverAddress string = "127.0.0.1"
func main() {
client, err := rpc.DialHTTP("tcp", serverAddress + ":2345")
if err != nil {
log.Fatal("dialing:", err)
}
// Synchronous call
args := &Args{
9,4}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d * %d=%d\n", args.A, args.B, reply)
var quotient Quotient
err = client.Call("Arith.Divide", args, "ient)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d / %d = %d remains %d\n", args.A, args.B, quotient.Quo, quotient.Rem)
}
客户端的代码主要有两部分:
定义和服务端Arith服务相同或相似的数据结构:
客户端定义了一样的结构体Args和Quotient,用于调用服务端Arith服务的Multiply方法和Divide方法。事实上,客户端Args和Quotient的定义也不一定要完全一致,但不能有冲突。
例如,以下定义也是可以的
type Args struct {
A, B, C int
}
type Args struct {
A, B *int
}
但以下定义都是不可以的
type Args struct {
A int; B float
}
type Args struct {
C, D int
}
连接服务端及服务调用:
- 通过rpc.DialHTTP与服务端建立连接并获取Client对象
- 通过client.Call调用Arith服务的方法,Call方法第一个参数的形式为“服务名.方法名”,服务名和方法名都必须和服务端的定义一致
RPC客户端只要遵循服务端的服务定义的格式就可以像调用本地方法一样调用远程的服务。
代码运行:
上述代码已放在github仓库,可直接下载运行:
$ mkdir -p $GOPATH/src/github.com/linshk
$ cd $GOPATH/src/github.com/linshk
$ git clone https://github.com/linshk/grpc-learning
$ cd grpc-learning
# 以后台方式运行服务端
$ go run rpcserver/main.go &
$ go run rpcclient/main.go
# 客户端输出:
# Arith: 9 * 4=36
# Arith: 9 / 4 = 2 remains 1
gRPC helloworld
进入examples目录查看示例项目
$ cd $GOPATH/src/google.golang.org/grpc/examples/helloworld
helloword目录结构:
- helloworld
- helloworld
- helloworld.pb.go(protoc-gen-go根据helloworld.proto的服务定义自动生成的服务端客户端代码)
- helloworld.proto (Greeter服务(gRPC服务)的定义)
- greeter_client
- main.go(借助helloworld.pb.go构建的gRPC客户端)
- greeter_server
- main.go(借助helloworld.pb.go构建的gRPC服务端)
- mock_helloworld
- hw_mock.go(MockGen自动生成的mock服务器代码)
- hw_mock_test.go
- helloworld
首先查看helloworld.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
前面我们手动构建RPC服务端代码时需要先定义服务的类型(Arith),函数参数及计算结果的类型(Args为参数的类型,Int为Multiply的结果类型,Quotient为Divide方法的结果类型),服务可供调用的方法(Multiply和Quotient),而Greeter服务的定义也是如此:
- 服务的定义:
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
服务名为Greeter,服务可供调用的方法为SayHello,且SayHello方法的形式与前面的Multiply和Divide有所不同,SayHello返回的返回值即是方法的调用结果。
- 参数类型及结果类型的定义:
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
接着再看由该服务定义自动生成的代码helloworld.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: helloworld.proto
package helloworld
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _