Contents

gRPC

1、gRPC Go 简介

gRPC 是 Google 开源的高性能、跨语言的 RPC 框架,基于 HTTP/2 协议和 Protocol Buffers 序列化机制。gRPC Go(grpc-go)是 gRPC 官方提供的 Go 语言实现,广泛用于微服务间通信、移动后端、以及需要高效双向流式传输的场景。

gRPC的主要优势包括:

  • 高性能:gRPC使用HTTP/2作为传输协议,可以实现低延迟、高吞吐量的RPC通信。

  • 跨语言:gRPC支持多种编程语言,包括C++、Java、Python、Go、JavaScript等,可以实现跨语言的RPC通信。

  • 可扩展性:gRPC支持流式通信、压缩、加密等功能,可以根据需要扩展功能。

  • 可靠性:gRPC支持重试、超时、负载均衡等功能,可以保证RPC通信的可靠性。

核心特性:

  • 强类型接口:通过 .proto 文件定义服务契约,自动生成客户端与服务端代码。
  • 四种 RPC 模式:一元 RPC、服务端流、客户端流、双向流。
  • 基于 HTTP/2:支持多路复用、头部压缩、流控。
  • 可插拔拦截器:用于日志、认证、限流、重试等横切关注点。

1.1 一元 RPC(Unary RPC)

一元 RPC(Unary RPC)是 gRPC 中最简单、最常用的通信模式,也是最容易理解的服务调用方式。

核心定义:一元 RPC 是指客户端发送一个请求消息给服务端,服务端处理后返回一个响应消息的通信模式。整个过程是同步的,客户端会等待服务端的响应。

主要特点:

  1. 简单直观:类似于传统的函数调用,输入参数,获得返回值
  2. 同步阻塞:客户端发送请求后会阻塞等待,直到收到服务端响应或超时
  3. 一对一映射:一个请求对应一个响应,不会有多个响应
  4. 无状态:每次调用都是独立的,不维护连接状态

与 HTTP 的相似性: 一元 RPC 的工作方式与 HTTP 请求-响应模式非常相似,但在性能和功能上有所提升:

  • 性能优势:基于 HTTP/2,支持多路复用和头部压缩
  • 类型安全:通过 Protocol Buffers 提供强类型约束
  • 跨语言:自动生成客户端和服务端代码,支持多种编程语言

适用场景(包含但不限于):

  • 用户认证:登录验证、权限检查
  • 数据查询:根据ID查询用户信息、订单详情等
  • 简单操作:创建、更新、删除单个资源
  • 配置获取:获取系统配置、应用设置
  • 状态检查:健康检查、服务状态查询

1.2 服务端流式 RPC(Server Streaming RPC)

服务端流式 RPC(Server Streaming RPC)是 gRPC 提供的第二种通信模式,它允许服务端向客户端持续推送数据流,非常适合实时数据传输场景。

核心定义:服务端流式 RPC 是指客户端发送一个请求消息给服务端,服务端返回一个数据流,在这个流中可以包含多个响应消息。客户端从流中读取消息直到没有更多消息为止。

主要特点:

  1. 一对多通信:一个请求可以对应多个响应消息
  2. 服务端主动推送:服务端可以在适当的时机主动向客户端发送数据
  3. 流式传输:数据以流的形式持续传输,而不是一次性返回
  4. 单向数据流:数据流向是单向的,从服务端流向客户端
  5. 实时性强:支持实时或近实时的数据推送

与传统轮询的对比: 相比于客户端不断轮询获取数据的方式,服务端流式 RPC 具有明显优势:

  • 减少网络开销:避免了频繁的轮询请求
  • 实时性更好:服务端可以立即推送新数据
  • 服务端控制:由服务端决定何时发送数据,更加灵活
  • 连接复用:一个长连接可以传输多次数据

适用场景(包含但不限于):

  • 实时数据推送:股票价格、汇率变化、传感器数据
  • 大数据集传输:分批返回大量查询结果,避免内存溢出
  • 监控和告警:系统监控数据、日志流、告警信息推送
  • 进度通知:文件处理进度、任务执行状态更新
  • 实况更新:体育比赛比分、新闻推送、社交媒体动态

1.3 客户端流式 RPC(Client Streaming RPC)

客户端流式 RPC(Client Streaming RPC)是 gRPC 提供的第三种通信模式,它允许客户端向服务端持续发送数据流,非常适合大数据量传输和批量处理场景。

核心定义:客户端流式 RPC 是指客户端发送一个数据流给服务端,在这个流中可以包含多个请求消息,服务端接收完整个流后返回一个响应消息。

