Go gRPC 系列二:一元客戶端與服務端

前言

你們好,我是煎魚,本章節將使用 Go 來編寫 gRPC Server 和 Client,讓其互相通信。在此之上會使用到以下庫:git

  • google.golang.org/grpc
  • github.com/golang/protobuf/protoc-gen-go

安裝

gRPC

go get -u google.golang.org/grpc
複製代碼

Protocol Buffers v3

wget https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-all-3.5.1.zip
unzip protobuf-all-3.5.1.zip
cd protobuf-3.5.1/
./configure
make
make install
複製代碼

檢查是否安裝成功github

protoc --version
複製代碼

若出現如下錯誤,執行 ldconfig 命名就能解決這問題golang

protoc: error while loading shared libraries: libprotobuf.so.15: cannot open shared object file: No such file or directory
複製代碼

Protoc Plugin

go get -u github.com/golang/protobuf/protoc-gen-go
複製代碼

安裝環境如有問題,可參考我先前的文章 《介紹與環境安裝》 內有詳細介紹,再也不贅述json

gRPC

本小節開始正式編寫 gRPC 相關的程序,一塊兒上車吧 😄segmentfault

圖示

image

目錄結構

$ tree go-grpc-example 
go-grpc-example
├── client
├── proto
│   └── search.proto
└── server.go
複製代碼

IDL

編寫

在 proto 文件夾下的 search.proto 文件中,寫入以下內容:bash

syntax = "proto3";

package proto;

service SearchService {
    rpc Search(SearchRequest) returns (SearchResponse) {}
}

message SearchRequest {
    string request = 1;
}

message SearchResponse {
    string response = 1;
}
複製代碼

生成

在 proto 文件夾下執行以下命令:tcp

$ protoc --go_out=plugins=grpc:. *.proto
複製代碼
  • plugins=plugin1+plugin2:指定要加載的子插件列表

咱們定義的 proto 文件是涉及了 RPC 服務的,而默認是不會生成 RPC 代碼的,所以須要給出 plugins 參數傳遞給 protoc-gen-go,告訴它,請支持 RPC(這裏指定了 gRPC)ui

  • --go_out=.:設置 Go 代碼輸出的目錄

該指令會加載 protoc-gen-go 插件達到生成 Go 代碼的目的,生成的文件以 .pb.go 爲文件後綴google

  • : (冒號)

冒號充當分隔符的做用,後跟所須要的參數集。若是這處不涉及 RPC,命令可簡化爲:spa

$ protoc --go_out=. *.proto
複製代碼

注:建議你看看兩條命令生成的 .pb.go 文件,分別有什麼區別

生成後

執行完畢命令後,將獲得一個 .pb.go 文件,文件內容以下:

type SearchRequest struct {
	Request              string   `protobuf:"bytes,1,opt,name=request" json:"request,omitempty"`
	XXX_NoUnkeyedLiteral struct{} `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

func (m *SearchRequest) Reset()         { *m = SearchRequest{} }
func (m *SearchRequest) String() string { return proto.CompactTextString(m) }
func (*SearchRequest) ProtoMessage()    {}
func (*SearchRequest) Descriptor() ([]byte, []int) {
	return fileDescriptor_search_8b45f79ee13ff6a3, []int{0}
}

func (m *SearchRequest) GetRequest() string {
	if m != nil {
		return m.Request
	}
	return ""
}
複製代碼

經過閱讀這一部分代碼,能夠知道主要涉及以下方面:

  • 字段名稱從小寫下劃線轉換爲大寫駝峯模式(字段導出)
  • 生成一組 Getters 方法,能便於處理一些空指針取值的狀況
  • ProtoMessage 方法實現 proto.Message 的接口
  • 生成 Rest 方法,便於將 Protobuf 結構體恢復爲零值
  • Repeated 轉換爲切片
type SearchRequest struct {
	Request              string   `protobuf:"bytes,1,opt,name=request" json:"request,omitempty"`
}

func (*SearchRequest) Descriptor() ([]byte, []int) {
	return fileDescriptor_search_8b45f79ee13ff6a3, []int{0}
}

type SearchResponse struct {
	Response             string   `protobuf:"bytes,1,opt,name=response" json:"response,omitempty"`
}

func (*SearchResponse) Descriptor() ([]byte, []int) {
	return fileDescriptor_search_8b45f79ee13ff6a3, []int{1}
}

...

func init() { proto.RegisterFile("search.proto", fileDescriptor_search_8b45f79ee13ff6a3) }

var fileDescriptor_search_8b45f79ee13ff6a3 = []byte{
	// 131 bytes of a gzipped FileDescriptorProto
	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x29, 0x4e, 0x4d, 0x2c,
	0x4a, 0xce, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x53, 0x4a, 0x9a, 0x5c, 0xbc,
	0xc1, 0x60, 0xe1, 0xa0, 0xd4, 0xc2, 0xd2, 0xd4, 0xe2, 0x12, 0x21, 0x09, 0x2e, 0xf6, 0x22, 0x08,
	0x53, 0x82, 0x51, 0x81, 0x51, 0x83, 0x33, 0x08, 0xc6, 0x55, 0xd2, 0xe1, 0xe2, 0x83, 0x29, 0x2d,
	0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x15, 0x92, 0xe2, 0xe2, 0x28, 0x82, 0xb2, 0xa1, 0x8a, 0xe1, 0x7c,
	0x23, 0x0f, 0x98, 0xc1, 0xc1, 0xa9, 0x45, 0x65, 0x99, 0xc9, 0xa9, 0x42, 0xe6, 0x5c, 0x6c, 0x10,
	0x01, 0x21, 0x11, 0x88, 0x13, 0xf4, 0x50, 0x2c, 0x96, 0x12, 0x45, 0x13, 0x85, 0x98, 0xa3, 0xc4,
	0x90, 0xc4, 0x06, 0x16, 0x37, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xf3, 0xba, 0x74, 0x95, 0xc0,
	0x00, 0x00, 0x00,
}
複製代碼

而這一部分代碼主要是圍繞 fileDescriptor 進行,在這裏 fileDescriptor_search_8b45f79ee13ff6a3 表示一個編譯後的 proto 文件,而每個方法都包含 Descriptor 方法,表明着這一個方法在 fileDescriptor 中具體的 Message Field

Server

這一小節將編寫 gRPC Server 的基礎模板,完成一個方法的調用。對 server.go 寫入以下內容:

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"

	pb "github.com/EDDYCJY/go-grpc-example/proto"
)

type SearchService struct{}

func (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) {
	return &pb.SearchResponse{Response: r.GetRequest() + " Server"}, nil
}

const PORT = "9001"

func main() {
	server := grpc.NewServer()
	pb.RegisterSearchServiceServer(server, &SearchService{})

	lis, err := net.Listen("tcp", ":"+PORT)
	if err != nil {
		log.Fatalf("net.Listen err: %v", err)
	}

	server.Serve(lis)
}
複製代碼
  • 建立 gRPC Server 對象,你能夠理解爲它是 Server 端的抽象對象
  • 將 SearchService(其包含須要被調用的服務端接口)註冊到 gRPC Server 的內部註冊中心。這樣能夠在接受到請求時,經過內部的服務發現,發現該服務端接口並轉接進行邏輯處理
  • 建立 Listen,監聽 TCP 端口
  • gRPC Server 開始 lis.Accept,直到 Stop 或 GracefulStop

Client

接下來編寫 gRPC Go Client 的基礎模板,打開 client/client.go 文件,寫入如下內容:

package main

import (
	"context"
	"log"

	"google.golang.org/grpc"

	pb "github.com/EDDYCJY/go-grpc-example/proto"
)

const PORT = "9001"

func main() {
	conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("grpc.Dial err: %v", err)
	}
	defer conn.Close()

	client := pb.NewSearchServiceClient(conn)
	resp, err := client.Search(context.Background(), &pb.SearchRequest{
		Request: "gRPC",
	})
	if err != nil {
		log.Fatalf("client.Search err: %v", err)
	}

	log.Printf("resp: %s", resp.GetResponse())
}
複製代碼
  • 建立與給定目標(服務端)的鏈接交互
  • 建立 SearchService 的客戶端對象
  • 發送 RPC 請求,等待同步響應,獲得回調後返回響應結果
  • 輸出響應結果

驗證

啓動 Server

$ pwd
$GOPATH/github.com/EDDYCJY/go-grpc-example
$ go run server.go
複製代碼

啓動 Client

$ pwd             
$GOPATH/github.com/EDDYCJY/go-grpc-example/client
$ go run client.go 
2018/09/23 11:06:23 resp: gRPC Server
複製代碼

總結

在本章節,咱們對 Protobuf、gRPC Client/Server 分別都進行了介紹。但願你結合文中講述內容再寫一個 Demo 進行深刻了解,確定會更棒 🤔

若是有任何疑問或錯誤,歡迎在 issues 進行提問或給予修正意見,若是喜歡或對你有所幫助,歡迎 Star,對做者是一種鼓勵和推動。

個人公衆號

image

參考

本系列示例代碼

相關文章
相關標籤/搜索