Golang gRPC 入門


  • gRPC 是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計,帶來諸如雙向流、流控、頭部壓縮、單 TCP 連接上的多復用請求等特。這些特性使得其在移動設備上表現更好,更省電和節省空間占用。
  • 在 gRPC 里客戶端應用可以像調用本地對象一樣直接調用另一台不同的機器上服務端應用的方法,使得能夠更容易地創建分布式應用和服務。
  • gRPC 默認使用 protocol buffers,這是 Google 開源的一套成熟的結構數據序列化機制,它的作用與 XML、json 類似,但它是二進制格式,性能好、效率高(缺點:可讀性差)。

gRPC采用protobuf描述 接口和數據, 可以把他理解為: protobuf ON HTTP2 的一種RPC

Hello gRPC

環境安裝

從Protobuf的角度看,gRPC只不過是一個針對service接口生成代碼的生成器。因此需要提前安裝gRPC的代碼生成插件

# protoc-gen-go 
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

# 安裝protoc-gen-go-grpc插件
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.1.0

代碼生成

基於protoc-gen-go-grpc來生產gPRC代碼

protobuf 定義接口的語法:

service <service_name> {
    rpc <function_name> (<request>) returns (<response>);
}
  • service: 用於申明這是個服務的接口
  • service_name: 服務的名稱,接口名稱
  • function_name: 函數的名稱
  • request: 函數參數, 必須的
  • response: 函數返回, 必須的, 不能沒有
syntax = "proto3";

package hello;

option go_package = "github.com/ProtoDemo/grpc/service";

service HelloService {
  rpc Hello(Request) returns (Response);
}

message Request {
  string value = 1;
}

message Response {
  string value = 1;
}

編譯生產代碼,指定gRPC插件對應參數

$ protoc -I=./ --go_out=./grpc/service --go_opt=module="github.com/ProtoDemo/grpc/service" --go-grpc_out=./grpc/service --go-grpc_opt=module="github.com/ProtoDemo/grpc/service" grpc/service/service.proto


生成的代碼:

// 客戶端
// HelloServiceClient is the client API for HelloService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HelloServiceClient interface {
   Hello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
}

// 服務端
// HelloServiceServer is the server API for HelloService service.
// All implementations must embed UnimplementedHelloServiceServer
// for forward compatibility
type HelloServiceServer interface {
   Hello(context.Context, *Request) (*Response, error)
   mustEmbedUnimplementedHelloServiceServer()
}

gRPC服務端

基於服務端的HelloServiceServer接口可以重新實現HelloService服務

首先構建一個服務實體,實現gRPC定義的接口

package main

import (
	"context"
	"fmt"
	"github.com/ProtoDemo/grpc/service"
	"google.golang.org/grpc"
	"net"
)

// 通過接口約束HelloService服務
var _ service.HelloServiceServer = (*HelloService)(nil)

type HelloService struct {
	service.UnimplementedHelloServiceServer
}

func (p *HelloService) Hello(ctx context.Context, req *service.Request) (*service.Response, error) {
	resp := &service.Response{}
	resp.Value = "hello:" + req.Value
	return resp, nil
}

func main() {
	// 通過grpc.NewServer()構造一個gRPC服務對象
	gRPCServer := grpc.NewServer()
	// 然后通過gRPC插件生成的RegisterHelloServiceServer函數注冊實現的HelloServiceImpl服務
	service.RegisterHelloServiceServer(gRPCServer, new(HelloService))

	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
		fmt.Println("ListenTCP error:", err)
	}
    // 通過gRPCServer.Serve(listener)在一個監聽端口上提供gRPC服務
	gRPCServer.Serve(listener)
}

gRPC客戶端

package main

import (
   "context"
   "fmt"
   "github.com/ProtoDemo/grpc/service"
   "google.golang.org/grpc"
)

func main() {
   //grpc.Deal負責和gRPC服務建立鏈接
   conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
   if err != nil {
      fmt.Println(err)
   }
   defer conn.Close()

   //NewHelloServiceClient函數基於已經簡建立鏈接構造HelloServiceClient對象
   //返回client其實是一個helloServiceClient接口對象
   client := service.NewHelloServiceClient(conn)
   //通過結構定義的方法就可以調用微服務端對應的gRPC服務提供的方法
   req := &service.Request{Value: "hello"}
   reply, err := client.Hello(context.Background(), req)
   if err != nil {
      fmt.Println(err)
   }
   fmt.Println(reply.Value)
}

gRPC流

RPC是遠程函數調用,因此每次調用的函數參數和返回值不能太大,否則將嚴重影響每次調用的響應時間。因此傳統的RPC方法調用對於上傳和下載較大數據量場景並不適合。為此,gRPC框架針對服務器端和客戶端分別提供了流特性

服務端或客戶端的單向流是雙向流的特例,在HelloService增加一個支持雙向流的Channel方法

關鍵字stream指定啟用流特性,參數部分是接收客戶端參數的流,返回值是返回給客戶端的流。

所以 定義streaming RPC 的語法如下:

rpc <function_name> (stream <type>) returns (stream <type>) {}
syntax = "proto3";

package hello;

option go_package = "github.com/ProtoDemo/grpc/service";

service HelloService {
  rpc Hello(Request) returns (Response);

  rpc Channel (stream Request) returns (stream Response);
}

message Request {
  string value = 1;
}

message Response {
  string value = 1;
}

生成Streaming RPC

接口變化

// 客戶端的Channel方法返回一個HelloService_ChannelClient類型的返回值,可以用於服務端進行雙向通信
// HelloServiceClient is the client API for HelloService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HelloServiceClient interface {
	Hello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
	Channel(ctx context.Context, opts ...grpc.CallOption) (HelloService_ChannelClient, error)
}

// 在服務端的channel方法參數是一個新的HelloService_ChannelServer類型的參數,可以用於和客戶端雙向通信
// HelloServiceServer is the server API for HelloService service.
// All implementations must embed UnimplementedHelloServiceServer
// for forward compatibility
type HelloServiceServer interface {
	Hello(context.Context, *Request) (*Response, error)
	Channel(HelloService_ChannelServer) error
	mustEmbedUnimplementedHelloServiceServer()
}

HelloService_ChannelClient 和 HelloService_ChannelServer 接口定義:

// Request --->
// Response <---
type HelloService_ChannelClient interface {
   Send(*Request) error
   Recv() (*Response, error)
   grpc.ClientStream
}

// Request <---
// Response --->
type HelloService_ChannelServer interface {
   Send(*Response) error
   Recv() (*Request, error)
   grpc.ServerStream
}

可以發現服務端和客戶端的流輔助接口均定義了Send和Recv方法用於流數據的雙向通信

服務端

server端邏輯:

  • 接收一個Request

  • 響應一個Response

  package main
  
  import (
  	"context"
  	"fmt"
  	"github.com/ProtoDemo/grpc/service"
  	"google.golang.org/grpc"
  	"io"
  	"net"
  )
  
  // 通過接口約束HelloService服務
  var _ service.HelloServiceServer = (*HelloService)(nil)
  
  type HelloService struct {
  	service.UnimplementedHelloServiceServer
  }
  
  func (p *HelloService) Hello(ctx context.Context, req *service.Request) (*service.Response, error) {
  	resp := &service.Response{}
  	resp.Value = "hello:" + req.Value
  	return resp, nil
  }
  
  func (p *HelloService) Channel(stream service.HelloService_ChannelServer) error {
  	// 服務端在循環中接收客戶端發來的數據
  	for {
  		// 接收一個請求
  		args, err := stream.Recv()
  		if err != nil {
  			// 如果遇到io.EOF表示客戶端流被關閉
  			if err == io.EOF {
  				return nil
  			}
  			return err
  		}
  
  		// 響應一個請求
  		// 生成返回的數據通過流發送給客戶端
  		resp := &service.Response{Value: "hello:" + args.GetValue()}
  		err = stream.Send(resp)
  		if err != nil {
  			// 服務端發送異常, 函數退出, 服務端流關閉
  			return err
  		}
  	}
  }
  
  func main() {
  	// 通過grpc.NewServer()構造一個gRPC服務對象
  	gRPCServer := grpc.NewServer()
  	// 然后通過gRPC插件生成的RegisterHelloServiceServer函數注冊實現的HelloServiceImpl服務
  	service.RegisterHelloServiceServer(gRPCServer, new(HelloService))
  
  	listener, err := net.Listen("tcp", ":1234")
  	if err != nil {
  		fmt.Println("ListenTCP error:", err)
  	}
  	gRPCServer.Serve(listener)
  }
  

雙向流數據的發送和接收都是完全獨立的行為。需要注意的是,發送和接收的操作並不需要一一對應,根據真實場景進行組織代碼

客戶端

package main

import (
   "context"
   "fmt"
   "github.com/ProtoDemo/grpc/service"
   "google.golang.org/grpc"
   "io"
   "time"
)

func main() {
   //grpc.Deal負責和gRPC服務建立鏈接
   conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
   if err != nil {
      fmt.Println(err)
   }
   defer conn.Close()

   //NewHelloServiceClient函數基於已經簡建立鏈接構造HelloServiceClient對象
   //返回client其實是一個helloServiceClient接口對象
   client := service.NewHelloServiceClient(conn)
   // 客戶端需要先調用Channel方法獲取返回的流對象
   stream,err :=client.Channel(context.Background())
   if err != nil {
      fmt.Println(err)
   }
   // 在客戶端將發送和接收操作放到兩個獨立的Goroutine。
   // 向服務端發送數據
   go func() {
      for {
         if err := stream.Send(&service.Request{Value: "Hi"}); err != nil {
            fmt.Println(err)
         }
         time.Sleep(time.Second)
      }
   }()
   // 循環中接收服務端返回的數據
   for {
      reply ,err := stream.Recv()
      if err != nil {
         if err == io.EOF {
            break
         }
         fmt.Println(err)
      }
      fmt.Println(reply.GetValue())
   }
}

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM