Golang gRPC 快速入门

gRPC 快速入门

gRPC 是 Google 开发的一个高性能、开源的 RPC(远程过程调用)框架, 广泛用于微服务架构中。
它基于 HTTP/2 和 Protocol Buffers(protobuf),
支持多语言、双向流式传输、负载均衡等功能,非常适合分布式系统。

核心概念

  • RPC:远程过程调用,即客户端可以像调用本地函数一样,调用远程服务器提供的函数。
  • Proto Buffers:gRPC 使用 protobuf 来定义消息格式和服务。它是一种高效的二进制序列化协议。
  • HTTP/2:gRPC 使用 HTTP/2 协议,支持多路复用、流控制、头部压缩和更低的延迟。

安装 gRPC 和 Protocol Buffers

在你的 Go 环境中安装 gRPC 和 protobuf 插件:

  1. 安装 gRPC-Go:
go get google.golang.org/grpc
  1. 安装 Protocol Buffers 编译器(protoc)

macos的如下:

brew install protoc # 其他环境请自查哈哈

如果在win环境中,需要将路径添加到环境变量Path中,方便实用protoc进行代码生成。

  1. 安装 Go 的代码生成插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

下文代码目录结构(可跳过)

tree -L 9 --noreport --charset ascii | sed 's/^/    /' > directory_structure.md
  • -L : 目录深度。
    .
    |-- common
    |   |-- app
    |   |   `-- grpc_client.go
    |   `-- service
    |       `-- grpc
    |           |-- impl
    |           |   `-- user.go
    |           |-- pb
    |           |   `-- user
    |           |       |-- user.pb.go
    |           |       `-- user_grpc.pb.go
    |           |-- proto
    |           |   `-- user.proto
    |           `-- register.go
    |-- directory_structure.md
    |-- grpc_client
    |   |-- core
    |   |   `-- core.go
    |   |-- main.go
    |   `-- service
    |       `-- grpc
    |           `-- client
    |               `-- user
    |                   `-- user.go
    |-- grpc_server
    |   |-- core
    |   |   `-- core.go
    |   |-- main.go
    |   `-- service

目录介绍:

  • impl : 对 pb 目录下的 xxx_grpc.pb.go 中未实现方法的实现的目录。
  • pb : 使用 protoc 指令,生成的代码文件目录
  • proto : 存放 xxx.proto 目录。

编写Proto文件

.proto 文件用来描述一个 gRPC 存在的服务和消息格式。

以下为定义一个rpc服务 UserService 并提供 GetUserInfo 方法,并定义该方法的请求参数、响应的消息。

文件:/common/service/grpc/proto/user.proto

syntax = "proto3";                        // 指定使用 Protocol Buffers 的 proto3 语法

package user;                             // 定义包名,类似于命名空间
option go_package ="./user";              // 指定生成的 Go 代码所在的包名以及该代码的导入路径。

import "google/protobuf/timestamp.proto"; // 导入其他 .proto 文件(如有需要)

// 定义服务
service UserService {
  // 定义获取用户信息的 RPC 方法
  rpc GetUserInfo (GetUserRequest) returns (GetUserResponse);
}

// ----------------------------  GetUserInfo

// 定义 GetUserRequest 请求消息
message GetUserRequest {
  int32 user_id = 1;  // 用户 ID
}

// 定义 GetUserResponse 响应消息
message GetUserResponse {
  User user = 1;  // 用户信息
  string remark = 2; // 备注信息
  google.protobuf.Timestamp resp_at = 3;// 消息回复时间
}

// ---------------------------- 内嵌类型

// 定义用户信息
message User {
  int32 id = 1;  // 用户 ID
  string name = 2;  // 用户名
  int32 age = 3;  // 年龄
  Gender gender = 4;  // 性别
  Address address = 5;  // 地址信息
  google.protobuf.Timestamp created_at = 6;  // 账户创建时间
  Status status = 7;  // 使用枚举表示用户状态
}

生成代码

同时生成 .pb.go_grpc.pb.go 文件

protoc --go_out=. --go-grpc_out=. --proto_path=. user.proto
  • --go_out=. : 生成 xxx.pb.go 文件到当前目录下。此处为当前目录下。
  • --go-grpc_out=. : 生成 xxx_grpc.pb.go 文件到当前目录下。此处为当前目录下。
  • --proto_path=. : 用于生成的的 .proto 文件所处的目录。此处为当前目录下。

如此便可生成以下两种文件:

  • xxx.pb.go 文件:

    生成 Protobuf 消息的结构体和序列化/反序列化的代码,专注于数据传输。

  • xxx_grpc.pb.go 文件:

    主要提供与 gRPC 服务相关的代码,包括客户端的调用接口和服务端的注册与实现。它封装了 gRPC 的底层通信逻辑,开发者只需实现接口即可。

基础用法

服务端

服务端方法(默认)

xxx_grpc.pb.go 文件中,可以看到下面这种代码:

// UnimplementedUserServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedUserServiceServer struct{}

func (UnimplementedUserServiceServer) GetUserInfo(context.Context, *GetUserRequest) (*GetUserResponse, error) {
    return nil, status.Errorf(codes.Unimplemented, "method GetUserInfo not implemented")
}

方法 GetUserInfo 是我们在 .proto 文件中,为 UserService 定义的方法。
需要重写该方法。

服务端方法(重写)

或者说是实现该方法,

  1. 定义结构体 UserSys ,嵌入 Unimplemented* ,以继承其方法。
  2. 复制 xxx_grpc.pb.go 中原有方法,修改其内部逻辑。
package impl

import (
    "context"
    "fmt"
    pbUser "go-developer/apps/common/service/grpc/pb/user"
    "google.golang.org/protobuf/types/known/timestamppb"
    "time"
)

type UserSys struct {
    pbUser.UnimplementedUserServiceServer
}

func NewUserSys() *UserSys {
    return &UserSys{}
}

// GetUserInfo 重写 xxx_grpc.pb.go 的 GetUserInfo
func (sys *UserSys) GetUserInfo(ctx context.Context, req *pbUser.GetUserRequest) (*pbUser.GetUserResponse, error) {

    time.Sleep(time.Millisecond) // 模拟查询

    return &pbUser.GetUserResponse{
        User: &pbUser.User{
            Id: req.GetUserId(),
        },
        Remark: fmt.Sprintf("你好,%d", req.GetUserId()),
        RespAt: timestamppb.Now(),
    }, nil
}

如此,Grpc 服务端的 UserServiceGetUserInfo 方法就实现了。
接下来要启动服务,使其可以被外部调用。

启动服务


// 提供一个不安全的GRPC服务。
func ServeGrpcInsecure() {
    // 初始化对端口的监听
    listener, err := net.Listen("tcp", ":8800")
    if err != nil {
        panic("rpc port listen failed :" + err.Error())
    }

    // 初始化grpc服务器
    gServer := grpc.NewServer()
    // 注册服务
    grpc2.RegisterServices(gServer)
    // 开始监听并提供 gRPC 服务
    err = gServer.Serve(listener)
    if err != nil {
        panic(err)
    }
}

客户端

就像核心概念中说的,客户端可以像调用本地函数一样,调用远程服务器提供的函数。

连接服务

文件:src/common/app/grpc_client.go


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

var (
    GrpcClientConn *grpc.ClientConn
    err    error
)

// 初始化一个不安全的客户端连接
func InitClientInsecure() {
    GrpcClientConn, err = grpc.NewClient("127.0.0.1:8800", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        panic("failed to conn grpc server:" + err.Error())
    }
}

调用远程方法

调用的方法是 xxx_grpc.pb.go 中的方法,

  1. New其服务对应的Client,传入上文中的连接。此处的服务是 UserService ,所以为 NewUserServiceClient()
  2. 调用需要的方法,传入参数获得响应。

import (
    "context"
    "go-developer/apps/common/apps"
    pbUser "go-developer/apps/common/service/grpc/pb/user"
)

type UserSys struct {
    ctx context.Context
}

func NewUserSys(ctx context.Context) *UserSys {
    return &UserSys{
        ctx: ctx,
    }
}

func (sys *UserSys) GetUserInfo(userId int32) interface{} {

    req := &pbUser.GetUserRequest{
        UserId: userId,
    }

    // 此处调用的方式是 xxx_grpc.pb.go 中的 New*Client 方法
    userClient := pbUser.NewUserServiceClient(apps.Client)
    resp, err := userClient.GetUserInfo(sys.ctx, req)
    if err != nil {
        panic(err)
    }

    return resp
}

TLS 加密

你可能已经发现,上文中的服务端和客户端之间,其tcp连接并没有使用任何加密方式,是不安全的。

通常用于 TCP 加密的方式是通过 TLS (Transport Layer Security),它提供了通信的安全性。

注册证书

  • ca.pem
  • ca.key
  • server.pem
  • server.key
  • client.pem
  • client.key

单向加密

理论

  1. TLS 单向加密(One-Way TLS)

在单向 TLS 中,只有服务器需要提供证书,而客户端则不需要。客户端通过验证服务器的证书来确认它与预期的服务器通信,并建立加密连接。这种模式下,只有服务器被验证,客户端未进行验证。

工作流程:

  1. 客户端发起请求:客户端向服务器发起连接请求。
  2. 服务器发送证书:服务器向客户端提供其数字证书,证书包含公钥。
  3. 客户端验证证书:客户端使用 CA(证书颁发机构)签发的根证书,验证服务器提供的证书是否有效。
  4. 建立加密通道:验证成功后,客户端生成一个对称密钥,并用服务器的公钥加密该密钥,发送给服务器。双方用该对称密钥加密通信数据。

应用场景:

  • 大多数普通 HTTPS 连接使用的都是单向 TLS,如浏览器访问网站。
  • 单向 TLS 保证了服务器的真实性和通信的加密,但不验证客户端的身份。

安全性:

  • 数据在传输过程中加密,防止窃听和篡改。
  • 客户端能够确认服务器的身份,但服务器无法确认客户端的身份。

    实操

服务端
  1. 初始化 Credentials
// 服务端 TLS 单向凭证
func ServerTLSOneWayCred() credentials.TransportCredentials {
    creds, err := credentials.NewServerTLSFromFile(iconst.ServerPemPath, iconst.ServerKeyPath)
    if err != nil {
        log.Fatalln("初始化服务端TLS凭证失败" + err.Error())
    }
    return creds
}

其中:

  • iconst.ServerPemPath : server.pem 的路径
  • iconst.ServerKeyPath : server.key 的路径
  1. 使用该凭证
    gServer := grpc.NewServer(grpc.Creds(ServerTLSOneWayCred())) // tls 单向认证的服务

总结:
与无校验的对比,只在 grpc.NewServer 增加了 grpc.Creds 这个option

客户端
  1. 获取认证
// 客户端 获取 tls 单向认证
func ClientTlsOneWayCred() credentials.TransportCredentials {
    cred, err := credentials.NewClientTLSFromFile(iconst.ServerPemPath, `*.fanfine.cn`)
    if err != nil {
        panic("new client tls cred failed : " + err.Error())
    }
    return cred
}
  • iconst.ServerPemPath : server.pem 的路径
  • *.fanfine.cn : server.conf 中配置的合法域名。
    1. 使用凭证
    GrpcClientConn, err = grpc.NewClient("127.0.0.1:8800",
        grpc.WithTransportCredentials(ClientTlsOneWayCred())) // 单向的客户端连接

总结:
与无校验对比,更换了凭证

双向加密

理论

在双向 TLS 中,客户端和服务器都需要提供并验证各自的证书。服务器不仅要向客户端证明自己的身份,客户端也要向服务器提供证书,证明它的合法性。

工作流程:

  1. 客户端发起请求:客户端向服务器发起连接请求。
  2. 服务器发送证书:服务器向客户端提供其数字证书。
  3. 客户端验证服务器证书:客户端验证服务器的证书是否由可信的 CA 签发。
  4. 客户端发送证书:服务器请求客户端提供其证书,客户端将自己的证书发送给服务器。
  5. 服务器验证客户端证书:服务器验证客户端证书的合法性。
  6. 建立加密通道:验证成功后,双方协商对称密钥,并用各自的公钥加密传输数据。

应用场景:

  • 高度敏感的通信场景,如金融交易、企业内部服务间通信等。
  • 双向 TLS 可确保服务器和客户端互相验证身份,提供更高的安全性。

安全性:

  • 双向认证确保了通信双方的身份都被验证,从而防止了恶意客户端的访问。
  • 在对安全性要求极高的应用中,双向 TLS 是一种常见选择。

实操

服务端
  1. 初始化凭证
// 服务端 TLS 双向凭证
func ServerTLSTwoWayAuth() credentials.TransportCredentials {

    // 读服务端证书
    cert, err := tls.LoadX509KeyPair(iconst.ServerPemPath, iconst.ServerKeyPath)
    if err != nil {
        log.Fatal("证书读取错误", err)
    }

    // 创建一个新的、空的 CertPool
    certPool := x509.NewCertPool()
    ca, err := os.ReadFile(iconst.CAPemPath)
    if err != nil {
        log.Fatalln("ca证书os读取失败", err)
    }
    // 尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
    certPool.AppendCertsFromPEM(ca)

    // 构建基于 TLS 的 TransportCredentials 选项
    creds := credentials.NewTLS(&tls.Config{
        // 设置证书链,允许包含一个或多个
        Certificates: []tls.Certificate{cert},
        // 要求必须校验客户端的证书。可以根据实际情况选用以下参数
        ClientAuth: tls.RequireAndVerifyClientCert,
        // 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
        ClientCAs: certPool,
    })
    return creds
}
  1. 使用双向认证
    gServer := grpc.NewServer(grpc.Creds(ServerTLSTwoWayAuth())) // tls 双向认证服务
