grpc 初试

安装grpc

mac

brew tap grpc/grpc
brew install --with-plugins grpc
/usr/local/bin

protoc
grpc_cpp_plugin
grpc_node_plugin
grpc_python_plugin
grpc_csharp_plugin
grpc_objective_c_plugin
grpc_ruby_plugin

安装 packages

go get -u -v github.com/golang/protobuf/{proto,protoc-gen-go}
go get -u google.golang.org/grpc

Hello gRPC

目录结构

├── client
│   └── main.go
├── proto
│   └── hello
│       └── hello.proto
└── server
    └── main.go

hello.proto

syntax = "proto3"; // 指定proto版本

package proto;     // 指定包名

// 定义Hello服务
service Hello {
    // 定义SayHello方法
    rpc SayHello(HelloRequest) returns (HelloReply) {}
}

// HelloRequest 请求结构
message HelloRequest {
    string name = 1;
}

// HelloReply 响应结构
message HelloReply {
    string message = 1;
}

生成 .pb.go

因为这里我们还没有引入grpc-gateway所以命令很简单

protoc -I proto/hello/ proto/hello/hello.proto --go_out=plugins=grpc:proto/hello

server/main.go

package main

import (
    "fmt"
    "net"

    pb "grpc-go-practice2/proto/hello" // 引入编译生成的包

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/grpclog"
)

const (
    // Address gRPC服务地址
    Address = "127.0.0.1:50052"
)

// 定义helloService并实现约定的接口
type helloService struct{}

// HelloService ...
var HelloService = helloService{}

func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    resp := new(pb.HelloReply)
    resp.Message = "Hello " + in.Name + "."

    return resp, nil
}

func main() {
    listen, err := net.Listen("tcp", Address)
    if err != nil {
        grpclog.Fatalf("failed to listen: %v", err)
    }

    // 实例化grpc Server
    s := grpc.NewServer()

    // 注册HelloService
    pb.RegisterHelloServer(s, HelloService)
    fmt.Println("Listen on " + Address)

    s.Serve(listen)
}

client/main.go

package main

import (
    "fmt"
    pb "grpc-go-practice2/proto/hello" // 引入proto包

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/grpclog"
)

const (
    // Address gRPC服务地址
    Address = "127.0.0.1:50052"
)

func main() {
    // 连接
    conn, err := grpc.Dial(Address, grpc.WithInsecure())

    if err != nil {
        grpclog.Fatalln(err)
    }

    defer conn.Close()

    // 初始化客户端
    c := pb.NewHelloClient(conn)

    // 调用方法
    reqBody := new(pb.HelloRequest)
    reqBody.Name = "gRPC"
    r, err := c.SayHello(context.Background(), reqBody)
    if err != nil {
        grpclog.Fatalln(err)
    }
    fmt.Println(r.Message)
}

测试是否成功调用

go run server/main.go
go run client/main.go

输出以下字符代表调用成功

Hello gRPC.

GRPC SSL/TLS 认证

说白了 就是走https证书,因为后面要做的 grpc-gateway 必须基于https

制作私钥 (.key)

openssl genrsa -out server.key 2048
# 证书签名
openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650

证书签名信息

Country Name (2 letter code) []:CN
State or Province Name (full name) []:BeiJing
Locality Name (eg, city) []:ChaoYang
Organization Name (eg, company) []:iluoy Co. Ltd
Organizational Unit Name (eg, section) []:Dev
Common Name (eg, fully qualified host name) []:iluoy
Email Address []:iluoy@iluoy.com

修改 server/main.go

package main

import (
    "fmt"
    pb "grpc-go-practice2/proto/hello" // 引入proto包

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials" // 引入grpc认证包
    "google.golang.org/grpc/grpclog"
)

const (
    // Address gRPC服务地址
    Address = "127.0.0.1:50052"
)