主要特点:

  1. 多对一通信:多个请求消息对应一个响应
  2. 客户端主导:由客户端决定何时发送数据、发送多少数据
  3. 流式上传:数据以流的形式分批传输,避免内存溢出
  4. 单向数据流:数据流向是单向的,从客户端流向服务端
  5. 累积处理:服务端通常需要接收完所有数据后才进行处理

与传统方式的对比: 相比于将所有数据打包成一个大请求的方式,客户端流式 RPC 具有明显优势:

  • 内存友好:数据分批传输,不会占用大量内存
  • 网络高效:避免了超大请求可能导致的网络超时
  • 进度可控:可以实现传输进度监控和断点续传
  • 错误恢复:部分失败时可以只重传失败的部分

适用场景(包含但不限于):

  • 文件上传:大文件分块上传、图片/视频上传
  • 数据导入:批量数据导入数据库、CSV/Excel文件处理
  • 日志收集:将大量日志数据流式发送到中央日志系统
  • 传感器数据:物联网设备持续上传传感器数据
  • 实时分析:将数据流发送给分析引擎进行实时处理

1.4 双向流式 RPC(Bidirectional Streaming RPC)

双向流式 RPC(Bidirectional Streaming RPC)是 gRPC 提供的最强大、最灵活的通信模式,它允许客户端和服务端同时向对方持续发送数据流,实现真正的全双工通信。

核心定义:双向流式 RPC 是指客户端和服务端都可以独立地发送数据流,两个数据流是独立操作的,可以按任意顺序进行读写。

主要特点:

  1. 全双工通信:客户端和服务端可以同时发送和接收数据
  2. 独立数据流:两个方向的数据流是完全独立的,互不影响
  3. 异步处理:发送和接收可以异步进行,无需等待对方响应
  4. 实时交互:支持真正的实时双向交互
  5. 最大灵活性:结合了前三种模式的所有优势

与其他模式的对比: 双向流式 RPC 集合了所有其他模式的优势,同时提供了最大的灵活性:

  • 相比一元 RPC:支持持续的双向交互,而不仅仅是单次请求-响应
  • 相比服务端流:客户端也可以持续发送数据,不仅仅是接收
  • 相比客户端流:服务端可以在接收过程中就开始响应,无需等待全部接收完成
  • 独特优势:两个数据流完全独立,可以实现复杂的交互模式

适用场景(包含但不限于):

  • 实时聊天:用户既要发送消息,也要接收其他用户的消息
  • 在线游戏:玩家操作上传,游戏状态实时同步
  • 协作编辑:多用户实时编辑文档,操作同步和冲突解决
  • 实时数据分析:客户端持续上传数据,服务端实时返回分析结果
  • 视频会议:音视频数据的双向实时传输
  • 交易系统:实时报价推送和交易指令提交

1.5 四种服务类型对比

通过前面四个实战案例的深入分析,我们可以从多个维度对比这四种 gRPC 服务类型:

特性维度 一元 RPC 服务端流式 RPC 客户端流式 RPC 双向流式 RPC
通信模式 1:1 (请求:响应) 1:N (请求:多响应) N:1 (多请求:响应) N:M (多请求:多响应)
数据流向 客户端 → 服务端 → 客户端 客户端 → 服务端 ⇒ 客户端 客户端 ⇒ 服务端 → 客户端 客户端 ⇔ 服务端
实时性 同步阻塞 实时推送 批量处理 实时双向交互
内存占用 低 中等 中等 高
开发复杂度 简单 中等 中等 复杂
并发处理 简单 需要连接管理 需要状态管理 需要复杂连接管理
典型应用 用户查询 实时监控 文件上传 实时聊天

适用场景总结

一元 RPC:最适合传统的请求-响应场景,如用户认证、数据查询、配置获取等。简单直观,性能稳定,是微服务架构中最常用的通信方式。

服务端流式 RPC:完美解决了实时数据推送的问题,如股票行情、监控告警、实况更新等。避免了客户端轮询的开销,提供了优秀的实时性。

客户端流式 RPC:专门为大数据量上传而设计,如文件上传、数据导入、日志收集等。通过分块传输有效解决了大文件传输的内存和网络问题。

双向流式 RPC:提供了最强大的实时交互能力,如在线聊天、协作编辑、在线游戏等。虽然实现复杂,但为复杂的实时交互场景提供了完美的技术基础。

2、开发示例

使用buf 生成代码

# buf.gen.yaml
version: v2
inputs:
  - directory: api