客户端

此时客户端若仍为单向认证的方式,会出现报错:

panic: rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: remote error: tls: certificate required"

修改:

  1. 获取双向认证的 cred

// 客户端 获取 tls 双向认证
func ClientTlsTwoWayCred() credentials.TransportCredentials {
    // 1. 读取客户端密钥对
    cert, _ := tls.LoadX509KeyPair(iconst.ClientPemPath, iconst.ClientKeyPath)

    // 2. 创建一个新的、空的 CertPool
    certPool := x509.NewCertPool()

    // 3. 读取CA
    ca, err := os.ReadFile(iconst.CAPemPath)
    if err != nil {
        log.Fatalln("ca证书os读取失败", err)
    }
    // 4. 将ca加到 CertPool 中
    ok := certPool.AppendCertsFromPEM(ca)

    if !ok {
        log.Fatalln("添加ca证书失败!")
    }
    // 5. 构建基于 TLS 的 TransportCredentials 选项
    creds := credentials.NewTLS(&tls.Config{
        Certificates: []tls.Certificate{cert},
        // 此ServerName需为注册证书时配置的DNS
        ServerName: `fanfine.cn`,
        RootCAs:    certPool,
    })
    return creds
}
  1. 修改启动服务时使用的 cred
相关报错
panic: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: tls: first record does not look like a TLS handshake"

表示在 gRPC 连接时,TLS 握手失败。通常是由于客户端和服务器的配置不匹配,或者服务器并未使用 TLS 协议,而客户端尝试使用 TLS 进行连接。
具体原因可能有以下几种情况:

  1. 服务器未启用 TLS,但客户端启用了 TLS
  2. 证书配置错误
  3. 端口配置问题
  4. 防火墙或代理的影响

出现:

panic: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: tls: failed to verify certificate: x509: certificate signed by unknown authority (possibly because of \"x509: invalid signature: parent certificate cannot sign this kind of certificate\" while trying to verify candidate authority certificate \"fanfine\")"

这个错误提示表示 gRPC 客户端在与服务器建立 TLS 连接时,无法验证服务器的证书,检查证书是否正确。

Auth认证

服务端开启拦截器

拦截器拦截 ctx 中的 authorization 键,校验正确性。
拦截器也可以拦截以验证其他数据,类似于Gin中的中间件的。

拦截器示例:

  1. 支持豁免部分方法,使其不参与相关验证
  2. 验证上下文的元数据中的 authorization ,验证登录情况

// 豁免的方法
var ExemptMethods = map[string]struct{}{
    //pbUser.UserService_GetUserInfo_FullMethodName: {},
}

// auth 拦截器,用于登录验证之类,从metadata中取key
func AuthInterceptor(
    ctx context.Context,
    req any,
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler) (resp any, err error) {

    // 若方法设置了豁免,则无需验证
    if _, ok := ExemptMethods[info.FullMethod]; ok {
        log.Println("被豁免")
        return handler(ctx, req)
    }

    // 提取metadata信息
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, errors.New("missing metadata")
    }
    fmt.Println("收到的metadata是:", md)
    // 提取 authorization
    if val, ok := md["access_token"]; ok {
        token := val[0]
        // 在redis或其他缓存中找到token
        time.Sleep(time.Millisecond) // 模拟查询
        // 校验token的正确性
        if token != "the_true_access_token" {
            return nil, errors.New("invalid access_token")
        }
    } else {
        return nil, errors.New("access_token missing")
    }
    // 校验通过,执行具体逻辑
    return handler(ctx, req)
}

客户端

有两种向ctx的metadata中写入键值对的方式

  1. WithPerRPCCredentials 一种动态Token的方式。

  2. 随用随写的方式,调用前写入ctx的metadata中。

方法一

  • WithPerRPCCredentials

在 NewClient 时,作为凭证option使用,

    GrpcClientConn, err = grpc.NewClient("127.0.0.1:8800",
        grpc.WithTransportCredentials(cred),
        grpc.WithPerRPCCredentials(     // 动态变化Token认证方式 
            OAuthToken{
                AccessToken: "the_true_access_token",
            }), 
    )