func main() {
    // TLS连接
    creds, err := credentials.NewClientTLSFromFile("../keys/server.pem", "iluoy")
    if err != nil {
        grpclog.Fatalf("Failed to create TLS credentials %v", err)
    }
    conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))

    if err != nil {
        grpclog.Fatalln(err)
    }

    defer conn.Close()

    // 初始化客户端
    c := pb.NewHelloClient(conn)

    // 调用方法
    reqBody := new(pb.HelloRequest)
    reqBody.Name = "gRPC"
    r, err := c.SayHello(context.Background(), reqBody)

    if err != nil {
        grpclog.Fatalln(err)
    }
    fmt.Println(r.Message)
}

修改 client/main.go

package main

import (
    "fmt"
    "net"

    pb "grpc-go-practice2/proto/hello"

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials" // 引入grpc认证包
    "google.golang.org/grpc/grpclog"
)

const (
    // Address gRPC服务地址
    Address = "127.0.0.1:50052"
)

// 定义helloService并实现约定的接口
type helloService struct{}

// HelloService ...
var HelloService = helloService{}

func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    resp := new(pb.HelloReply)
    resp.Message = "Hello " + in.Name + "."

    return resp, nil
}

func main() {
    listen, err := net.Listen("tcp", Address)
    if err != nil {
        grpclog.Fatalf("failed to listen: %v", err)
    }

    // TLS认证
    creds, err := credentials.NewServerTLSFromFile("../keys/server.pem", "../keys/server.key")
    if err != nil {
        grpclog.Fatalf("Failed to generate credentials %v", err)
    }

    // 实例化grpc Server, 并开启TLS认证
    s := grpc.NewServer(grpc.Creds(creds))

    // 注册HelloService
    pb.RegisterHelloServer(s, HelloService)

    fmt.Println("Listen on " + Address + " with TLS")

    s.Serve(listen)
}

运行测试

cd server && go run main.go
cd client && go run main.go
Hello gRPC.

使用 grpc-gateway RPC转换HTTP

这里我们将使用grpc-gateway把RPC包装成HTTP服务,这样就方便其他服务调用

安装 grpc-gateway packages

go get -u -v github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u -v github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
go get -u -v github.com/golang/protobuf/protoc-gen-go

修改 proto/hello/hello.proto

syntax = "proto3";  // 指定proto版本

package proto;     // 指定包名

import "google/api/annotations.proto";

// 定义Hello服务
service HelloHttp {
    // 定义SayHello方法
    rpc SayHello(HelloHttpRequest) returns (HelloHttpReply) {
        // http option
        option (google.api.http) = {
            post: "/example/echo"
            body: "*"
        };
    }
}

// HelloRequest 请求结构
message HelloHttpRequest {
    string name = 1;
}

// HelloReply 响应结构
message HelloHttpReply {
    string message = 1;
}

Generate gRPC stub

生成基于grpc-gateway的hello.pb.go文件

这里的/Users/ailuoy/go-work/golib/src替换成自己的GOPATH

protoc -I/usr/local/include -I. \
  -I/Users/ailuoy/go-work/golib/src \
  -I/Users/ailuoy/go-work/golib/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --go_out=plugins=grpc:. \
  proto/hello/hello.proto

Generate reverse-proxy

生成 hello.pb.gw.go 文件. 这个是网关文件

protoc -I/usr/local/include -I. \
  -I/Users/ailuoy/go-work/golib/src \
  -I/Users/ailuoy/go-work/golib/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --grpc-gateway_out=logtostderr=true:. \
  proto/hello/hello.proto

Generate swagger definitions

生成 hello.swagger.json 文档文件

protoc -I/usr/local/include -I. \
  -I/Users/ailuoy/go-work/golib/src \
  -I/Users/ailuoy/go-work/golib/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --swagger_out=logtostderr=true:. \
  proto/hello/hello.proto

修改 server/main.go

package main

import (
    "crypto/tls"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "golang.org/x/net/context"
    "golang.org/x/net/http2"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/grpclog"
    pb "grpc-go-practice2/proto/hello"
    "io/ioutil"
    "net"
    "net/http"
    "strings"
)

// 定义helloHTTPService并实现约定的接口
type helloHTTPService struct{}

// HelloHTTPService Hello HTTP服务
var HelloHTTPService = helloHTTPService{}

