TIL:gRPC-2-Unary

繼上一篇的TIL:gRPC 1. 初介紹,我們開始來製作第一個類型的gRPC:Unary,如下圖看到的,他是一個如過去HTTP 1.1模式的溝通方式,Client發送一個請求,然後Server端再把結果回覆給Client:

關於gRPC的開發,不管是哪一種類型,Server與Client的溝通介面都是從proto buffer文件的定義開始,所以Client只要拿到.proto文件,然後根據自己熟悉的開發語言找到套件進行Compile,就可以開始開發Client端的程式碼了。

所以,我們先從定義.proto文件開始,下面示範Client傳遞一個名稱,然後Server端回傳該名字對應的ID,下圖為這個範例的資料夾結構。

Step 1. 定義.proto文件
撰寫.proto文件時,可以分為幾個區塊

  1. 第一個是文件格式定義,其中

    • syntax: proto buffer版本,因為目前最新為第三版,故可以直接給予proto3
    • package: 表這份文件所屬的package,這邊我們用與檔名相同
    • go_package: 表在用Golang語言開發時引入的package名稱,我們用pb作為名稱來與其他package區分。
      1
      2
      3
      4
      5
      syntax = "proto3";

      package lookup;

      option go_package = "pb";
  2. 接下來,就開始撰寫邏輯的部分,我們可以分為兩個大部分

    • message: 定義溝通的參數介面,這邊要注意到
    • service: 定義溝通的function介面,這邊這邊要注意到
      • 可以透過service名稱來區隔不同的service type,例如:OrderService、CheckOutService與Shipservice就可以分開來定義
      • 每一個service內的function name需以rpc開頭,並且將input與output介面定義上去,之後會用到串流方式傳輸資料,就可以在參數前加上stream關鍵字
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    message LookingUp {
    string first_name = 1;
    string last_name = 2;
    }

    message LookUpRequest {
    LookingUp lookingUp = 1;
    }

    message LookUpResponse {
    int32 result = 1;
    }

    service LookUpService{
    //unary
    rpc LookUp(LookUpRequest) returns (LookUpResponse) {};
    }

Step 2. 將.proto文件做Compile

  1. 安裝protobuf的工具,並且記得將該路徑加入到$GOPATH/bin

    1
    go get -u github.com/golang/protobuf/protoc-gen-go
  2. 然後切換到專案的資料夾下後,輸入下面的command,如果沒有發生錯誤,就可以在/pb中發現多了一個檔案,其副檔名.pb.go

    1
    protoc Lookupservice/Unary/pb/lookup.proto --go_out=plugins=grpc:.

Step 3. 開發Server端程式

  1. 首先,先建立一個模擬的data table,讓client呼叫時可以達到查詢的效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    type MyDataTable struct {
    ID int32
    Name string
    }

    var (
    recordTable []MyDataTable
    )

    func InitData() {

    recordTable = make([]MyDataTable, 0)

    recordTable = append(recordTable, MyDataTable{
    ID: 13579,
    Name: "WeiZhe",
    })
    recordTable = append(recordTable, MyDataTable{
    ID: 24680,
    Name: "Ray",
    })
    }

  2. 我們先來定義一個structure,他會包含與Client溝通的function

  • 我們先到剛剛產生的lookup.pb.go檔案找尋到下面這段,這代表LookUpServiceServer這個interface中有一個function等待我們去implement,我們把LookUp(context.Context, *LookUpRequest) (*LookUpResponse, error)複製起來

    1
    2
    3
    4
    type LookUpServiceServer interface {
    //unary
    LookUp(context.Context, *LookUpRequest) (*LookUpResponse, error)
    }
  • 回到server.go,先宣告一個structure

  • 然後,將剛剛從lookup.pb.go複製的function附屬於剛剛宣告的structure,來讓我們來implement他
    • 一開始貼上會出現錯誤,這是因為function中的名稱是附屬於lookup.pb.go下,故記得補上pb.即可。
  • implement的邏輯即是for loop查詢recordTable 這個slice,然後比對如果有與傳進來的req.LookingUp.FirstName相同,則return查到的ID,否則回傳error message說明找不到。
    • 傳進來的參數可以透過*pb.LookUpRequest來查詢
    • 回傳的*pb.LookUpResponse是一個structure,所以我們需要將結果組裝起來成相同類型:記得回傳的需要是一個位址類型。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      type serviceSvr struct{}

      func (s *serviceSvr) LookUp(ctx context.Context, req *pb.LookUpRequest) (response *pb.LookUpResponse, err error) {
      var (
      firstName string
      lookUpResp *pb.LookUpResponse
      content MyDataTable
      result int32
      )

      log.Printf("LookUp function invoked by %v", req)
      log.Println(recordTable)

      firstName = req.LookingUp.FirstName
      result = -1
      for _, content = range recordTable {
      log.Println(content.Name, "/", firstName)
      if content.Name == firstName {
      result = content.ID
      break
      }
      }
      if result == -1 {
      return nil, errors.New("failed to find ID, please check first name again")
      }
      lookUpResp = &pb.LookUpResponse{
      Result: result,
      }
      return lookUpResp, nil
      }

  1. 定義完service中function的邏輯後,我們來初始化一個gRPC的Server
  • 先建立一個listener,並且用tcp監聽著port: 50051
    1
    2
    3
    4
    5
    if listener, err = net.Listen("tcp", ":50051"); err != nil {
    log.Fatalf("failed to create a listener: %v", err)
    return
    }

  • 首先引入gRPC package: google.golang.org/grpc,接著呼叫NewServer就能成功初始一個gRPC Server
  • 接著,引入/pb資料夾後,就可以參考到剛剛透過protoc產生的RegisterLookUpServiceServer function,在這邊把我們剛剛初始成功的gRPC:grpcSvr與上一個步驟產生的serviceSvr這個綁有LookUp function的structure: &serviceSvr{}綁上去。
  • 最後,我們將gRPC server開始launch起來,就開始正式服務了。
    1
    2
    3
    4
    5
    6
    7
    8
    grpcSvr = grpc.NewServer()
    pb.RegisterLookUpServiceServer(grpcSvr, &serviceSvr{})

    if err = grpcSvr.Serve(listener); err != nil {
    log.Fatalf("failed to serve gRPC: %v", err)
    return
    }

