GO的框架之gRPC(一)

gRPC gRPC是什么,有哪些优点? gRPC是一种高性能、开源的远程过程调用(RPC)框架,它可以使不同平台和语言之间的服务相互通信。它的优点包括:高效性、跨平台、异步流处理、支持多种语言、安全、易于使用和开源。 gRPC服务端的创建流程如下。 (1)创建Netty HTTP2服务端。 (2)将

gRPC

gRPC是什么,有哪些优点?

gRPC是一种高性能、开源的远程过程调用(RPC)框架,它可以使不同平台和语言之间的服务相互通信。它的优点包括:高效性、跨平台、异步流处理、支持多种语言、安全、易于使用和开源。

Untitled.png

gRPC服务端的创建流程如下。

(1)创建Netty HTTP2服务端。

(2)将需要调用的服务端接口实现类注册到内部的Registry中,当客户端发起 RPC调用时,可以根据RPC请求消息中的服务定义信息查询到服务接口实现类。

(3)创建gRPC Server。gRPC Server是gRPC服务端的抽象,聚合了各种Listener,用于RPC消息的统一调度和处理。gRPC Server在接收到gRPC请求消息后会先对gRPC消息头和消息体进行解析和处理,然后经过内部的服务路由和调用,最后返回响应消息。

总结

gRPC 是一个强大的框架,它通过以下方式实现跨语言、高性能的 RPC:

  1. 服务定义:使用 Protocol Buffers 定义服务和消息格式。
  2. 代码生成:使用 protoc 编译 .proto 文件生成客户端和服务器端代码。
  3. 服务实现:在服务器端实现服务接口,并启动服务器。
  4. 客户端调用:在客户端调用远程服务并处理响应。

gRPC 的各层实现与 OSI 七层模型的对应关系如下:

  • 物理层数据链路层:由底层网络硬件和驱动程序处理。
  • 网络层:依赖 IP 协议进行数据包路由。
  • 传输层:使用 TCP 协议提供可靠传输。
  • 会话层:使用 HTTP/2 管理会话和多路复用。
  • 表示层:使用 Protocol Buffers 进行数据序列化。
  • 应用层:包括用户定义的服务逻辑和 gRPC 框架提供的功能。

(1)客户端调用远程方法发起RPC调用,对调用的请求信息使用ProtoBuf进行对象序列化压缩。

(2)服务端(gRPC Server)在接收到请求后,解码请求体,进行业务逻辑处理并返回。

(3)对响应结果使用ProtoBuf进行对象序列化压缩。

(4)客户端接收到服务端的响应结果,解码请求体,回调被调用的方法,唤醒正在等待响应(阻塞)的客户端调用并返回响应结果。

(5)语言中立,支持多种语言。

(6)通信协议基于标准的HTTP2来设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性。这些特性使得gRPC在移动端设备上更加省电和节省网络流量。同时HTTP2协议让gRPC的网络兼容能力更好。

(7)序列化支持ProtoBuf和JSON。ProtoBuf是一种语言无关的高性能序列化框架,基于HTTP2和ProtoBuf,保障了RPC调用的高性能。

gRPC 与 OSI 七层模型的对应关系

物理层(Physical Layer)

物理层涉及物理设备之间的实际连接。在 gRPC 的上下文中,这包括网络电缆、无线传输等硬件设备。gRPC 并不直接处理这部分内容。

数据链路层(Data Link Layer)

数据链路层负责设备之间的数据帧传输和错误检测与纠正。在 gRPC 中,这一层通常由底层网络接口卡(NIC)和驱动程序处理。gRPC 并不直接操作数据链路层。

网络层(Network Layer)

网络层负责数据包的路由选择和转发。gRPC 依赖 TCP/IP 协议栈中的 IP 协议来实现这一层的功能,确保数据包可以从客户端路由到服务器。

传输层(Transport Layer)

