18

grpc快速使用

 4 years ago
source link: https://studygolang.com/articles/24455
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

使用grpc和protobuf实现rpc通信的例子

创建工程

mkdir grpc_test
cd grpc_test
go mod init

//使用github的grpc替换gp的grpc  
go mod edit -replace=google.golang.org/grpc=github.com/grpc/grpc-go@latest
go mod edit -replace=golang.org/x/net=github.com/golang/net@latest

go mod tidy
go mod vendor
go build -mod=vendor

科学上网可以不用上面的replace,直接设置如下环境:

export GOPROXY=https://goproxy.io 或者 set GOPROXY=https://goproxy.io

工程准备

grpc_test
    /proto/ 
        /search.proto   
    /client/    
        /client.go  
    /server/    
        /server.go

生成proto

编写proto

syntax = "proto3";

package proto;

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

message SearchRequest {
    string request = 1;
}

message SearchResponse {
    string response = 1;
}

定义中包含了服务接口的定义.

protobuf环境

首先下载protoc放入path

https://github.com/protocolbuffers/protobuf/releases

go get -u github.com/golang/protobuf/protoc-gen-go //下载proto go插件
export PATH=$PATH:$GOPATH/bin  //protoc-gen-go 添加到path

生成

确保proto和protoc-gen-go可用,protoc参数中--go_out会自动加载protoc-gen-go

protoc --go_out=plugins=grpc: 生成目录 proto文件或者目录

protoc --go_out=plugins=grpc:. *.proto

会生成search.pb.go

编写server

package main

import (
	"context"
	"log"
	"net"

	pb "grpctest/proto"

	"google.golang.org/grpc"
)

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() //创建 gRPC Server对象

	//将 SearchService(其包含需要被调用的服务端接口)注册到gRPC Server 的内部注册中心
	//这样可以在接受到请求时,通过内部的服务发现,发现该服务端接口并转接进行逻辑处理
	pb.RegisterSearchServiceServer(server, &SearchService{})

	lis, err := net.Listen("tcp", ":"+PORT) //创建 Listen,监听 TCP 端口
	if err != nil {
		log.Fatalf("net.Listen err: %v", err)
	}

	//gRPC Server开始 lis.Accept,直到 Stop 或 GracefulStop
	server.Serve(lis)
}

编写client

package main

import (
	"context"
	"log"

	"google.golang.org/grpc"

	pb "grpctest/proto"
)

const PORT = "9001"

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

	//创建 SearchService 的客户端对象
	client := pb.NewSearchServiceClient(conn)

	//发送 RPC 请求,等待同步响应,得到回调后返回响应结果
	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())
}

数据流模式RPC

在上面的例子中展示的一元rpc也就是简单rpc的模式。gPrc还有流模式的rpc. 分为服务端流rpc\客户端流rpc\双向流迷失rpc

服务端流RPC

在服务端流模式的RPC实现中,服务端得到客户端请求后,处理结束返回一个数据应答流。在发送完所有的客户端请求的应答数据后,服务端的状态详情和可选的跟踪元数据发送给客户端

服务接口定义

通过stream修饰的方式表示该接口调用时,服务端会以数据流的形式将数据返回给客户端

//订单服务service定义
service OrderService {
    rpc GetOrderInfos (OrderRequest) returns (stream OrderInfo) {}; //服务端流模式
}

生成代码变化

protoc --go_out=plugins=grpc:. *.proto

在自动生成的go代码程序当中,每一个流模式对应的服务接口,都会自动生成对应的单独的client和server程序,以及对应的结构体实现。

服务端生成代码

流模式下,服务接口的服务端提供Send方法,将数据以流的形式进行发送

type OrderService_GetOrderInfosServer interface {
    Send(*OrderInfo) error
    grpc.ServerStream
}

type orderServiceGetOrderInfosServer struct {
    grpc.ServerStream
}

func (x *orderServiceGetOrderInfosServer) Send(m *OrderInfo) error {
    return x.ServerStream.SendMsg(m)
}

客户端生成代码

流模式下,服务接口的客户端提供Recv()方法接收服务端发送的流数据

type OrderService_GetOrderInfosClient interface {
    Recv() (*OrderInfo, error)
    grpc.ClientStream
}

type orderServiceGetOrderInfosClient struct {
    grpc.ClientStream
}

func (x *orderServiceGetOrderInfosClient) Recv() (*OrderInfo, error) {
    m := new(OrderInfo)
    if err := x.ClientStream.RecvMsg(m); err != nil {
        return nil, err
    }
    return m, nil
}

服务端实现

因为是流模式开发,服务端将数据以流的形式进行发送,因此,该方法的第二个参数类型为OrderService_GetOrderInfosServer,该参数类型是一个接口,其中包含Send方法,允许发送流数据。Send方法的具体实现在编译好的pb.go文件中,进一步调用grpc.SeverStream.SendMsg方法 服务端注册模式和一元rpc是没区别的

//订单服务实现
type OrderServiceImpl struct {
}

//获取订单信息s
func (os *OrderServiceImpl) GetOrderInfos(request *message.OrderRequest, stream message.OrderService_GetOrderInfosServer) error {
    fmt.Println(" 服务端流 RPC 模式")

    orderMap := map[string]message.OrderInfo{
        "201907300001": message.OrderInfo{OrderId: "201907300001", OrderName: "衣服", OrderStatus: "已付款"},
        "201907310001": message.OrderInfo{OrderId: "201907310001", OrderName: "零食", OrderStatus: "已付款"},
        "201907310002": message.OrderInfo{OrderId: "201907310002", OrderName: "食品", OrderStatus: "未付款"},
    }
    for id, info := range orderMap {
        if (time.Now().Unix() >= request.TimeStamp) {
            fmt.Println("订单序列号ID:", id)
            fmt.Println("订单详情:", info)
            //通过流模式发送给客户端
            stream.Send(&info)
        }
    }
    return nil
}

客户端实现

服务端使用Send方法将数据以流的形式进行发送,客户端可以使用Recv()方法接收流数据,因为数据流失源源不断的,因此使用for无限循环实现数据流的读取,当读取到io.EOF时,表示流数据结束.

for {
        orderInfo, err := orderInfoClient.Recv()
        if err == io.EOF {
            fmt.Println("读取结束")
            return
        }
        if err != nil {
            panic(err.Error())
        }
        fmt.Println("读取到的信息:", orderInfo)
    }

客户端流RPC

服务端以数据流的形式返回数据的形式。对应的,也存在客户端以流的形式发送请求数据的形式。

服务接口定义

与服务端同理,客户端流模式的RPC服务声明格式,就是使用stream修饰服务接口的接收参数

//订单服务service定义
service OrderService {
    rpc AddOrderList (stream OrderRequest) returns (OrderInfo) {}; //客户端流模式
}

生成代码的差异

SendAndClose和Recv方法是客户端流模式下的服务端对象所拥有的方法 Send和CloseAndRecv是客户端流模式下的客户端对象所拥有的方法。

双向流模式

上文已经讲过了服务端流模式和客户端流模式。如果将客户端和服务端两种流模式结合起来,就是第三种模式,双向流模式。即客户端发送数据的时候以流数据发送,服务端返回数据也以流的形式进行发送,因此称之为双向流模式。

服务接口定义

//订单服务service定义
service OrderService {
    rpc GetOrderInfos (stream OrderRequest) returns (stream OrderInfo) {}; //双向流模式
}

生成代码的差异

服务端和客户端都实现了send 和 recv方法用来接收和发送流式的数据

TLS验证和Token认证

gRPC中默认支持两种授权方式,分别是:SSL/TLS认证方式、基于Token的认证方式

SSL/TLS认证方式

SL全称是Secure Sockets Layer,又被称之为安全套接字层,是一种标准安全协议,用于在通信过程中建立客户端与服务器之间的加密链接。 TLS的全称是Transport Layer Security,TLS是SSL的升级版。在使用的过程中,往往习惯于将SSL和TLS组合在一起写作SSL/TLS。 简而言之,SSL/TLS是一种用于网络通信中加密的安全协议。

使用SSL/TLS协议对通信连接进行安全加密,是通过非对称加密的方式来实现的。所谓非对称加密方式又称之为公钥加密,密钥对由公钥和私钥两种密钥组成。私钥和公钥成对存在,先生成私钥,通过私钥生成对应的公钥。公钥可以公开,私钥进行妥善保存。

在加密过程中:客户端想要向服务器发起链接,首先会先向服务端请求要加密的公钥。获取到公钥后客户端使用公钥将信息进行加密,服务端接收到加密信息,使用私钥对信息进行解密并进行其他后续处理,完成整个信道加密并实现数据传输的过程。 公钥加密私钥解密,非对称加密算法.

生成证书

openssl ecparam -genkey -name secp384r1 -out server.key
openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650

开启TLS认证的服务端和客户端连接代码

//TLS认证
creds, err := credentials.NewServerTLSFromFile("./keys/server.pem","./keys/server.key")
if err != nil {
    grpclog.Fatal("加载在证书文件失败", err)
}

