gRPC 簡單使用

簡介

RPC 的全稱是 Remote Procedure Call(遠程過程調用), 便可以在客戶端應用程序中直接調用其餘計算機(服務端)上定義的方法.git

gRPC 是一個 RPC 框架, 使用 protobuf 做爲數據交換協議.github

定義服務

既然是 RPC 系統, 主要的目的在於定義方法, 或者說服務 service.golang

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}
複製代碼

在 gRPC 中能夠定義四種類型的服務:shell

  • 一元 RPC: 客戶端向服務器發送單個請求並獲取單個響應, 相似普通函數調用
  • 服務器流式 RPC: 客戶端發送單個請求, 服務端返回流式響應
  • 客戶端流式 RPC: 客戶端發送流式請求, 服務端返回單個響應
  • 雙向流式 RPC: 使用兩個獨立的流, 客戶端發送流式請求, 服務端返回流式響應
rpc SayHello(HelloRequest) returns (HelloResponse){ } rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){ } rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) { } rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){ } 複製代碼

Golang 下使用

定義 protobuf 文件 hello.proto 的內容爲:bash

syntax = "proto3";

import "google/protobuf/any.proto";

package hello;
option go_package = "hello";

message HelloReq {
  string name = 1;
}

message HelloResp {
  int32 code = 1;
  string greet = 2;
  google.protobuf.Any details = 3;
}

service HelloService {
  rpc Greet(HelloReq) returns (HelloResp) {};
  rpc GreetWithServerStream(HelloReq) returns (stream HelloResp) {};
  rpc GreetWithClientStream(stream HelloReq) returns (HelloResp) {};
  rpc GreetWithBidirectionalStream(stream HelloReq) returns (stream HelloResp) {};
}
複製代碼

初始化項目, 安裝必要的依賴:服務器

go mod init tzh.com/app
go get -u github.com/golang/protobuf/protoc-gen-go
go get -u google.golang.org/grpc
mkdir hello
# 假設 protoc3 已經解壓好了
.\protoc3\bin\protoc.exe --proto_path=. hello.proto --go_out=plugins=grpc:./hello
複製代碼

生成代碼的時候, 注意指定插件 plugins=grpc.app

main.go 以下:框架

package main

import (
	"context"
	"flag"
	"fmt"
	"io"
	"log"
	"net"
	"strings"

	"google.golang.org/grpc"
	pb "tzh.com/app/hello"
)

const port = ":5000"

type server struct {
	pb.UnimplementedHelloServiceServer
}

func (s *server) Greet(ctx context.Context, in *pb.HelloReq) (*pb.HelloResp, error) {
	return &pb.HelloResp{Code: 0, Greet: "hello " + in.GetName()}, nil
}

// 對於空格分隔的 name, 使用 stream 發送屢次數據
func (s *server) GreetWithServerStream(in *pb.HelloReq, stream pb.HelloService_GreetWithServerStreamServer) error {
	names := strings.Split(in.GetName(), " ")
	for i, name := range names {
		err := stream.Send(&pb.HelloResp{
			Code:  int32(i),
			Greet: fmt.Sprintf("part %d: hello %s", i, name),
		})
		if err != nil {
			return err
		}
	}
	return nil
}

// 對於客戶端發送的多個 name, 合併後發送單條響應
func (s *server) GreetWithClientStream(stream pb.HelloService_GreetWithClientStreamServer) error {
	names := make([]string, 0)
	for {
		msg, err := stream.Recv()
		if err != nil {
			break
		}
		names = append(names, msg.GetName())
	}
	stream.SendAndClose(&pb.HelloResp{
		Code:  0,
		Greet: fmt.Sprintf("hello %s count: %d", strings.Join(names, " "), len(names)),
	})
	return nil
}

// 雙向流, 對於每一個請求, 一一響應
func (s *server) GreetWithBidirectionalStream(stream pb.HelloService_GreetWithBidirectionalStreamServer) error {
	for {
		msg, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		if err := stream.Send(&pb.HelloResp{
			Code:  0,
			Greet: "hello " + msg.GetName(),
		}); err != nil {
			return err
		}
	}
}

func runServer() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("failed to listen on %s: %v", port, err)
	}
  s := grpc.NewServer()
  // 註冊服務
	pb.RegisterHelloServiceServer(s, &server{})
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to server: %v", err)
	}
}

func run1(client pb.HelloServiceClient) {
	log.Println("############ Greet ")
	r, err := client.Greet(context.Background(), &pb.HelloReq{
		Name: "tt",
	})
	if err != nil {
		log.Fatalf("failed to get greet resp: %v", err)
	}
	log.Printf("get code : %d, get greet: %s \n", r.GetCode(), r.GetGreet())
}

func run2(client pb.HelloServiceClient) {
	log.Println("############ GreetWithServerStream")
	serverStream, err := client.GreetWithServerStream(context.Background(), &pb.HelloReq{
		Name: "tt aa xx ff",
	})
	if err != nil {
		log.Fatalf("failed with GreetWithServerStream: %v", err)
	}

	for {
		r, err := serverStream.Recv()
		if err != nil {
			break
		}
		log.Printf("get code : %d, get greet: %s \n", r.GetCode(), r.GetGreet())
	}
}

func run3(client pb.HelloServiceClient) {
	log.Println("############ GreetWithClientStream ")
	clientStream, err := client.GreetWithClientStream(context.Background())
	if err != nil {
		log.Fatalf("failed with GreetWithClientStream: %v", err)
	}

	for _, name := range []string{"tt", "qq", "aa", "yy"} {
		err := clientStream.Send(&pb.HelloReq{
			Name: name,
		})
		if err != nil {
			log.Fatalf("failed with GreetWithClientStream during send request: %v", err)
			break
		}
	}
	r, err := clientStream.CloseAndRecv()
	if err != nil {
		log.Fatalf("failed with GreetWithClientStream when get response: %v", err)
	}
	log.Printf("get code : %d, get greet: %s \n", r.GetCode(), r.GetGreet())
}

func run4(client pb.HelloServiceClient) {
	log.Println("############ GreetWithBidirectionalStream ")
	stream, err := client.GreetWithBidirectionalStream(context.Background())
	if err != nil {
		log.Fatalf("failed with GreetWithBidirectionalStream: %v", err)
	}

	for _, name := range []string{"tt", "qq", "aa", "yy"} {
		err := stream.Send(&pb.HelloReq{
			Name: name,
		})
		if err != nil {
			log.Fatalf("failed with GreetWithClientStream during send request: %v", err)
			break
		}
	}
	stream.CloseSend()

	for {
		r, err := stream.Recv()
		if err != nil {
			break
		}
		log.Printf("get code : %d, get greet: %s \n", r.GetCode(), r.GetGreet())
	}
}

func runClient() {
	address := "localhost" + port
	conn, err := grpc.Dial(address, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("failed to connect %s: %v", address, err)
	}
	defer conn.Close()
	client := pb.NewHelloServiceClient(conn)

	run1(client)
	run2(client)
	run3(client)
	run4(client)
}

func main() {
	isClient := flag.Bool("client", false, "run client")
	flag.Parse()
	if *isClient {
		runClient()
	} else {
		runServer()
	}
}
複製代碼

代碼有點長, 由於將服務端代碼和客戶端代碼都混合在同一個文件中, 使用下面的命令分別啓動服務端和運行客戶端.tcp

# 運行服務端
go run main.go
# 運行客戶端
go run main.go --client
複製代碼

參考

相關文章
相關標籤/搜索