篇幅可能较长,可以先收藏,方便后续观看。
文章名称 | 地址 |
---|---|
Go微服务(一)——RPC详细入门 | 前往 |
Go微服务(二)——Protobuf详细入门 | 前往 |
Go微服务(三)——gRPC详细入门 | 前往 |
这里会联合protobuf语法以及protobuf如何去定义rpc服务,前面我们只生成了结构体,现在我们要让他为我们同时把接口生成,有了响应的接口,我们就再也不用去手写接口了。
gRPC是什么可以用官网的一句话来概括:
A high-performance, open-source universal RPC framework
一个高性能、开源的通用 RPC 框架
视频里的:
跨语言的RPC技术:
主流语言都支持。
基于Protobuf 消息格式:
高效二进制协议。
基于HTTP2协议通讯:
Protobuf ON HTTP2:
博客里的:
既然是 server/client 模型,那么我们直接用 Restful API 不是也可以满足吗,为什么还需要RPC呢?下面我们就来看看RPC到底有哪些优势:
gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说, gRPC使用的http2.0,而restful api则不一定)。不过gRPC还是有些特有的优势,如下:
gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件
另外,通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能。
gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式, 但是通常web服务的restful api似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如HLS,RTMP等,这些就不是我们通常web服务了,而是有专门的服务器应用。)
但是,通常我们不会去单独使用gRPC,而是将gRPC作为一个部件进行使用,这是因为在生产环境,我们面对大并发的情况下,需要使用分布式系统来去处理,而gRPC并没有提供分布式系统相关的一些必要组件。而且,真正的线上服务还需要提供包括负载均衡,限流熔断,监控报警,服务注册和发现等等必要的组件。
grpc性能高:protobuf为什么比json性能高?
Protobuf是由Google开发的二进制格式,用于在不同服务之间序列化数据。是一种IDL(interface description language)语言;
他比json快六倍;
为什么protobuf比json快?
protobuf的二进制数据流:
json数据流:
{
"content":"test",
"user":"test",
"user_id":"test"
}
对比json数据和protobuf数据格式可以知道:
grpc性能高:http2.0为什么比http1.1性能高?
多路复用:
http2.0和http 1.* 还有 http1.1pipling的对比:
grpc 多路复用还有哪些优点:
为什么http/1.1不能实现多路复用而http2.0可以?
因为http/1.1传输是用的文本,而http2.0用的是二进制分帧传输
头部压缩:
二进制分帧:
服务器主动推送资源:
由于支持服务器主动推送资源,则可以省去一部分请求。
比如你需要两个文件1.html,1.css,如果是http1.0则需要请求两次,服务端返回两次。但是http2.0则可以客户端请求一次,然后服务端直接回吐两次。
# protoc-gen-go 插件之前在protobuf教程中已经安装
#
# go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# 安装protoc-gen-go-grpc插件
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
查看当前grpc插件的版本:
protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.2.0
./service/service.proto
syntax = "proto3";
package hello;
option go_package = "MicroServiceStudy01/08-grpc/service";
message Request{
string value = 1;
}
message Response{
string value = 1;
}
// The HelloService service definition
// service 关键字
// HelloService 服务名称 对应接口的名称
// service服务会对应.pb.go文件里interface,里面的rpc对应接口中的函数
service HelloService{
rpc Hello (Request) returns (Response){}
rpc Channel(stream Request) returns (stream Response) {}
}
生成代码:
# D:\code\GoLandProjects\src\MicroServiceStudy01\
protoc -I . \
--go_out=./08-grpc/service \
--go_opt=module="MicroServiceStudy01/08-grpc/service" \
-go-grpc_out=./08-grpc/service \
--go-grpc_opt=module="MicroServiceStudy01/08-grpc/service" \
./08-grpc/service/service.proto
--go_out=......
:proto-gen-go 插件编译产物的存放目录
--go_opt=......
:protoc—gen-go 插件的opt参数,采用go moudle模式.
--go-grpc_out=.....
:proto-gen-go-grpc 插件编译产物的存放目录
--go-grpc_opt=......
:proto-gen-go-grpc 插件的opt参数,采用go moudle模式.
可能大家会感觉生成代码的命令内容比较多,后面会有有一些小技巧,真正去做代码生成的时候,很少会写这么多,这是现在的临时性工作。
我们可以看到,除了之前我们见过的.pb.go
文件,还多了个_grpc.pb.go
文件,里面是我们proto文件里定义的service对应生成的代码。
生成的客户端代码:
...
// HelloServiceClient是HelloService服务的客户端API。
type HelloServiceClient interface {
Hello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
Channel(ctx context.Context, opts ...grpc.CallOption) (HelloService_ChannelClient, error)
}
...
生成的服务端代码:
...
// HelloServiceServer 是 HelloService 服务的服务端 API。
// 所有实现都必须嵌入 UnimplementedHelloServiceServer
// 为了向前兼容
type HelloServiceServer interface {
Hello(context.Context, *Request) (*Response, error)
Channel(HelloService_ChannelServer) error
mustEmbedUnimplementedHelloServiceServer()
}
...
我们在生成的service_grpc.pb.go
文件中要注意一个部分:
// UnimplementedHelloServiceServer 必须嵌入到向前兼容的实现中。
type UnimplementedHelloServiceServer struct {
}
func (UnimplementedHelloServiceServer) Hello(context.Context, *Request) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented")
}
func (UnimplementedHelloServiceServer) Channel(HelloService_ChannelServer) error {
return status.Errorf(codes.Unimplemented, "method Channel not implemented")
}
func (UnimplementedHelloServiceServer) mustEmbedUnimplementedHelloServiceServer() {}
生成的这个文件里有一个叫做UnimplementedHelloServiceServer
的对象,这个对象它是实现了我们的HelloSerivce
接口,但是它实现了过后给你反的是一个错误信息,如果你没有任何对象实现这个接口,而你又调用了这个服务的话,它会给你返回一个return status.Errorf(codes.Unimplemented, "method Channel not implemented")
这样的错误信息。
./server/server.go
type HelloService struct {
// UnimplementedHelloServiceServer这个结构体是必须要内嵌进来的
// 也就是说我们定义的这个结构体对象必须继承UnimplementedHelloServiceServer。
// 嵌入之后,我们就已经实现了GRPC这个服务的接口,但是实现之后我们什么都没做,没有写自己的业务逻辑,
// 我们要重写实现的这个接口里的函数,这样才能提供一个真正的rpc的能力。
service.UnimplementedHelloServiceServer
}
var _ service.HelloServiceServer = (*HelloService)(nil)
// Hello 重写实现的接口里的Hello函数
func (p *HelloService) Hello(ctx context.Context, req *service.Request) (*service.Response, error){
resp := &service.Response{}
resp.Value = "hello:" + req.Value
return resp, nil
}
func main(){
// 首先通过grpc.NewServer() 构造一个grpc服务对象
grpcServer:=grpc.NewServer()
// 然后通过grpc插件生成的RegisterHelloServiceServer函数注册我们实现的HelloService服务。
service.RegisterHelloServiceServer(grpcServer,new(HelloService))
listen,err:=net.Listen("tcp",":1234")
if err!=nil{
log.Fatal("Listen TCP err:", err)
}
//最后通过grpcServer.Serve(listen) 在一个监听端口上提供gRPC服务
grpcServer.Serve(listen)
}
注意两点:
内嵌:
Unimplementedxxx
这个结构体是必须要内嵌进来的,也就是说我们定义的这个结构体对象必须继承Unimplementedxxx
。
嵌入之后,我们就已经实现了GRPC这个服务的接口,但是实现之后我们什么都没做,没有写自己的业务逻辑,我们要重写实现的这个接口里的函数,这样才能提供一个真正的rpc的能力。
重写
client/client.go
func main() {
// grpc.Dial负责和gRPC服务建立连接
conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
// 这里会提示,WithInsecure已被弃用,
// 如果你不想继续使用WithInsecure,可以使用
// 函数insecure.NewCredentials()返回credentials.TransportCredentials的一个实现。
// 您可以将其作为DialOption与grpc.WithTransportCredentials一起使用:
// 但是,API标记为实验性的,因此即使他们已经添加了弃用警告,您也不必立即切换。
//conn, err := grpc.Dial("localhost:1234",grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal("Dial err: ", err)
}
defer conn.Close()
// NewHelloServiceClient函数是xxx_grpc.pb.go中自动生成的函数,
// 基于已经建立的连接构造HelloServiceClient对象,
// 返回的client其实是一个HelloServiceClient接口对象
//
client := service.NewHelloServiceClient(conn)
// 通过接口定义的方法就可以调用服务端对应gRPC服务提供的方法
req := &service.Request{Value: "小亮"}
reply, err := client.Hello(context.Background(), req)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply.GetValue())
}
client := service.NewHelloServiceClient(conn)
func NewHelloServiceClient(cc grpc.ClientConnInterface) HelloServiceClient {
return &helloServiceClient{cc}
}
NewHelloServiceClient函数是xxx_grpc.pb.go中自动生成的函数,基于已经建立的连接构造HelloServiceClient对象,返回的client其实是一个HelloServiceClient接口对象。
需要的参数是grpc拨号后放回的连接对象。
context.Background()
:
返回一个非nil的空Context。它永远不会被取消,没有值,并且没有截止日期。它通常由主函数使用,初始化和测试,并作为传入的顶级上下文请求。
这一部分我们本次不作为重点内容,暂时简单过一遍。
09-grpc-stream/service/service.proto
syntax = "proto3";
package hello;
option go_package = "MicroServiceStudy01/09-grpc-stream/service";
message Request{
string value = 1;
}
message Response{
string value = 1;
}
// The HelloService service definition
// service 关键字
// HelloService 服务名称 对应接口的名称
// service服务会对应.pb.go文件里interface,里面的rpc对应接口中的函数
service HelloService{
rpc Hello (Request) returns (Response){}
rpc Channel(stream Request) returns (stream Response) {}
}
所以定义streaming RPC的语法如下:
rpc <function_name> (stream <type>) returns (stream <type>) {}
关键点:
stream关键字:
这个关键字可以出现在请求上面(stream Request)
,也可以出现在响应上(stream Response)
;
cd ./09-grpc-stream/service
protoc -I . --go_out=. --go_opt=module="MicroServiceStudy01/09-grpc-stream/service" --go-grpc_out=. --go-grpc_opt=module="MicroServiceStudy01/09-grpc-stream/service" ./service.proto
可以看到,流式上传,他的客户端是没有参数的,只有一些选项和我们的上下文信息,会返回一个HelloService_ChannelClient
类型的返回值(就是管道),可以用于和服务端进行双向通信。
可以看到在服务端的Channel方法参数是一个新的HelloService_ChannelServer类型 的参数(就是管道),可以用于和客户端双向通信;
HelloService_ChannelClient
和HelloService_ChannelServer
接口定义:
// Request ----->
// Response <-----
type HelloService_ChannelClient interface {
Send(*Request) error
Recv() (*Response, error)
grpc.ClientStream
}
// Request <-----
// Response ----->
type HelloService_ChannelServer interface {
Send(*Response) error
Recv() (*Request, error)
grpc.ServerStream
}
可以发现服务端和客户端的流辅助接口 均定义了Send和Recv方法 用于流数据的双向通信。
09-grpc-stream/server/server.proto
server端的核心逻辑:
type HelloService struct {
service.UnimplementedHelloServiceServer
}
var _ service.HelloServiceServer = (*HelloService)(nil)
func (p *HelloService) Channel(stream service.HelloService_ChannelServer) error {
// 服务端在循环中接收客户端发来的数据
for {
args, err := stream.Recv()
if err != nil {
// 如果遇到io.EOF表示客户端流关闭
if err == io.EOF {
return nil
}
return err
}
// 响应一个请求
// 生成返回的数据通过流发送给客户端
resp := &service.Response{
Value: "hello," + args.GetValue(),
}
err = stream.Send(resp)
if err != nil {
// 服务端发送异常,函数退出,服务端流关闭
return err
}
}
}
func main() {
// 1. 构造一个gRPC服务对象
grpcServer:=grpc.NewServer()
// 2. 通过gRPC插件生成的RegisterHelloServiceServer 函数注册我们实现的HelloService服务。
service.RegisterHelloServiceServer(grpcServer,new(HelloService))
// 3. 监听:1234端口
listen,err:=net.Listen("tcp",":1234")
if err!=nil{
log.Fatal("Listen TCP err:", err)
}
// 4. 通过grpcServer.Serve(listen) 在一个监听端口上提供gRPC服务
grpcServer.Serve(listen)
}
09-grpc-stream/client/client.proto
func main() {
conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := service.NewHelloServiceClient(conn)
// 客户端先调用Channel方法,获取返回的流对象
stream, err01 := client.Channel(context.Background())
if err01 != nil {
log.Fatal(err01)
}
// 在客户端我们将发送和接收放到两个独立的Goroutine
// 首先向服务端发送数据:
go func() {
for {
req := &service.Request{
Value: "小明",
}
if err := stream.Send(req); err != nil {
log.Fatal(err)
}
time.Sleep(time.Second)
}
}()
// 然后再循环中接收服务端返回的数据
for {
reply, err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
log.Fatal(err)
}
fmt.Println(reply.GetValue())
}
}
前面我们的rpc都存在一些缺陷,我们的rpc在调用的时候是裸着的,任何一个有这个客户端的人都可以调用,就相当于大门常打开,所以我们需要为他加一把锁。
但是加锁的话有很多方式,比如颁发证书,我为你这个客户端颁发了证书,你才可以调用,这也是最常用的方式,不过这里我们没有这样处理,而是写的一个认证中间件:
// UnaryServerInterceptor 提供了一个钩子来拦截服务器上一元RPC的执行。
// info 包含拦截器可以操作的RPC的所有信息。
// handler 是服务方法实现的包装器.
// 拦截器负责调用 handler 来完成RPC。
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
ctx
:请求上下文req
:rpc请求数据info
:服务端相关数据,不用理解这个handler
:处理请求的handler,相对于next()
resp
:rpc相应数据err
:rpc错误// StreamServerInterceptor 提供了一个钩子来拦截服务器上 流RPC 的执行。
// info 包含拦截器可以操作的RPC的所有信息。
// handler是服务方法实现的包装器.
// 拦截器负责调用处理程序来完成RPC。
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error
srv
:service信息ss
:Server数据流info
:服务端相关数据,不用理解这个handler
:处理请求的handler,相对于next()
err
:rpc错误syntax = "proto3";
package hello;
option go_package = "MicroServiceStudy01/10-grpc-auth/service";
message Request{
string value = 1;
}
message Response{
string value = 1;
}
// The HelloService service definition
// service 关键字
// HelloService 服务名称 对应接口的名称
// service服务会对应.pb.go文件里interface,里面的rpc对应接口中的函数
service HelloService{
rpc Hello (Request) returns (Response){}
rpc Channel(stream Request) returns (stream Response) {}
}
生成代码:
cd ./10-grpc-auth/service
protoc -I . --go_out=. --go_opt=module="MicroServiceStudy01/10-grpc-auth/service" --go-grpc_out=. --go-grpc_opt=module="Mic
roServiceStudy01/10-grpc-auth/service" ./service.proto
10-grpc-auth/auther/client.go
客户端的核心逻辑就是从meta中取clientId,clientSecret;
package author
import "context"
type Authentication struct {
clientId string
clientSecret string
}
// NewClientAuthentication 构造凭证
func NewClientAuthentication(clientId, clientSecret string) *Authentication {
return &Authentication{
clientId: clientId,
clientSecret: clientSecret,
}
}
// WithClientCredentials 通过客户端初始化凭证
func (a *Authentication) WithClientCredentials(clientId, clientSecret string) {
a.clientId = clientId
a.clientSecret = clientSecret
}
// GetRequestMetadata 从meta中取凭证信息:clientId,clientSecret
func (a *Authentication) GetRequestMetadata(context.Context, ...string) (
map[string]string, error) {
return map[string]string{
"client_id": a.clientId,
"client_secret": a.clientSecret,
}, nil
}
// RequireTransportSecurity 指示凭据是否需要传输安全性。
func (a *Authentication) RequireTransportSecurity() bool {
return false
}
10-grpc-auth/auther/server.go
package author
import (
"context"
"fmt"
"github.com/infraboard/mcube/logger"
"github.com/infraboard/mcube/logger/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
const (
ClientHeaderKey = "client_id"
ClientSecretKey = "client_secret"
)
type grpcAuthor struct {
log logger.Logger
}
// 构造并初始化 grpc author
func newGrpcAuthor() *grpcAuthor {
return &grpcAuthor{
// zap.L(): 返回一个未命名的全局 logger。
// .Named(): 添加一个新的路径段 到 logger 的 名称 。段由句点连接。
log: zap.L().Named("Grpc Author"),
}
}
// GetClientCredentialsFromMeta 从客户端发来的请求中获取凭证信息
func (a *grpcAuthor) GetClientCredentialsFromMeta(md metadata.MD) (
clientId, clientSecret string) {
cids := md.Get(ClientHeaderKey)
sids := md.Get(ClientSecretKey)
if len(cids) > 0 {
clientId = cids[0]
}
if len(sids) > 0 {
clientSecret = sids[0]
}
return
}
// 验证凭证信息
func (a *grpcAuthor) validateServiceCredential(clientId, clientSecret string) error {
if clientId == "" && clientSecret == "" {
return status.Errorf(codes.Unauthenticated, "client_id or client_secret is \"\"")
}
if !(clientId == "admin" && clientSecret == "123456") {
return status.Errorf(codes.Unauthenticated, "client_id or client_secret invalidate")
}
return nil
}
// Auth 普通模式的拦截器
func (a *grpcAuthor) Auth(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
// 从上下文中获取认证信息
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, fmt.Errorf("ctx is not an grpc incoming context!")
}
fmt.Println("grpc header info :", md)
// 获取客户端凭证信息
clientId, clientSecret := a.GetClientCredentialsFromMeta(md)
// 校验调用的客户端携带的凭证是否有效
if err := a.validateServiceCredential(clientId, clientSecret); err != nil {
return nil, err
}
resp, err = handler(ctx, req)
return resp, err
}
// StreamAuth 流模式的拦截器
func (a *grpcAuthor) StreamAuth(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) (err error) {
fmt.Println(srv, info)
// 从上下文中获取认证信息
// https://www.bilibili.com/video/BV1mi4y1d7SL?p=3&t=5032.0
md, ok := metadata.FromIncomingContext(ss.Context())
if !ok {
return fmt.Errorf("ctx is not an grpc incoming context!")
}
fmt.Println("grpc header info :", md)
// 获取客户端凭证
clientId, clientSecret := a.GetClientCredentialsFromMeta(md)
// 校验调用的客户端凭证是否有效
if err := a.validateServiceCredential(clientId, clientSecret); err != nil {
return err
}
return handler(srv, ss)
}
func GrpcAuthUnaryServerInterceptor() grpc.UnaryServerInterceptor {
return newGrpcAuthor().Auth
}
func GrpcAuthStreamServerInterceptor() grpc.StreamServerInterceptor {
return newGrpcAuthor().StreamAuth
}
10-grpc-auth/client/client.go
package main
import (
"MicroServiceStudy01/10-grpc-auth/author"
"MicroServiceStudy01/10-grpc-auth/service"
"context"
"fmt"
"google.golang.org/grpc"
"log"
)
func main() {
conn, err := grpc.Dial("localhost:1234",
grpc.WithInsecure(),
grpc.WithPerRPCCredentials(author.NewClientAuthentication("admin", "123456")),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// NewHelloServiceClient函数是xxx_grpc.pb.go中自动生成的函数,
// 基于已经建立的连接构造HelloServiceClient对象,
// 返回的client其实是一个HelloServiceClient接口对象
//
client := service.NewHelloServiceClient(conn)
// 通过接口定义的方法就可以调用服务端对应gRPC服务提供的方法
req := &service.Request{Value: "小亮"}
reply, err01 := client.Hello(context.Background(), req)
if err01 != nil {
log.Fatal(err01)
}
fmt.Println(reply.GetValue())
}
10-grpc-auth/server/server.go
package main
import (
"MicroServiceStudy01/10-grpc-auth/author"
"MicroServiceStudy01/10-grpc-auth/service"
"context"
"google.golang.org/grpc"
"log"
"net"
)
type HelloService struct {
// UnimplementedHelloServiceServer这个结构体是必须要内嵌进来的
// 也就是说我们定义的这个结构体对象必须继承UnimplementedHelloServiceServer。
// 嵌入之后,我们就已经实现了GRPC这个服务的接口,但是实现之后我们什么都没做,没有写自己的业务逻辑,
// 我们要重写实现的这个接口里的函数,这样才能提供一个真正的rpc的能力。
service.UnimplementedHelloServiceServer
}
var _ service.HelloServiceServer = (*HelloService)(nil)
// Hello 重写实现的接口里的Hello函数
func (p *HelloService) Hello(ctx context.Context, req *service.Request) (*service.Response, error){
resp := &service.Response{}
resp.Value = "hello:" + req.Value
return resp, nil
}
func main() {
// 1. 构造一个gRPC服务对象
grpcServer:=grpc.NewServer(
// 添加认证中间件,如果有多个中间件需要添加,使用ChainUnaryInterceptor
grpc.UnaryInterceptor(author.GrpcAuthUnaryServerInterceptor()),
// 添加stream API拦截器
grpc.StreamInterceptor(author.GrpcAuthStreamServerInterceptor()),
)
// 2.通过gRPC插件生成的RegisterHelloServiceServer 函数注册我们实现的HelloService服务。
service.RegisterHelloServiceServer(grpcServer,new(HelloService))
// 3. 监听:1234端口
listen,err:=net.Listen("tcp",":1234")
if err!=nil{
log.Fatal("Listen TCP err:", err)
}
// 4. 通过grpcServer.Serve(listen) 在一个监听端口上提供gRPC服务
grpcServer.Serve(listen)
}