PerRPCCredentials 是在grpc库的credentials中的接口,
token 应当在必要时刷新,是的,就比如 Oauth

需要实现该接口:


// PerRpcCred
type OAuthToken struct {
    AccessToken  string
    RefreshToken string
    AppId        string
    Secret       string
}

func (oauthToken OAuthToken) getOrRefresh() {
    // 获取或刷新access_token的逻辑
}
func (oauthToken OAuthToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    // 以 oauth 为例,
    // access_token 需要在必要时刷新
    oauthToken.getOrRefresh()

    return map[string]string{
        "access_token": oauthToken.AccessToken,
    }, nil
}

// RequireTransportSecurity indicates whether the credentials requires
// transport security.
func (cred OAuthToken) RequireTransportSecurity() bool {
    return true
}

方式二

    md := metadata.New(map[string]string{
        "access_token": "the_true_access_token",
    })
    ctx := metadata.NewOutgoingContext(context.Background(), md)
    // 初始化 User 服务的客户端连接
    userClient := pbUser.NewUserServiceClient(app.GrpcClientConn)
    resp, err := userClient.GetUserInfo(ctx, req)
    if err != nil {
        panic(err)
    }
    fmt.Println("得到响应", resp)

链式拦截器

错误示范

在进行auth校验之后,我还想要在handler之后,进行日志记录,或者向日志服务器提交日志。

我尝试再写一个 logger 拦截器


// 日志拦截器,在handler之后记录日志
func LoggerInterceptor(
    ctx context.Context,
    req any,
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler) (resp any, err error) {

    resp, err = handler(ctx, req)

    // 记录日志
    log.Println("这是logger拦截器,在handler之后的操作,FullMethod = ", info.FullMethod)
    return
}

我将该拦截器加入到 grpc.NewServer()

    gServer := grpc.NewServer(
        grpc.Creds(ServerTLSTwoWayAuth()),
        grpc.UnaryInterceptor(AuthInterceptor),
    ) // 使用 <单/双向> 认证,且使用 auth 拦截器验证登录

此时出现报错:

panic: The unary server interceptor was already set and may not be reset.

看起来只能用一个 UnaryInterceptor

正确方式

  1. 单个拦截器,在 handler 的前后,调用auth校验、logger的函数
    也行,不过在业务越来越复杂,引入更多的操作,会越来越臃肿。
  2. 链式拦截器
gServer := grpc.NewServer(
  grpc.Creds(ServerTLSTwoWayAuth()),
  grpc.ChainUnaryInterceptor(
      AuthInterceptor,
      LoggerInterceptor,
  )) // 使用 <单/双> 向认证,且使用链式拦截器

链式拦截器的执行顺序

简单来说,拦截器链按照声明的顺序依次执行,在每个拦截器中调用 handler,最后再依次返回。

假设有三个拦截器 A、B 和 C,它们的调用链设置如下:

    grpc.ChainUnaryInterceptor(A, B, C)
  1. 进入顺序(请求链路):

    • 拦截器 A → B → C → handler
    • 先执行 A 的前置逻辑,传递给 B。
    • 然后执行 B 的前置逻辑,传递给 C。
    • 最后执行 C 的前置逻辑,再调用实际的 handler。

  2. 返回顺序(响应链路):

    • handler 执行完毕后,按 逆序 返回结果:C → B → A。
    • 首先返回到 C,执行 C 的后置逻辑。
    • 然后返回到 B,执行 B 的后置逻辑。
    • 最后返回到 A,执行 A 的后置逻辑,最终将结果返回给客户端。

func A(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler) (interface{}, error) {
    // 前置逻辑
    log.Println("A前")
    // 调用下一个拦截器或 handler
    resp, err := handler(ctx, req)
    // 后置逻辑
    log.Println("A后")
    return resp, err
}

func B(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler) (interface{}, error) {
    log.Println("B前")
    resp, err := handler(ctx, req)
    log.Println("B后")
    return resp, err
}

func C(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler) (interface{}, error) {
    log.Println("C前")
    resp, err := handler(ctx, req)
    log.Println("C后")
    return resp, err
}

server := grpc.NewServer(
    grpc.ChainUnaryInterceptor(A, B, C),
)

本文由 上传。


如果您喜欢这篇文章,请点击链接 Golang gRPC 快速入门 查看原文。


您也可以直接访问:https://www.fanfine.cn/blog/242

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