//实例化grpc server, 开启TLS认证
server := grpc.NewServer(grpc.Creds(creds))
//TLS连接
creds, err := credentials.NewClientTLSFromFile("./keys/server.pem", "go-grpc-example")
if err != nil {
    panic(err.Error())
}
//1、Dail连接
conn, err := grpc.Dial("localhost:8092", grpc.WithTransportCredentials(creds))
if err != nil {
    panic(err.Error())
}

基于Token认证方式

在web应用的开发过程中会使用另外一种认证方式进行身份验证,那就是:Token认证。基于Token的身份验证是无状态,不需要将用户信息服务存在服务器或者session中. 基于Token认证的身份验证主要过程是:客户端在发送请求前,首先向服务器发起请求,服务器返回一个生成的token给客户端。客户端将token保存下来,用于后续每次请求时,携带着token参数。服务端在进行处理请求之前,会首先对token进行验证,只有token验证成功了,才会处理并返回相关的数据。

自定义Token

grpc.WithPerRPCCredentials(PerRPCCredentials)

type PerRPCCredentials interface {
    //组织token信息
    GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
    //设置是否基于tls认证进行安全传输
    RequireTransportSecurity() bool 
}

自定义token只需要实现PerRPCCredentials接口就可以了。

在客户端进行连接时,我们将自定义的token认证信息作为参数进行传入

//token认证
type TokenAuthentication struct {
    AppKey    string
    AppSecret string
}

//组织token信息
func (ta *TokenAuthentication) RequestMetaData(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "appid":    ta.AppKey,
        "appkey": ta.AppSecret,
    }, nil
}

//是否基于TLS认证进行安全传输
func (a *TokenAuthentication) RequireTransportSecurity() bool {
    return true
}

auth := TokenAuthentication{
        AppKey:    "hello",
        AppSecret: "20190812",
}
conn, err := grpc.Dial("localhost:8093", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&auth))
if err != nil {
    panic(err.Error())
}

服务端token校验

在服务端的调用方法中实现对token请求参数的判断,可以通过metadata获取token认证信息

func (mm *MathManager) AddMethod(ctx context.Context, request *message.RequestArgs) (response *message.Response, err error) {

    //通过metadata
    md, exist := metadata.FromIncomingContext(ctx)
    if !exist {
        return nil, status.Errorf(codes.Unauthenticated, "无Token认证信息")
    }

    var appKey string
    var appSecret string

    if key, ok := md["appid"]; ok {
        appKey = key[0]
    }

    if secret, ok := md["appkey"]; ok {
        appSecret = secret[0]
    }
}

拦截器的使用

在服务端的方法中,每个方法都要进行token的判断。程序效率太低,可以优化一下处理逻辑,在调用服务端的具体方法之前,先进行拦截,并进行token验证判断,这种方式称之为拦截器处理。除了此处的token验证判断处理以外,还可以进行日志处理等.

Interceptor

在grpc中编程实现中,可以在NewSever时添加拦截器设置,grpc框架中可以通过UnaryInterceptor方法设置自定义的拦截器

grpc.UnaryInterceptor(UnaryServerInterceptor)


type UnaryServerInterceptor func(ctx context.Context, 
                                req interface{}, 
                                info *UnaryServerInfo, 
                                handler UnaryHandler) (resp interface{}, err error)

自定义拦截器

func TokenInterceptor(ctx context.Context, 
                    req interface{}, 
                    info *grpc.UnaryServerInfo, 
                    handler grpc.UnaryHandler) (resp interface{}, err error) {

    //通过metadata
    md, exist := metadata.FromIncomingContext(ctx)
    if !exist {
        return nil, status.Errorf(codes.Unauthenticated, "无Token认证信息")
    }

    var appKey string
    var appSecret string
    if key, ok := md["appid"]; ok {
        appKey = key[0]
    }
    if secret, ok := md["appkey"]; ok {
        appSecret = secret[0]
    }

    if appKey != "hello" || appSecret != "20190812" {
        return nil, status.Errorf(codes.Unauthenticated, "Token 不合法")
    }
    //通过token验证,继续处理请求
    return handler(ctx, req)
}

在自定义的TokenInterceptor方法定义中,和之前在服务的方法调用的验证逻辑一致,从metadata中取出请求头中携带的token认证信息,并进行验证是否正确。如果token验证通过,则继续处理请求后续逻辑,后续继续处理可以由grpc.UnaryHandler进行处理

注册拦截器

server:=grpc.NewServer(grpc.Creds(creds),grpc.UnaryInterceptor(TokenInterceptor))

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK