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}
別忘了將
export PATH=
定義消息類型
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
測試結果如下服務端:
客戶端: