gRPC库介绍
gRPC是一个高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。 gRPC提供了一种简单的方法来精确地定义服务和为iOS、Android和后台支持服务自动生成可靠性很强的客户端功能库。 客户端充分利用高级流和链接功能,从而有助于节省带宽、降低的TCP链接次数、节省CPU使用、和电池寿命。
gRPC具有以下重要特征:
1. 强大的IDL特性 RPC使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议,性能出众,得到了广泛的应用。
2. 支持多种语言 支持C++、Java、Go、Python、Ruby、C#、Node.js、Android Java、Objective-C、PHP等编程语言。
3. 基于HTTP/2标准设计
gRPC安装
grpc支持1.5及以上版本。
用以下命令安装grpc-go:
go get google.golang.org/grpc
安装Protocol Buffers v3
去https://github.com/google/protobuf/releases下载最新的稳定的版本然后解压缩,把里面的文件放到’PATH’中。
安装插件
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
别忘了将
GOPATH/bin添加到
PATH中:
export PATH=
PATH:
GOPATH/bin
定义消息类型
message UserInfoResponse {
string name = 1; // 用户姓名
uint32 age = 2; // 用户年龄
uint32 sex = 3; // 用户性别
uint32 count = 4; // 账户余额
}
如上例子每个字段每个字段都有唯一的一个数字标识符,这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。
值类型
| .proto Type | C++ | Java | Python | Go |
|---|---|---|---|---|
| double | double | double | float | float64 |
| float | float | float | float | float32 |
| int32 | int32 | int | int | int |
| int64 | int64 | long | int/long[3] | int64 |
| uint32 | uint32 | int[1] | int/long[3] | uint32 |
| uint64 | uint64 | long[1] | int/long[3] | uint64 |
官方示例代码
示例代码获取地址:https://github.com/andyidea/go-example。
定义服务
使用gRPC
- 在一个后缀名为.proto的文件内定义服务。
- 用protocol buffer编辑器生成服务端和客户端代码。
- 使用gRPC的Go API实现客户端与服务端代码。
定义服务
要定义一个服务必须要在你的.proto文件中指定service,然后在你的服务中定义rpc方法,指定请求和响应类型。gRPC可定义4种类型的service方法。
1. 简单的RPC,客户端使用存根发送请求到服务器并等待响应返回,就像平常的函数调用。
rpc GetFeature(Point) returns (Feature) {}
2 . 一个服务端流式PRC,客户端发送请求到服务器,拿到一个流去读取返回的消息序列。客户端读取返回的流,知道里面没有任何消息。从例子中我们可以看出,通过在响应类型前插入stream关键字就可以指定一个服务器端的流方法。
rpc ListFeatures(Rectangle) returns (stream Feature) {}
3 . 一个 客户端流式 RPC , 客户端写入一个消息序列并将其发送到服务器,同样也是使用流。一旦客户端完成写入消息,它等待服务器完成读取返回它的响应。通过在 请求 类型前指定 stream 关键字来指定一个客户端的流方法。
rpc RecordRoute(stream Point) returns (RouteSummary) {}
4 . 一个 双向流式 RPC 是双方使用读写流去发送一个消息序列。两个流独立操作,因此客户端和服务器可以以任意喜欢的顺序读写:比如, 服务器可以在写入响应前等待接收所有的客户端消息,或者可以交替的读取和写入消息,或者其他读写的组合。 每个流中的消息顺序被预留。你可以通过在请求和响应前加 stream 关键字去制定方法的类型。
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
示例
gRPC介绍的差不多了,下面就动手开始写一个示例,示例中使用了简单的RPC以及双向流式 RPC。
定义服务
首先我们要定义自己的后缀名为.proto的文件,我的名字为friday.proto
syntax = "proto3";
package friday;
// 请求用户信息
message UserInfoRequest {
int64 uid = 1; // 用户ID
}
// 请求用户信息的结果
message UserInfoResponse {
string name = 1; // 用户姓名
uint32 age = 2; // 用户年龄
uint32 sex = 3; // 用户性别
uint32 count = 4; // 账户余额
}
service Data {
//简单Rpc
// 获取用户数据
rpc GetUserInfo(UserInfoRequest) returns (UserInfoResponse){}
// 修改用户 双向流模式
rpc ChangeUserInfo(stream UserInfoResponse) returns (stream UserInfoResponse){}
}
定义完成后生成服务端与客户端代码
protoc --go_out=plugins=grpc:. friday.proto
完成后会在当前的文件夹中生成friday.pb.go的文件,打开文件我们就可以看到该文件中定义了客户端与服务端的方法,这里就不详细的说明了,下面我们就开始动手实现。
编写服务端代码
package main
import (
"net"
"google.golang.org/grpc"
pb "rpcTest/rpcbuild/rpcbuild/friday"
"rpcTest/rpcbuild/response"
"log"
)
const (
PORT = ":10023"
)
func main() {
lis, err := net.Listen("tcp", PORT)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterDataServer(s, &response.Server{})
s.Serve(lis)
}
为了方便以后的扩展,在该文件中只是开启了服务并监听相关端口,下面是具体实现。由于是就简单的demo服务端只是接收了请求并返回并没有进行过多的操作,具体请查看代码。
/*服务端的方法*/
package response
import (
"golang.org/x/net/context"
pb "rpcTest/rpcbuild/rpcbuild/friday"
"fmt"
"io"
)
type Server struct{
routeNotes []*pb.UserInfoResponse
}
//简单模式
func (this *Server)GetUserInfo(ctx context.Context, in *pb.UserInfoRequest)(*pb.UserInfoResponse,error){
uid := in.GetUid()
fmt.Println("The uid is ",uid)
return &pb.UserInfoResponse{
Name : "Jim",
Age : 18,
Sex : 0,
Count:1000,
},nil
}
//双向流模式
func (this *Server) ChangeUserInfo(stream pb.Data_ChangeUserInfoServer)(error){
for {
in, err := stream.Recv()
if err == io.EOF {
fmt.Println("read done")
return nil
}
if err != nil {
fmt.Println("ERR",err)
return err
}
fmt.Println("userinfo ",in)
for _, note := range this.routeNotes{
if err := stream.Send(note); err != nil {
return err
}
}
}
}
编写客户端代码
客户端使用了一个开源的通用的链接池,该链接池地址如下:
https://github.com/silenceper/pool
由于gRPC使用的是HTTP/2协议协议支持多路复用,在单个连接上实现同时进行多个业务单元数据的传输。故在客户端加入该链接池。
package main
import (
"google.golang.org/grpc"
pb "rpcTest/rpcbuild/rpcbuild/friday"
"rpcTest/rpcbuild/connect"
"fmt"
"sync"
)
const (
address = "127.0.0.1:10023"
)
func main(){
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
fmt.Println("did not connect: %v", err)
}
defer conn.Close()
// 创建连接
factory := func() (interface{}, error) {
return pb.NewDataClient(conn),nil
}
// 关闭链接,此处只是定义不需要调用了因为上面有defer conn.Close(),定义的目的在于初始化链接池。
close := func(v interface{}) error { return conn.Close()}
//初始化链接池
p,err := connect.InitThread(10,30,factory)
if err != nil{
fmt.Println("init error")
return
}
var wg sync.WaitGroup
for i := 0;i < 50;i++ {
wg.Add(1)
go func(){
defer wg.Done()
//获取连接
v,_ := p.Get()
client := v.(pb.DataClient)
info := &pb.UserInfoRequest{
Uid:10012,
}
connect.GetUserInfo(client,info)
//归还链接
p.Put(v)
}()
wg.Wait()
}
for i := 0;i < 50;i++ {
wg.Add(1)
go func(){
defer wg.Done()
//获取连接
v,_ := p.Get()
client := v.(pb.DataClient)
connect.ChangeUserInfo(client)
//归还链接
p.Put(v)
}()
wg.Wait()
}
//获取链接池大小
current := p.Len()
fmt.Println("len=", current)
}
客户端代码,主要创建链接,初始化链接池,每次请求时都会先获取一个链接用完之后归还,为了方便之后的扩展同样将客户端的拆分。
package connect
import (
"github.com/silenceper/pool"
"fmt"
"time"
"net"
)
/*
初始化
min // 最小链接数
max // 最大链接数
factory func() (interface{}, error) //创建链接的方法
close func(v interface{}) error //关闭链接的方法
*/
func InitThread(min,max int,factory func() (interface{}, error),close func(v interface{}) error)(pool.Pool,error){
poolConfig := &pool.PoolConfig{
InitialCap: min,
MaxCap: max,
Factory: factory,
Close: close,
//链接最大空闲时间,超过该时间的链接 将会关闭,可避免空闲时链接EOF,自动失效的问题
IdleTimeout: 15 * time.Second,
}
p, err := pool.NewChannelPool(poolConfig)
if err != nil {
fmt.Println("Init err=", err)
return nil,err
}
return p,nil
}
以上为初始化链接池。
/*客户端方法*/
package connect
import (
"golang.org/x/net/context"
pb "rpcTest/rpcbuild/rpcbuild/friday"
"fmt"
"io"
)
//简单模式
func GetUserInfo(client pb.DataClient, info *pb.UserInfoRequest) {
req, err := client.GetUserInfo(context.Background(),info)
if err != nil {
fmt.Println("Could not create Customer: %v", err)
}
fmt.Println("userinfo is ",req.GetAge(),req.GetCount(),req.GetName(),req.GetSex())
}
//双向流模式
func ChangeUserInfo(client pb.DataClient){
notes := []*pb.UserInfoResponse{
{Name:"jim",Age:18,Sex:2,Count:100},
{Name:"Tom",Age:20,Sex:1,Count:666},
}
stream, err := client.ChangeUserInfo(context.Background())
if err != nil {
fmt.Println("%v.RouteChat(_) = _, %v", client, err)
}
waitc := make(chan struct{})
go func() {
for {
in, err := stream.Recv()
if err == io.EOF {
// read done.
fmt.Println("read done ")
close(waitc)
return
}
if err != nil {
fmt.Println("Failed to receive a note : %v", err)
}
fmt.Println("Got message %s at point(%d, %d)",in.Count,in.Sex,in.Age,in.Name)
}
}()
fmt.Println("notes",notes)
for _, note := range notes {
if err := stream.Send(note); err != nil {
fmt.Println("Failed to send a note: %v", err)
}
}
stream.CloseSend()
<-waitc
}
完成之后我们就可以编译啦go run mian.go/client.go
测试结果如下服务端:
客户端:
本文介绍了如何使用gRPC在Golang中创建双向流模式。内容包括gRPC库的特点、安装步骤、定义消息类型和值类型、官方示例代码,以及详细解释了如何定义服务和实现服务端、客户端代码。通过一个简单的RPC和双向流式RPC示例,展示了gRPC的实际应用。
2534





