golang使用grpc的快速入门教程
创建工程
mkdir grpc_test | |
cd g RPC _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= 或者 set GOPROXY=
工程准备
grpc_test | |
/proto/ | |
/search.proto | |
/client/ | |
/client.go | |
/ server / | |
/server.go |
生成proto
编写proto
syntax = "proto"; | |
package proto; | |
service SearchService { | |
rpc Search(SearchRequest) returns (SearchResponse) {} | |
} | |
message SearchRequest { | |
string request =; | |
} | |
message SearchResponse { | |
string response =; | |
} |
定义中包含了服务接口的定义.
protobuf环境
首先下载protoc放入path
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 = "" | |
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 = "" | |
func main() { | |
//连接g rpc 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{ | |
"": message.OrderInfo{OrderId: "201907300001", OrderName: "衣服", OrderStatus: "已付款"}, | |
"": message.OrderInfo{OrderId: "201907310001", OrderName: "零食", OrderStatus: "已付款"}, | |
"": 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 secpr1 -out server.key | |
openssl req -new -x -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()) | |
} | |
//、Dail连接 | |
conn, err := grpc.Dial("localhost:", 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: "", | |
} | |
conn, err := grpc.Dial("localhost:", 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[] | |
} | |
if secret, ok := md["appkey"]; ok { | |
appSecret = secret[] | |
} | |
} |
拦截器的使用
在服务端的方法中,每个方法都要进行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[] | |
} | |
if secret, ok := md["appkey"]; ok { | |
appSecret = secret[] | |
} | |
if appKey != "hello" || appSecret != "" { | |
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))