1. gRPC簡述
gRPC 是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計。目前提供 C、Java 和 Go 語言版本,分別是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.
gRPC 基於 HTTP/2 標准設計,帶來諸如雙向流、流控、頭部壓縮、單 TCP 連接上的多復用請求等特。這些特性使得其在移動設備上表現更好,更省電和節省空間占用。
gRPC默認使用protocol buffers作為交換數據序列化的機制,即gRPC底層依賴protobuf。
gRPC官方文檔:https://grpc.io/docs/languages/go/quickstart/
服務非對象,消息非引用,促進微服務的系統間粗粒度消息交互設計理念。
2. gRPC-go安裝
1)安裝protobuf編譯器protoc,可直接從https://github.com/protocolbuffers/protobuf/releases安裝穩定版本。
2)安裝golang插件protoc-gen-go。
$ go get -u github.com/golang/protobuf/protoc-gen-go
3)下載grpc-go倉庫
grpc-go倉庫原地址為:google.golang.org/grpc,由於原地址屏蔽,需要從github拉下來后拷貝到$GOPATH/src/google.golang.org/grpc。
git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
4)配置goproxy,在/etc/profile最后加上如下代碼后source。
export GO111MODULE=on export GOPROXY="https://goproxy.cn"
5)直接編譯例程
$ go get google.golang.org/grpc/examples/helloworld/greeter_client
$ go get google.golang.org/grpc/examples/helloworld/greeter_server
編譯成功后可執行文件位於$GOPATH/bin下。
3. gRPC-go應用
1 定義服務
一個 RPC 服務通過參數和返回類型來指定可以遠程調用的方法。gRPC通過protobuf來實現。
使用 protocol buffers 接口定義語言來定義服務方法,用 protocol buffer 來定義參數和返回類型。客戶端和服務端均使用服務定義生成的接口代碼。
syntax = "proto3"; option go_package = "io.grpc.examples"; package helloworld; // The greeter service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; }
2. 生成gRPC代碼
使用protoc來生成創建應用所需的特定客戶端和服務端的代碼,文件*.pb.go。生成的代碼同時包括客戶端的存根和服務端要實現的抽象接口,均包含Greeter所定義的方法。
進入helloworld.proto所在目錄:
protoc -I . helloworld.proto --go_out=plugins=grpc:.
注意:生成兼容gRPC代碼。
若protoc文件指定了RPC services,protoc-gen-go可以生成兼容gRPC的代碼,需要指定go_out的plugins參數,常用方法如下:
If a proto file specifies RPC services, protoc-gen-go can be instructed to generate code compatible with gRPC (http://www.grpc.io/). To do this, pass the plugins
parameter to protoc-gen-go; the usual way is to insert it into the --go_out argument to protoc:
protoc --go_out=plugins=grpc:. *.proto
若不指定plugins=grpc參數,則生成的*.pb.go僅包含protobuf相關的代碼,不包含grpc相關代碼。
注意:插件參數列表
插件參數列表用逗號(,)分割,插件參數與輸出路徑用冒號(:)分隔。
To pass extra parameters to the plugin, use a comma-separated parameter list separated from the output directory by a colon:
protoc --go_out=plugins=grpc,import_path=mypackage:. *.proto
- paths=(import | source_relative) - specifies how the paths of generated files are structured. See the "Packages and imports paths" section above. The default is import.
- plugins=plugin1+plugin2 - specifies the list of sub-plugins to load. The only plugin in this repo is grpc.
- Mfoo/bar.proto=quux/shme - declares that foo/bar.proto is associated with Go package quux/shme. This is subject to the import_prefix parameter.
The following parameters are deprecated and should not be used:
- import_prefix=xxx - a prefix that is added onto the beginning of all imports.
- import_path=foo/bar - used as the package if no input files declare go_package. If it contains slashes, everything up to the rightmost slash is ignored.
注:新版本的protoc會報錯:
protoc-gen-go: unable to determine Go import path for "helloworld.proto" Please specify either: • a "go_package" option in the .proto source file, or • a "M" argument on the command line.
可在proto文件中定義go_package為當前包:
option go_package=".";
protoc --go_out=plugins=grpc:./proto proto/helloworld.proto
如此在proto目錄下生成helloworld.pb.go文件,並且該目錄下存放helloworld.proto。
// enhance protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ helloworld/helloworld.proto
3. server端
服務器有一個server結構,通過實現SayHello()方法,實現了從proto服務定義生成的GreeterServer接口。
// server is used to implement helloworld.GreeterServer. type server struct{} // SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { return &pb.HelloReply{Message: "Hello " + in.Name}, nil }
需要提供一個gRPC服務的另一個主要功能是讓這個服務在網絡上可用。
const ( port = ":50051" ) ... func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{}) s.Serve(lis) }
詳細代碼如下:
package main import ( "context" "log" "net" "google.golang.org/grpc" pb "google.golang.org/grpc/examples/helloworld/helloworld" ) const ( port = ":50051" ) // server is used to implement helloworld.GreeterServer. type server struct { pb.UnimplementedGreeterServer } // SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
4. client端
連接服務:如何連接 Greeter
服務器,需要創建一個 gRPC 頻道,指定我們要連接的主機名和服務器端口。然后我們用這個頻道創建存根實例。
const ( address = "localhost:50051" defaultName = "world" ) func main() { // Set up a connection to the server. conn, err := grpc.Dial(address) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewGreeterClient(conn) ... }
詳細代碼如下:
// Package main implements a client for Greeter service. package main import ( "context" "log" "os" "time" "google.golang.org/grpc" pb "google.golang.org/grpc/examples/helloworld/helloworld" ) const ( address = "localhost:50051" defaultName = "world" ) func main() { // Set up a connection to the server. conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewGreeterClient(conn) // Contact the server and print out its response. name := defaultName if len(os.Args) > 1 { name = os.Args[1] } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.GetMessage()) }
GRPC非加密(22數據幀)
4. gRPC認證
Grpc單向認證示例google.golang.org/grpc/examples/features/encryption
Grpc雙向認證示例openness的interfaceservice
https://github.com/open-ness/edgenode/tree/master/pkg/interfaceservice
https://github.com/open-ness/edgecontroller/tree/master/cmd/interfaceservicecli
4.1 grpc相關函數
https://godoc.org/google.golang.org/grpc/credentials
google.golang.org/grpc/credentials
func NewClientTLSFromCert(cp *x509.CertPool, serverNameOverride string) TransportCredentials
func NewClientTLSFromFile(certFile, serverNameOverride string) (TransportCredentials, error)
func NewServerTLSFromCert(cert *tls.Certificate) TransportCredentials
func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error)
func NewTLS(c *tls.Config) TransportCredentials
google.golang.org/grpc
func Dial(target string, opts ...DialOption) (*ClientConn, error)
func (cc *ClientConn) Close() error
func WithTransportCredentials(creds credentials.TransportCredentials) DialOption
func NewServer(opt ...ServerOption) *Server
func Creds(c credentials.TransportCredentials) ServerOption
4.2 單向認證
服務端
導入證書,創建server時包含證書。
package main import ( "context" "log" "net" "flag" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials" pb "example/helloworld" ) var port = flag.Int("port", 8088, "the port to serve on") // server is used to implement helloworld.GreeterServer. type server struct { pb.UnimplementedGreeterServer } // SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func main() { flag.Parse() lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("failed to listen: %v", err) } creds, err := credentials.NewServerTLSFromFile("cert/server.crt", "cert/server.key") if err != nil { log.Fatalf("failed to create credentials: %v", err) } s := grpc.NewServer(grpc.Creds(creds)) pb.RegisterGreeterServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
客戶端
grpc.Dial()時包含CA證書撥號。
package main import ( "context" "log" "time" "flag" "google.golang.org/grpc" "google.golang.org/grpc/credentials" pb "example/helloworld" ) var addr = flag.String("addr", "server:8088", "the address to connect to") var name = flag.String("name", "world", "the greeter's name") func main() { flag.Parse() creds, err := credentials.NewClientTLSFromFile("cert/ca.crt", "") if err != nil { log.Fatalf("failed to load credentials: %v", err) } // Set up a connection to the server. conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(creds), grpc.WithBlock()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewGreeterClient(conn) // Contact the server and print out its response. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.GetMessage()) }
GRPC單向認證(26數據幀)
4.3 雙向認證
服務端
需要定制tls.Config導入CA、client證書,生成credentials.TransportCredentials傳遞給grpc.Creds()后生成ServerOption,傳入grpc.NewServer()。
package main import ( "context" "log" "net" "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "path/filepath" "github.com/pkg/errors" "google.golang.org/grpc" "google.golang.org/grpc/credentials" pb "example/helloworld" ) const ( port = ":8088" certsDir = "cert/" ) // server is used to implement helloworld.GreeterServer. type server struct { pb.UnimplementedGreeterServer } // SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func getTransportCredentials() (*credentials.TransportCredentials, error) { crtPath := filepath.Clean(filepath.Join(certsDir, "server.crt")) keyPath := filepath.Clean(filepath.Join(certsDir, "server.key")) caPath := filepath.Clean(filepath.Join(certsDir, "ca.crt")) cert, err := tls.LoadX509KeyPair(crtPath, keyPath) if err != nil { return nil, fmt.Errorf("Failed load server key pair: %v", err) } certPool := x509.NewCertPool() ca, err := ioutil.ReadFile(caPath) if err != nil { return nil, fmt.Errorf("Failed appends CA certs from %s: %s", caPath, err) } if ok := certPool.AppendCertsFromPEM(ca); !ok { return nil, errors.Errorf("Failed append CA certs from %s", caPath) } creds := credentials.NewTLS(&tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, Certificates: []tls.Certificate{cert}, ClientCAs: certPool, }) fmt.Println("crtPath: "+crtPath + ", key: "+ keyPath + ", ca: " + caPath) return &creds, nil } func main() { tc, err := getTransportCredentials() if err != nil { log.Fatalf("fialed to read certificates: %s", err) } lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } defer lis.Close() s := grpc.NewServer(grpc.Creds(*tc)) pb.RegisterGreeterServer(s, &server{}) fmt.Println("listen ", port) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
客戶端
同樣需要定制tls.Config,導入CA、客戶端證書生成credentials.TransportCredentials,傳入grpc.WithTransportCredentials()生成DialOption,傳入grpc.Dial()。
package main import ( "context" "log" "os" "time" "path/filepath" "crypto/tls" "crypto/x509" "io/ioutil" "fmt" "github.com/pkg/errors" "google.golang.org/grpc" "google.golang.org/grpc/credentials" pb "example/helloworld" ) const ( // address = "https://server:8088" address = "server:8088" defaultName = "world" certsDir = "cert/" ) func getTransportCredentials() (*credentials.TransportCredentials, error) { crtPath := filepath.Clean(filepath.Join(certsDir, "client.crt")) keyPath := filepath.Clean(filepath.Join(certsDir, "client.key")) caPath := filepath.Clean(filepath.Join(certsDir, "ca.crt")) cert, err := tls.LoadX509KeyPair(crtPath, keyPath) if err != nil { return nil, err } certPool := x509.NewCertPool() ca, err := ioutil.ReadFile(caPath) if err != nil { return nil, err } if ok := certPool.AppendCertsFromPEM(ca); !ok { return nil, errors.Errorf("Failed append CA certs from %s", caPath) } creds := credentials.NewTLS(&tls.Config{ // ServerName: "server", // ClientAuth: tls.RequireAndVerifyClientCert, Certificates: []tls.Certificate{cert}, RootCAs: certPool, }) fmt.Println("crtPath: "+crtPath + ", key: "+ keyPath + ", ca: " + caPath) return &creds, nil } func createConnection(ctx context.Context, address string) *grpc.ClientConn { tc, err := getTransportCredentials() if err != nil { fmt.Println("Error when creating transport credentials: " + err.Error()) os.Exit(1) } conn, err := grpc.DialContext(ctx, address, grpc.WithTransportCredentials(*tc), grpc.WithBlock()) if err != nil { fmt.Println("Error when dialing: " + address + " err:" + err.Error()) os.Exit(1) } return conn } func main() { name := defaultName if len(os.Args) > 1 { name = os.Args[1] } // Set up a connection to the server. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() fmt.Println("conn...") conn := createConnection(ctx, address) defer conn.Close() c := pb.NewGreeterClient(conn) fmt.Println("call...") // Contact the server and print out its response. r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.GetMessage()) }
GRPC雙向認證(25數據幀)
5. 流式RPC
gRPC 允許你定義四類服務方法:單項RPC、服務端流式RPC、客戶端流式RPC和雙向流式RPC。上面介紹的RPC都是單項RPC,下面重點介紹流式RPC。
服務端流式 RPC,即客戶端發送一個請求給服務端,可獲取一個數據流用來讀取一系列消息。客戶端從返回的數據流里一直讀取直到沒有更多消息為止。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){ }
客戶端流式 RPC,即客戶端用提供的一個數據流寫入並發送一系列消息給服務端。一旦客戶端完成消息寫入,就等待服務端讀取這些消息並返回應答。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) { }
雙向流式 RPC,即兩邊都可以分別通過一個讀寫數據流來發送一系列消息。這兩個數據流操作是相互獨立的,所以客戶端和服務端能按其希望的任意順序讀寫,例如:服務端可以在寫應答前等待所有的客戶端消息,或者它可以先讀一個消息再寫一個消息,或者是讀寫相結合的其他方式。每個數據流里消息的順序會被保持。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){ }
注意關鍵字 stream,聲明其為一個流方法。
// client.go package main import ( "context" "io" "log" "google.golang.org/grpc" pb "helloworld.grpc/proto" ) const ( PORT = "9002" ) func main() { conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure()) if err != nil { log.Fatalf("grpc.Dial err: %v", err) } defer conn.Close() client := pb.NewStreamServiceClient(conn) err = printLists(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: "gRPC Stream Client: List", Value: 2018}}) if err != nil { log.Fatalf("printLists.err: %v", err) } err = printRecord(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: "gRPC Stream Client: Record", Value: 2018}}) if err != nil { log.Fatalf("printRecord.err: %v", err) } err = printRoute(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: "gRPC Stream Client: Route", Value: 2018}}) if err != nil { log.Fatalf("printRoute.err: %v", err) } } func printLists(client pb.StreamServiceClient, r *pb.StreamRequest) error { stream, err := client.List(context.Background(), r) if err != nil { return err } for { resp, err := stream.Recv() if err == io.EOF { break } if err != nil { return err } log.Printf("resp: pj.name: %s, pt.value: %d", resp.Pt.Name, resp.Pt.Value) } return nil } func printRecord(client pb.StreamServiceClient, r *pb.StreamRequest) error { stream, err := client.Record(context.Background()) if err != nil { return err } for n := 0; n < 6; n++ { err := stream.Send(r) if err != nil { return err } } resp, err := stream.CloseAndRecv() if err != nil { return err } log.Printf("resp: pt.name: %s, pt.Value: %d", resp.Pt.Name, resp.Pt.Value) return nil } func printRoute(client pb.StreamServiceClient, r *pb.StreamRequest) error { stream, err := client.Route(context.Background()) if err != nil { return err } for n := 0; n <= 6; n++ { err = stream.Send(r) if err != nil { return err } resp, err := stream.Recv() if err == io.EOF { break } if err != nil { return err } log.Printf("resp: pj.name: %s, pt.value: %d", resp.Pt.Name, resp.Pt.Value) } stream.CloseSend() return nil }
// server.go package main import ( "context" "io" "log" "google.golang.org/grpc" pb "helloworld.grpc/proto" ) const ( PORT = "9002" ) func main() { conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure()) if err != nil { log.Fatalf("grpc.Dial err: %v", err) } defer conn.Close() client := pb.NewStreamServiceClient(conn) err = printLists(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: "gRPC Stream Client: List", Value: 2018}}) if err != nil { log.Fatalf("printLists.err: %v", err) } err = printRecord(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: "gRPC Stream Client: Record", Value: 2018}}) if err != nil { log.Fatalf("printRecord.err: %v", err) } err = printRoute(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: "gRPC Stream Client: Route", Value: 2018}}) if err != nil { log.Fatalf("printRoute.err: %v", err) } } func printLists(client pb.StreamServiceClient, r *pb.StreamRequest) error { stream, err := client.List(context.Background(), r) if err != nil { return err } for { resp, err := stream.Recv() if err == io.EOF { break } if err != nil { return err } log.Printf("resp: pj.name: %s, pt.value: %d", resp.Pt.Name, resp.Pt.Value) } return nil } func printRecord(client pb.StreamServiceClient, r *pb.StreamRequest) error { stream, err := client.Record(context.Background()) if err != nil { return err } for n := 0; n < 6; n++ { err := stream.Send(r) if err != nil { return err } } resp, err := stream.CloseAndRecv() if err != nil { return err } log.Printf("resp: pt.name: %s, pt.Value: %d", resp.Pt.Name, resp.Pt.Value) return nil } func printRoute(client pb.StreamServiceClient, r *pb.StreamRequest) error { stream, err := client.Route(context.Background()) if err != nil { return err } for n := 0; n <= 6; n++ { err = stream.Send(r) if err != nil { return err } resp, err := stream.Recv() if err == io.EOF { break } if err != nil { return err } log.Printf("resp: pj.name: %s, pt.value: %d", resp.Pt.Name, resp.Pt.Value) } stream.CloseSend() return nil }
6. 攔截器interceptor
在每個 RPC 方法的前或后做某些事情,需要使用攔截器(interceptor)。grpc中有兩類攔截器:
普通方法:一元攔截器(grpc.UnaryInterceptor)
流方法:流攔截器(grpc.StreamInterceptor)
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
通過查看源碼可得知,要完成一個攔截器需要實現 UnaryServerInterceptor
方法。
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error
采用開源項目 go-grpc-middleware 可以實現多個攔截器:
import "github.com/grpc-ecosystem/go-grpc-middleware" myServer := grpc.NewServer( grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( ... )), grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( ... )), )
7. grpc傳輸json
參考:使用 JSON 協議的 gRPC https://github.com/johanbrandhorst/grpc-json-example
gRPC 是基於 Google Protocol Buffers payload 格式的,默認格式是 Protobuf,但是 gRPC-Go 的實現中也對外暴露了 Codec
interface ,它支持任意的 payload 編碼。我們可以使用任何一種格式,包括你自己定義的二進制格式、flatbuffers、或者使用我們今天要討論的 JSON ,作為請求和響應。
type Codec interface { // Marshal returns the wire format of v. Marshal(v interface{}) ([]byte, error) // Unmarshal parses the wire format into v. Unmarshal(data []byte, v interface{}) error // Name returns the name of the Codec implementation. The returned string // will be used as part of content type in transmission. The result must be // static; the result cannot change between calls. Name() string } func RegisterCodec(codec Codec)
Codec defines the interface gRPC uses to encode and decode messages. Note that implementations of this interface must be thread safe; a Codec's methods can be called from concurrent goroutines.
The Codec will be stored and looked up by result of its Name() method, which should match the content-subtype of the encoding handled by the Codec. This is case-insensitive, and is stored and looked up as lowercase. If the result of calling Name() is an empty string, RegisterCodec will panic. See Content-Type on https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests for more details.
server端修改
import _ "github.com/johanbrandhorst/grpc-json-example/codec"
client端修改
使用 gRPC 客戶端,你只需要使用合適的內容子類型作為 grpc.DialOption
來初始化:
import "github.com/johanbrandhorst/grpc-json-example/codec" func main() { conn := grpc.Dial("localhost:1000", grpc.WithDefaultCallOptions(grpc.CallContentSubtype(codec.JSON{}.Name())), ) }
參考:
1. gRPC 官方文檔中文版 http://doc.oschina.net/grpc?t=56831
2. gRPC in 3 minutes (Go) https://github.com/grpc/grpc-go/tree/master/examples
3. https://github.com/golang/protobuf
4. gRPC應用C++
6. https://github.com/open-ness/edgenode/blob/master/pkg/interfaceservice/interface_service.go
7. https://github.com/open-ness/edgecontroller/tree/master/cmd/interfaceservicecli
8. Grpc+Grpc Gateway實踐一 介紹與環境安裝
9. 使用 JSON 協議的 gRPC go語言中文網