plugins:
  - local: ["go", "run", "google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11"]
    out: api
    opt: paths=source_relative
  - local: ["go", "run", "google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2"]
    out: api
    opt: paths=source_relative


# buf.yaml
version: v2
modules:
  - path: api
  - path: internal
deps:
  - buf.build/googleapis/googleapis

2.1 一元 RPC

// api/hello/v1/hello.proto

syntax = "proto3";
     
package hello.v1;

option go_package = "grpcc/api/hello/v1;v1";

service TodoService {
    rpc Hello (HelloReq) returns (HelloResp) {}
}

message HelloReq {
    string name = 1;
}
message HelloResp {
    string message = 1;
}

服务端

// server/main.go

package main

import (
	v1 "grpcc/api/hello/v1"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

const (
	// Address 监听地址
	Address string = ":9000"
	// Network 网络通信协议
	Network string = "tcp"
)

func main() {
	// 监听本地端口
	listener, err := net.Listen(Network, Address)
	if err != nil {
		log.Fatalf("net.Listen err: %v", err)
	}
	log.Println(Address + " net.Listing...")
	// 新建gRPC服务器实例
	grpcServer := grpc.NewServer()
	// 在gRPC服务器注册我们的服务
	v1.RegisterHelloServiceServer(grpcServer, &HelloServiceServer{})

	reflection.Register(grpcServer)
	//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
	err = grpcServer.Serve(listener)
	if err != nil {
		log.Fatalf("grpcServer.Serve err: %v", err)
	}
}


// server/server.go
package main

import (
	"context"
	v1 "grpcc/api/hello/v1"
)

type HelloServiceServer struct {
	v1.UnimplementedHelloServiceServer
}

func (s *HelloServiceServer) Hello(ctx context.Context, req *v1.HelloReq) (*v1.HelloResp, error) {
	return &v1.HelloResp{
		Message: "Hello, " + req.GetName(),
	}, nil
}

客户端

// client/client.go

package main

import (
	"context"
	v1 "grpcc/api/hello/v1"
	"log"

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

const (
	// Address 连接地址
	Address string = ":9000"
)

func main() {
	// 连接服务器
	conn, err := grpc.NewClient(Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("net.Connect err: %v", err)
	}
	defer conn.Close()

	// 建立gRPC连接
	grpcClient := v1.NewHelloServiceClient(conn)
	// 创建发送结构体
	req := v1.HelloReq{
		Name: "grpc",
	}
	// 调用我们的服务(Route方法)
	// 同时传入了一个 context.Context ,在有需要时可以让我们改变RPC的行为,比如超时/取消一个正在运行的RPC
	res, err := grpcClient.Hello(context.Background(), &req)
	if err != nil {
		log.Fatalf("Call Route err: %v", err)
	}
	// 打印返回值
	log.Println(res)
}

2.2 服务端流式 RPC

// api/hello/v1/stock.proto

syntax = "proto3";
     
package hello.v1;

option go_package = "grpcc/api/hello/v1;v1";

service StockService {
    // (服务端流式 RPC)
    rpc GetStockInfo (StockReq) returns (stream StockResp) {}
}

message StockReq {
    string symbol = 1;
}
message StockResp {
    string name = 1;
    string price = 2;
    string change = 3;
    int64 timestamp = 4;
}

服务端

package main

import (
	"fmt"
	v1 "grpcc/api/hello/v1"
	"math/rand/v2"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type StockServiceServer struct {
	v1.UnimplementedStockServiceServer
}

func (s *StockServiceServer) GetStockInfo(
	req *v1.StockReq,
	stream grpc.ServerStreamingServer[v1.StockResp],
) error {
	if req.GetSymbol() == "" {
		return status.Error(codes.InvalidArgument, "stock symbol is required")
	}

	// 模拟初始股票价格
	price := 100.0

	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-stream.Context().Done():
			return stream.Context().Err()

		case <-ticker.C:
			// 模拟价格波动
			change := (rand.Float64() - 0.5) * 2
			price += change

			resp := &v1.StockResp{

				Price:     fmt.Sprint(price),
				Change:    fmt.Sprint(change),
				Timestamp: time.Now().Unix(),
			}

			if err := stream.Send(resp); err != nil {
				return err
			}
		}
	}
}

客户端

	gClient := v1.NewStockServiceClient(conn)
	stockReq := v1.StockReq{
		Symbol: "AAPL",
	}

	stockStream, err := gClient.GetStockInfo(context.Background(), &stockReq)
	if err != nil {
		log.Fatalf("Call GetStockInfo err: %v", err)
	}

	for {
		stockResp, err := stockStream.Recv()
		if err != nil {
			log.Fatalf("Call GetStockInfo err: %v", err)
		}
		log.Printf("Stock Name: %s, Price: %s, change: %s", stockResp.GetName(), stockResp.GetPrice(), stockResp.GetChange())
	}

3、扩展

gRPC 超时

gRPC默认的请求的超时时间是很长的,当你没有设置请求超时时间时,所有在运行的请求都占用大量资源且可能运行很长的时间,导致服务资源损耗过高,使得后来的请求响应过慢,甚至会引起整个进程崩溃。

// 增加 ctx
clientDeadline := time.Now().Add(time.Duration(3 * time.Second))
	ctx, cancel := context.WithDeadline(ctx, clientDeadline)
	defer cancel()

客户端超时判断

	res, err := grpcClient.Hello(context.Background(), &req)
	if err != nil {
		statu, ok := status.FromError(err)
		if ok {
			//判断是否为调用超时
			if statu.Code() == codes.DeadlineExceeded {
				log.Fatalln("Route timeout!")
			}
		}
		log.Fatalf("Call Route err: %v", err)
	}
	// 打印返回值
	log.Println(res)

服务端判断是否超时

func (s *StockServiceServer) GetStockInfo(ctx context.Context, req *v1.StockReq) (*v1.StockResp, error) {
    // 模拟耗时操作,期间周期性检查超时
    select {
    case <-ctx.Done():
        // 客户端 Deadline 到期或主动取消
        return nil, ctx.Err() // 框架会转换为 codes.DeadlineExceeded / codes.Canceled
    case <-time.After(2 * time.Second):
        // 正常业务处理完成
    }

    return &v1.StockResp{
        Symbol: req.GetSymbol(),
        Price:  100.0,
    }, nil
}

服务端兜底默认超时(拦截器)

func UnaryServerTimeout(timeout time.Duration) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
        if _, ok := ctx.Deadline(); !ok {
            var cancel context.CancelFunc
            ctx, cancel = context.WithTimeout(ctx, timeout)
            defer cancel()
        }
        return handler(ctx, req)
    }
}