传输层负责端到端的通信控制和错误检测。在 gRPC 中,这一层主要由 TCP 协议实现,提供可靠的、面向连接的传输。HTTP/2 运行在 TCP 之上。

会话层(Session Layer)

会话层管理会话的建立、维护和终止。HTTP/2 在这个层次上提供了多路复用、流量控制、首部压缩等功能。gRPC 使用 HTTP/2 来管理多个并发 RPC 调用的会话。

表示层(Presentation Layer)

表示层负责数据的语法和语义表示。在 gRPC 中,表示层的功能由 Protocol Buffers 实现,负责序列化和反序列化消息数据。

应用层(Application Layer)

应用层是用户和网络之间的接口。gRPC 的应用层包括客户端和服务器端的应用程序代码,以及 gRPC 框架提供的库和接口,用于定义和调用远程服务。

Restful API 对比

提到服务与服务之间的调用,大家最常见的就是 REST API 。

服务方提供一个接口文档,调用方按照这个接口文档调用接口,这貌似挺合理的。

但是也有不好的地方:

REST API 更擅长对资源进行 CRUD 操作。

通常 REST API 对一个资源进行增删改查是非常直观的,但是如果要对特定的目的操作就比较不那么直观了。

比如:要给名为张三的学生数学成绩加上10分。

虽然 REST API 也能通过更新操作进行操作,但是毕竟不是那么直观。

而 RPC 他是直接把方法抛出去了,直接调用方法为 Student.Increment(Name,Sore) 的方法就完成了,看着是不是更加直观。

RPC 更高效。

RPC 可以通过 TCP 进行长连接,在调用量非常大的时候是非常有优势的。

当然 RESTful 也可以通过 keep-alive 实现长连接,但是它最大的一个问题是它的request-response模型是阻塞的 ( http1.0 和 http1.1,http 2.0 没这个问题)。

说得直白点,发送一个请求后只有等到 response 返回才能发送第二个请求,RPC 的实现没有这个限制。

所以在如今大流量的情况下,RPC 更出色。

grpc的拦截器

拦截器的作用,是在执行核心业务方法的前后,创造出一个统一的切片,来执行所有业务方法锁共有的通用逻辑. 此外,我们还能够通过这部分通用逻辑的执行结果,来判断是否需要熔断当前的执行链路,以起到所谓的”拦截“效果.有关 grpc 拦截器的内容,其实和 gin 框架中的 handlersChain 是异曲同工的.

下面我们看看 grpc 中对于一个拦截器函数的具体定义:

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

其中几个入参的含义分别为:

  • req:业务处理方法的请求参数
  • info:当前所属的业务服务 service
  • handler:真正的业务处理方法

因此一个拦截器函数的使用模式应该是:

var myInterceptor1 = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    // 前处理校验
    if err := preLogicCheck();err != nil{
       // 前处理校验不通过,则拦截,不调用业务方法直接返回
       return nil,err
    }

     // 前处理校验通过,正常调用业务方法
     resp, err = handle(ctx,req)
     if err != nil{
         return nil,err
     }

      // 后置处理校验
      if err := postLogicCheck();err != nil{
         // 后置处理校验不通过,则拦截结果,包装错误返回
         return nil,err
      }

      // 正常返回结果
      return resp,nil
}

gRPC-go

rpc,全称 remote process call(远程过程调用),是微服务架构下的一种通信模式. 这种通信模式下,一台服务器在调用远程机器的接口时,能够获得像调用本地方法一样的良好体验.

rpc 通常对标的是 restful 风格的 http 调用方式,下面开放地聊聊个人眼中 rpc 相较于 http 的优势所在:

  • rpc 调用基于 sdk 方式,调用方法出入参协议固定,stub 文件本身还能起到接口文档的作用,很大程度上优化了通信双方约定协议达成共识的成本.
  • rpc 在传输层协议 tcp 基础之上,可以由实现框架自定义填充应用层协议细节,理论上存在着更高的上限