Step 4. 開發Client端程式
接著,我們可以開始來開發client的程式,相較於server端,client端的開發較為簡單

  1. 首先,一樣引入google.golang.org/grpc,然後使用Dial function來與server進行溝通
  • 第一個參數填入server端的位址
  • 第二個參數填入Dial function的選項,通常都是grpc.With開頭,這邊因為我們只是基本功能實現,所以就不用到SSL驗證,故填入grpc.WithInsecure():當然如果你有多個功i能要打開,你可以看到後面是一個interface,只要把要選項附加上去即可。
  • 記得養成好習慣,一但打開connection,就要加上defer來將這個connection做close。
  1. 然後,引入github.com/weizhe0422/gRPC/LookupService/Unary/pb,就可以參考到NewLookUpServiceClient function,並且把我們剛剛初始的connection傳進去,並得到一個gRPC Client的變數。

    1
    2
    3
    4
    5
    if conn, err = grpc.Dial(":50051", grpc.WithInsecure()); err != nil {
    log.Fatalf("failed to coneect to gRPC server: %v", err)
    }
    defer conn.Close()
    client = pb.NewLookUpServiceClient(conn)
  2. client端的最後一步就是開始呼叫server端的function並得到回應

  • 我們先把剛剛gRPC Client的變數傳到一個function來實作
  • 其中function實作中
    • 把request的資料組成pb.LookUpRequest類型
    • 使用Client變數中protoBuf定義的function name,並傳入context與剛剛組成的pb.LookUpRequest類型資料
    • 接著就可以拿到該function回傳的response結果
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      doUnary(client, firstName, lastName)

      func doUnary(client pb.LookUpServiceClient, firstName, lastName string){
      var(
      lookUpReq *pb.LookUpRequest
      lookUpResp *pb.LookUpResponse
      err error
      )

      lookUpReq = &pb.LookUpRequest{
      LookingUp: &pb.LookingUp{
      FirstName:firstName,
      LastName:lastName,
      },
      }

      if lookUpResp, err = client.LookUp(context.Background(),lookUpReq); err!=nil{
      log.Fatalf("failed to send LookUp: %v", err)
      }

      fmt.Println("LookUp result: ", lookUpResp.Result)
      }

Step 5. 測試連接

  • 切換到server端與client端的資料夾,並且各自將其run起來,就可以看到Client端成功從server查詢到first_name於data table中的編號。

至此已經完成第一個gRPC類型的開發,後面三個類型都是這個類型的些微變化就可以實現成功,所以如果能把這些步驟做到熟悉,後面的範例就能夠更快上手囉! 我們下個章節見啦!

0%