// 注意:流式 RPC 不受 UnaryInterceptor 影响,需要单独用 grpc.StreamInterceptor 处理,逻辑类似
grpcServer := grpc.NewServer(
    grpc.UnaryInterceptor(UnaryServerTimeout(5 * time.Second)),
)

TLS认证

# 证书生成
openssl genrsa -out server.key 2048
openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650

func main() {
	// 监听本地端口
	listener, err := net.Listen(Network, Address)
	if err != nil {
		log.Fatalf("net.Listen err: %v", err)
	}
	log.Println(Address + " net.Listing...")
	// 新建gRPC服务器实例

	// 设置证书
	creds, err := credentials.NewServerTLSFromFile("cert/server.crt", "cert/server.key")
	if err != nil {
		log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
	}
	grpcServer := grpc.NewServer(grpc.Creds(creds))
	// 在gRPC服务器注册我们的服务
	v1.RegisterHelloServiceServer(grpcServer, &HelloServiceServer{})

	v1.RegisterStockServiceServer(grpcServer, &StockServiceServer{})

	reflection.Register(grpcServer)
	//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
	err = grpcServer.Serve(listener)
	if err != nil {
		log.Fatalf("grpcServer.Serve err: %v", err)
	}
}

客户端配置证书(客户端认证服务端端)

	creds, err := credentials.NewClientTLSFromFile("cert/server.crt", "grpcc")
	if err != nil {
		log.Fatalf("credentials.NewClientTLSFromFile err: %v", err)
	}
	conn, err := grpc.NewClient(Address, grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatalf("net.Connect err: %v", err)
	}
	defer conn.Close()

双向认证

### 创建根 CA
mkdir -p certs
cd certs

# 生成 CA 私钥
openssl genrsa -out ca.key 4096

# 生成 CA 根证书
openssl req -x509 -new -nodes \
  -key ca.key \
  -sha256 \
  -days 3650 \
  -out ca.crt \
  -subj "/CN=My gRPC Root CA"
  
  
### 生成服务端证书
openssl genrsa -out server.key 2048

openssl req -new \
  -key server.key \
  -out server.csr \
  -subj "/CN=localhost"
  
# 创建 server.ext
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:localhost,IP:127.0.0.1

# 签发
openssl x509 -req \
  -in server.csr \
  -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -out server.crt \
  -days 825 \
  -sha256 \
  -extfile server.ext
  
  