事物往往具有多面性,一些优点在转换视角后可能也会成为对应的劣势,因此从另一个角度看,rpc 相较于 http 存在如下缺点:

  • 基于 sdk 方式调用,灵活度低、开发成本高,更多地适合用于系统内部模块间的通信交互,不适合对外
  • 用户自定义实现应用层协议,下限水平也很不稳定

grpc-go 是基于 go 语言实现的 grpc 框架,要知道 go 语言本身也是 google 实现的,因此 golang 和 grpc 都是 google 的亲儿子,两者的契合度也是没得说,敬请诸位放心食用。

grpc-go 以 HTTP2 作为应用层协议,使用 protobuf (下文可能简称 pb)作为数据序列化协议以及接口定义语言。

Untitled.png

pb 桩文件

1.编写protobuf文件

syntax = "proto3"; // 固定语法前缀
option go_package = ".";  // 指定生成的Go代码在你项目中的导入路径

package pb; // 包名

// 定义服务
service HelloService {
    // SayHello 方法
    rpc SayHello (HelloReq) returns (HelloResp) {}
}

// 请求消息
message HelloReq {
    string name = 1;
}

// 响应消息
message HelloResp {
    string reply = 1
}

文件以 .proto 作为后缀,扮演着 grpc 客户端与服务端通信交互的接口定义语言(DDL)的角色.核心包括:

  • 定义业务处理服务 HelloService,声明业务方法的名称(SayHello)以及出入参协议(HelloReq/HelloResp)
  • 遵循 protobuf 的风格,分别声明出入参的类型定义:HelloReq 和 HelloResp,其中分别包含了字符串类型的成员字段 name 和 reply
  1. 生成pb.go文件
protoc --go_out=. --go-grpc_out=. pb/hello.proto

执行上述指令后,会生成 pb.go 和 grpc.pb.go 两个文件.

pb.go:

核心是基于 .proto 定义的出入参协议,生成对应的 golang 类定义代码.

// ...
package proto


import (
    protoreflect "google.golang.org/protobuf/reflect/protoreflect"
    protoimpl "google.golang.org/protobuf/runtime/protoimpl"
    reflect "reflect"
    sync "sync"
)

// ...
// 请求消息
type HelloReq struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields
    Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Age  int32  `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
}


// ...
// 响应消息
type HelloResp struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Reply string `protobuf:"bytes,1,opt,name=reply,proto3" json:"reply,omitempty"`
}

grpc.pb.go

package proto


import (
    context "context"
    grpc "google.golang.org/grpc"
    codes "google.golang.org/grpc/codes"
    status "google.golang.org/grpc/status"
)


// 基于 .proto 文件生成的客户端框架代码
// 客户端 interface
type HelloServiceClient interface {
    // SayHello 方法
    SayHello(ctx context.Context, in *HelloReq, opts ...grpc.CallOption) (*HelloResp, error)
}


// 客户端实现类
type helloServiceClient struct {
    cc grpc.ClientConnInterface
}


// 客户端构造器函数
func NewHelloServiceClient(cc grpc.ClientConnInterface) HelloServiceClient {
    return &helloServiceClient{cc}
}


// 客户端请求入口
func (c *helloServiceClient) SayHello(ctx context.Context, in *HelloReq, opts ...grpc.CallOption) (*HelloResp, error) {
    out := new(HelloResp)
    err := c.cc.Invoke(ctx, "/pb.HelloService/SayHello", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}


// 服务端注册入口
func RegisterHelloServiceServer(s grpc.ServiceRegistrar, srv HelloServiceServer) {
    s.RegisterService(&HelloService_ServiceDesc, srv)
}


// 服务端业务方法框架代码
func _HelloService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
    in := new(HelloReq)
    if err := dec(in); err != nil {
        return nil, err
    }
    if interceptor == nil {
        return srv.(HelloServiceServer).SayHello(ctx, in)
    }
    info := &grpc.UnaryServerInfo{
        Server:     srv,
        FullMethod: "/pb.HelloService/SayHello",
    }
    handler := func(ctx context.Context, req interface{}) (interface{}, error) {
        return srv.(HelloServiceServer).SayHello(ctx, req.(*HelloReq))
    }
    return interceptor(ctx, in, info, handler)
}


// 服务端业务处理服务描述符
var HelloService_ServiceDesc = grpc.ServiceDesc{
    ServiceName: "pb.HelloService",
    HandlerType: (*HelloServiceServer)(nil),
    Methods: []grpc.MethodDesc{
        {
            MethodName: "SayHello",
            Handler:    _HelloService_SayHello_Handler,
        },
    },
    Streams:  []grpc.StreamDesc{},
    Metadata: "proto/hello.proto",
}

grpc.pb.go里面核心内容包含了:

  • 基于.proto文件的客户端桩代码,后续作为用户使用grpc客户端模块的sdk入口
  • 基于 .proto 文件生成了服务端的服务注册桩代码,后续作为用户使用 grpc 服务端模块的 sdk 入口
  • 基于 .proto 文件生成了业务处理服务(pb.HelloService)的描述符,每个描述符内部会建立基于方法名(SayHello)到具体处理函数(_HelloService_SayHello_Handler)的映射关系

Untitled.png

服务端使用代码

package main


import (
    "context"
    "fmt"
    "net"


    "github.com/grpc_demo/proto"


    "google.golang.org/grpc"
)


// 业务处理服务
type HelloService struct {
    proto.UnimplementedHelloServiceServer
}


// 实现具体的业务方法逻辑
func (s *HelloService) SayHello(ctx context.Context, req *proto.HelloReq) (*proto.HelloResp, error) {
    return &proto.HelloResp{
        Reply: fmt.Sprintf("hello name: %s", req.Name),
    }, nil
}


func main() {
    // 创建 tcp 端口监听器
    listener, err := net.Listen("tcp", ":8093")
    if err != nil {
        panic(err)
    }


    // 创建 grpc server
    server := grpc.NewServer()
    // 将自定义的业务处理服务注册到 grpc server 中
    proto.RegisterHelloServiceServer(server, &HelloService{})
    // 运行 grpc server
    if err := server.Serve(listener); err != nil {
        panic(err)
    }
}
 
  • 声明业务处理服务 HelloService实现好桩文件中定义的业务处理方法 SayHello
  • 调用 net.Listen 方法,创建 tcp 端口监听
  • 调用 grpc.NewServer 方法,创建一个 grpc server 对象
  • 调用桩文件中预生成好的注册方法 proto.RegisterHelloServiceServer,将 HelloService 注册到 grpc server 对象当中
  • 运行 server.Serve 方法,监听指定的端口,真正启动 grpc server,

客户端使用代码

port (
    "context"
    "fmt"

    "github.com/grpc_demo/proto"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)


func main() {
    // 通过指定地址,建立与 grpc 服务端的连接
    conn, err := grpc.Dial("localhost:8093", grpc.WithTransportCredentials(insecure.NewCredentials()))
    // ...
    // 调用 .grpc.pb.go 文件中预生成好的客户端构造器方法,创建 grpc 客户端
    client := proto.NewHelloServiceClient(conn)
  
    // 调用 .grpc.pb.go 文件预生成好的客户端请求方法,使用 .pb.go 文件中预生成好的请求参数作为入参,向 grpc 服务端发起请求
    resp, err := client.SayHello(context.Background(), &proto.HelloReq{
        Name: "xiaoxuxiansheng",
    })
    // ...
    // 打印取得的响应参数
    fmt.Printf("resp: %+v", resp)
}
  • 调用 grpc.Dial 方法,与指定地址的 grpc 服务端建立连接
  • 调用桩文件中的方法 proto.NewHelloServiceClient,创建 pb 文件预声明好的 grpc 客户端对象
  • 调用 client.SayHello 方法,发送 grpc 请求,并处理响应结果
LICENSED UNDER CC BY-NC-SA 4.0
Comment