gRPC应用golang

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框架中,PB主要有三个作用:1)可以用来定义(消息)数据结构;2)可以用来定义服务接口;3)可以通过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应用

使用gRPC需要四个步骤:1)定义gRPC服务;2)生成客户端和服务器代码;3)实现gRPC服务;4)实现gRPC客户端。

RPC调用具有如下两个特点:1)调用方便,RPC 屏蔽了底层的网络通信细节,使得调用 RPC 就像调用本地方法一样方便;2)不需要打包和解包:RPC 调用的入参和返回的结果都是 Go 的结构体,不需要对传入参数进行打包操作,也不需要对返回参数进行解包操作,简化了调用步骤。

1 定义服务

一个 RPC 服务通过参数和返回类型来指定可以远程调用的方法。gRPC通过protobuf来实现。

使用 protocol buffers 接口定义语言来定义服务方法,用 protocol buffer 来定义参数和返回类型。客户端和服务端均使用服务定义生成的接口代码。

注:不同版本protoc和protoc-gen-go对proto语法要求不同,编译出来的pb文件和目录也不相同。推荐使用最新版。目前测试最新版本protoc(v3.19.4)和protoc-gen-go(v1.27.1)。

syntax = "proto3";

//go:generate protoc -I. --experimental_allow_proto3_optional --go_out=plugins=grpc:. //option go_package
= "io.grpc.examples";
option go_package = "."; package helloworld;
// The greeter service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc PrintHello (NilRequest) 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; }
message NilRequest{
}

option关键字用来对.proto文件进行一些设置,其中go_package是必须的,而且go_package的值必须是包导入的路径。protoc(v3.19.4)时指定go_package=".";可以在当前目录生成pb文件,否则会以包目录形式生成层级pb文件。

package指定生成的.pb.go文件所在的包名。

service关键字定义服务,并指定该服务拥有的RPC方法(rpc),定义方法的请求和返回的结构体类型。

NilRequest是空参数,虽为空参数,但是必须定义message(message内容为空)。

2. 生成gRPC代码

使用protoc来生成创建应用所需的特定客户端和服务端的代码,文件*.pb.go。生成的代码同时包括客户端的存根和服务端要实现的抽象接口,均包含Greeter所定义的方法。

进入helloworld.proto所在目录:

protoc -I. --experimental_allow_proto3_optional --go_out=plugins=grpc:. helloworld.proto

注意:生成兼容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 -I. --experimental_allow_proto3_optional --go_out=plugins=grpc:. 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"

  pb "grpc_hello/helloworld"
  "google.golang.org/grpc"
)

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 (s *server) PrintHello(ctx context.Context, in *pb.NilRequest) (*pb.HelloReply, error){
  return &pb.HelloReply{Message: "Hello World"}, 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 频道,指定我们要连接的主机名和服务器端口。然后我们用这个频道创建存根实例。grpc.Dial()支持多个选项,参考dialoptions.go

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

import (
  "context"
  "log"
  "os"
  "time"

  pb "grpc_hello/helloworld"
  "google.golang.org/grpc"
)

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.Message)
/*
  r, err = c.PrintHello(ctx, &pb.NilRequest{})
  if err != nil {
    log.Fatalf("could not greet: %v", err)
  }
  log.Printf("Greeting: %s", r.Message)
*/
}

GRPC非加密(22数据帧)

3.2 一个应用场景

在做服务开发时,我们经常会遇到一种场景:定义一个接口,接口会通过判断是否传入某个参数,决定接口行为。例如,我们想提供一个 GetUser 接口,期望 GetUser 接口在传入 username 参数时,根据 username 查询用户的信息,如果没有传入 username,则默认根据 userId 查询用户信息。

这时候,我们需要判断客户端有没有传入 username 参数。我们不能根据 username 是否为空值来判断,因为我们不能区分客户端传的是空值,还是没有传 username 参数。这是由 Go 语言的语法特性决定的:如果客户端没有传入 username 参数,Go 会默认赋值为所在类型的零值,而字符串类型的零值就是空字符串。

那我们怎么判断客户端有没有传入 username 参数呢?最好的方法是通过指针来判断,如果是 nil 指针就说明没有传入,非 nil 指针就说明传入。

syntax = "proto3";

package proto;
option go_package = ".";

//go:generate protoc -I. --experimental_allow_proto3_optional --go_out=plugins=grpc:.

service User {
  rpc GetUser(GetUserRequest) returns (GetUserResponse) {}
}

message GetUserRequest {
  string class = 1;
  optional string username = 2;
  optional string user_id = 3;
}

message GetUserResponse {
  string class = 1;
  string user_id = 2;
  string username = 3;
  string address = 4;
  string sex = 5;
  string phone = 6;
}

在需要设置为可选字段的前面添加了 optional 标识。在执行 protoc 命令时,需要传入--experimental_allow_proto3_optional参数以打开 optional 选项(新版本不用此参数)。

protoc v3.19.4可以编译optional字段为指针(通过*.pb.go中type GetUserRequest struct确认)。

4. gRPC认证

Grpc单向认证示例google.golang.org/grpc/examples/features/encryption

Grpc双向认证示例opennessinterfaceservice

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 DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err 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导入CAclient证书,生成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++

5.      grpc-go基于双向认证安全通信

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语言中文网

posted @ 2020-02-29 18:19  yuxi_o  阅读(1004)  评论(0编辑  收藏  举报