# 生成客户端证书
openssl genrsa -out client.key 2048

openssl req -new \
  -key client.key \
  -out client.csr \
  -subj "/CN=grpc-client"
  
# 创建 client.ext
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=clientAuth

# 签发
openssl x509 -req \
  -in client.csr \
  -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -out client.crt \
  -days 825 \
  -sha256 \
  -extfile client.ext
  
  
# 检查证书
openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile ca.crt client.crt

证书使用

package security

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"os"

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

// ServerTLS 服务端双向 TLS 配置
func ServerTLS(
	certFile string,
	keyFile string,
	caFile string,
) (credentials.TransportCredentials, error) {
	// 加载服务端证书和私钥
	serverCert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return nil, fmt.Errorf("load server certificate: %w", err)
	}

	// 加载可信 CA
	caPEM, err := os.ReadFile(caFile)
	if err != nil {
		return nil, fmt.Errorf("read CA certificate: %w", err)
	}

	clientCAs := x509.NewCertPool()
	if !clientCAs.AppendCertsFromPEM(caPEM) {
		return nil, fmt.Errorf("invalid client CA certificate")
	}

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{serverCert},

		// 必须提供客户端证书,并且验证证书链
		ClientAuth: tls.RequireAndVerifyClientCert,
		ClientCAs:  clientCAs,

		MinVersion: tls.VersionTLS12,
	}

	return credentials.NewTLS(tlsConfig), nil
}

// ClientTLS 客户端双向 TLS 配置
func ClientTLS(
	certFile string,
	keyFile string,
	caFile string,
	serverName string,
) (credentials.TransportCredentials, error) {
	// 加载客户端证书和私钥
	clientCert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return nil, fmt.Errorf("load client certificate: %w", err)
	}

	// 加载服务端 CA
	caPEM, err := os.ReadFile(caFile)
	if err != nil {
		return nil, fmt.Errorf("read CA certificate: %w", err)
	}

	roots := x509.NewCertPool()
	if !roots.AppendCertsFromPEM(caPEM) {
		return nil, fmt.Errorf("invalid server CA certificate")
	}

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{clientCert},

		// 验证服务端证书
		RootCAs: roots,

		// 验证服务端证书 SAN 中的 DNS/IP
		ServerName: serverName,

		MinVersion: tls.VersionTLS12,
	}

	return credentials.NewTLS(tlsConfig), nil
}

grpc中间件

go-grpc-middleware简介

go-grpc-middleware封装了认证(auth), 日志( logging), 消息(message), 验证(validation), 重试(retries) 和监控(retries)等拦截器。

import "github.com/grpc-ecosystem/go-grpc-middleware"
myServer := grpc.NewServer(
    // 流式RPC的拦截器
    grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
        grpc_ctxtags.StreamServerInterceptor(),
        grpc_opentracing.StreamServerInterceptor(),
        grpc_prometheus.StreamServerInterceptor,
        grpc_zap.StreamServerInterceptor(zapLogger),
        grpc_auth.StreamServerInterceptor(myAuthFunction),
        grpc_recovery.StreamServerInterceptor(),
    )),
    // 简单RPC的拦截器
    grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
        grpc_ctxtags.UnaryServerInterceptor(),
        grpc_opentracing.UnaryServerInterceptor(),
        grpc_prometheus.UnaryServerInterceptor,
        grpc_zap.UnaryServerInterceptor(zapLogger),
        grpc_auth.UnaryServerInterceptor(myAuthFunction),
        grpc_recovery.UnaryServerInterceptor(),
    )),
)

metadata

是一个键值对列表,key必须是ASCII 字符串,value可以是二进制数据(通常编码为 ASCII 字符串)或普通字符串,可以理解为类似http多header,Metadata 的 Key 是大小写不敏感的,传输过程中通常会被自动转换为小写

限制:自定义的metdadata不能以grpc-开头

package main

import (
	"context"
	"fmt"

	"google.golang.org/grpc/metadata"
)

func main() {
	ctx := context.Background()
	// metadata key
	MDKey := "name"
	// 设置metadata数据
	metaData := map[string]string{
		MDKey: "jerry",
	}
	md := metadata.New(metaData)
	ctx = metadata.NewOutgoingContext(ctx, md)

	// 从ctx中获取metadata, get方法返回到是个slice,取第一个即可
	data := ""
	if md, ok := metadata.FromIncomingContext(ctx); ok {
		names := md.Get(MDKey)
		if len(names) > 0 {
			data = names[0]
		}
	}

	fmt.Printf("name: %v", data)
}