// SayHello 实现Hello服务接口
func (h helloHTTPService) SayHello(ctx context.Context, in *pb.HelloHttpRequest) (*pb.HelloHttpReply, error) {
    resp := new(pb.HelloHttpReply)
    resp.Message = "Hello " + in.Name + "."

    return resp, nil
}

func main() {
    endpoint := "127.0.0.1:50052"
    conn, err := net.Listen("tcp", endpoint)
    if err != nil {
        grpclog.Fatalf("TCP Listen err:%v\n", err)
    }

    // grpc server
    creds, err := credentials.NewServerTLSFromFile("../keys/server.pem", "../keys/server.key")
    if err != nil {
        grpclog.Fatalf("Failed to create server TLS credentials %v", err)
    }
    grpcServer := grpc.NewServer(grpc.Creds(creds))
    pb.RegisterHelloHttpServer(grpcServer, HelloHTTPService)

    // gw server
    ctx := context.Background()
    dcreds, err := credentials.NewClientTLSFromFile("../keys/server.pem", "iluoy")
    if err != nil {
        grpclog.Fatalf("Failed to create client TLS credentials %v", err)
    }
    dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
    gwmux := runtime.NewServeMux()
    if err = pb.RegisterHelloHttpHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil {
        grpclog.Fatalf("Failed to register gw server: %v\n", err)
    }

    // http服务
    mux := http.NewServeMux()
    mux.Handle("/", gwmux)

    srv := &http.Server{
        Addr:      endpoint,
        Handler:   grpcHandlerFunc(grpcServer, mux),
        TLSConfig: getTLSConfig(),
    }

    grpclog.Infof("gRPC and https listen on: %s\n", endpoint)

    if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil {
        grpclog.Fatal("ListenAndServe: ", err)
    }

    return
}

func getTLSConfig() *tls.Config {
    cert, _ := ioutil.ReadFile("../keys/server.pem")
    key, _ := ioutil.ReadFile("../keys/server.key")
    var demoKeyPair *tls.Certificate
    pair, err := tls.X509KeyPair(cert, key)
    if err != nil {
        grpclog.Fatalf("TLS KeyPair err: %v\n", err)
    }
    demoKeyPair = &pair
    return &tls.Config{
        Certificates: []tls.Certificate{*demoKeyPair},
        NextProtos:   []string{http2.NextProtoTLS}, // HTTP2 TLS支持
    }
}

// grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC
// connections or otherHandler otherwise. Copied from cockroachdb.
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
    if otherHandler == nil {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            grpcServer.ServeHTTP(w, r)
        })
    }
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
            grpcServer.ServeHTTP(w, r)
        } else {
            otherHandler.ServeHTTP(w, r)
        }
    })
}

修改 client/main.go

package main

import (
    "fmt"
    pb "grpc-go-practice2/proto/hello" // 引入proto包

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials" // 引入grpc认证包
    "google.golang.org/grpc/grpclog"
)

const (
    // Address gRPC服务地址
    Address = "127.0.0.1:50052"
)

func main() {
    // TLS连接
    creds, err := credentials.NewClientTLSFromFile("../keys/server.pem", "iluoy")
    if err != nil {
        grpclog.Fatalf("Failed to create TLS credentials %v", err)
    }
    conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))

    if err != nil {
        grpclog.Fatalln(err)
    }

    defer conn.Close()

    // 初始化客户端
    c := pb.NewHelloHttpClient(conn)

    // 调用方法
    reqBody := new(pb.HelloHttpRequest)
    reqBody.Name = "gRPC"
    r, err := c.SayHello(context.Background(), reqBody)

    if err != nil {
        grpclog.Fatalln(err)
    }
    fmt.Println(r.Message)
}

测试grpc

cd server && go run main.go
cd client && go run main.go
Hello gRPC.

测试http

curl -X POST -k https://localhost:50052/example/echo -d '{"name": "gRPC-HTTP is working!"}'
{"message":"Hello gRPC-HTTP is working!."}

测试成功后, 我们只需要开启一个 server/main.go 就同时支持grpc和http两种格式的调用了

错误

no Go files in.../googleapis/google/api

参考文章