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 是指客户端发送一个请求消息给服务端,服务端处理后返回一个响应消息的通信模式。整个过程是同步的,客户端会等待服务端的响应。
主要特点:
- 简单直观:类似于传统的函数调用,输入参数,获得返回值
- 同步阻塞:客户端发送请求后会阻塞等待,直到收到服务端响应或超时
- 一对一映射:一个请求对应一个响应,不会有多个响应
- 无状态:每次调用都是独立的,不维护连接状态
与 HTTP 的相似性: 一元 RPC 的工作方式与 HTTP 请求-响应模式非常相似,但在性能和功能上有所提升:
- 性能优势:基于 HTTP/2,支持多路复用和头部压缩
- 类型安全:通过 Protocol Buffers 提供强类型约束
- 跨语言:自动生成客户端和服务端代码,支持多种编程语言
适用场景(包含但不限于):
- 用户认证:登录验证、权限检查
- 数据查询:根据ID查询用户信息、订单详情等
- 简单操作:创建、更新、删除单个资源
- 配置获取:获取系统配置、应用设置
- 状态检查:健康检查、服务状态查询
1.2 服务端流式 RPC(Server Streaming RPC)
服务端流式 RPC(Server Streaming RPC)是 gRPC 提供的第二种通信模式,它允许服务端向客户端持续推送数据流,非常适合实时数据传输场景。
核心定义:服务端流式 RPC 是指客户端发送一个请求消息给服务端,服务端返回一个数据流,在这个流中可以包含多个响应消息。客户端从流中读取消息直到没有更多消息为止。
主要特点:
- 一对多通信:一个请求可以对应多个响应消息
- 服务端主动推送:服务端可以在适当的时机主动向客户端发送数据
- 流式传输:数据以流的形式持续传输,而不是一次性返回
- 单向数据流:数据流向是单向的,从服务端流向客户端
- 实时性强:支持实时或近实时的数据推送
与传统轮询的对比: 相比于客户端不断轮询获取数据的方式,服务端流式 RPC 具有明显优势:
- 减少网络开销:避免了频繁的轮询请求
- 实时性更好:服务端可以立即推送新数据
- 服务端控制:由服务端决定何时发送数据,更加灵活
- 连接复用:一个长连接可以传输多次数据
适用场景(包含但不限于):
- 实时数据推送:股票价格、汇率变化、传感器数据
- 大数据集传输:分批返回大量查询结果,避免内存溢出
- 监控和告警:系统监控数据、日志流、告警信息推送
- 进度通知:文件处理进度、任务执行状态更新
- 实况更新:体育比赛比分、新闻推送、社交媒体动态
1.3 客户端流式 RPC(Client Streaming RPC)
客户端流式 RPC(Client Streaming RPC)是 gRPC 提供的第三种通信模式,它允许客户端向服务端持续发送数据流,非常适合大数据量传输和批量处理场景。
核心定义:客户端流式 RPC 是指客户端发送一个数据流给服务端,在这个流中可以包含多个请求消息,服务端接收完整个流后返回一个响应消息。
主要特点:
- 多对一通信:多个请求消息对应一个响应
- 客户端主导:由客户端决定何时发送数据、发送多少数据
- 流式上传:数据以流的形式分批传输,避免内存溢出
- 单向数据流:数据流向是单向的,从客户端流向服务端
- 累积处理:服务端通常需要接收完所有数据后才进行处理
与传统方式的对比: 相比于将所有数据打包成一个大请求的方式,客户端流式 RPC 具有明显优势:
- 内存友好:数据分批传输,不会占用大量内存
- 网络高效:避免了超大请求可能导致的网络超时
- 进度可控:可以实现传输进度监控和断点续传
- 错误恢复:部分失败时可以只重传失败的部分
适用场景(包含但不限于):
- 文件上传:大文件分块上传、图片/视频上传
- 数据导入:批量数据导入数据库、CSV/Excel文件处理
- 日志收集:将大量日志数据流式发送到中央日志系统
- 传感器数据:物联网设备持续上传传感器数据
- 实时分析:将数据流发送给分析引擎进行实时处理
1.4 双向流式 RPC(Bidirectional Streaming RPC)
双向流式 RPC(Bidirectional Streaming RPC)是 gRPC 提供的最强大、最灵活的通信模式,它允许客户端和服务端同时向对方持续发送数据流,实现真正的全双工通信。
核心定义:双向流式 RPC 是指客户端和服务端都可以独立地发送数据流,两个数据流是独立操作的,可以按任意顺序进行读写。
主要特点:
- 全双工通信:客户端和服务端可以同时发送和接收数据
- 独立数据流:两个方向的数据流是完全独立的,互不影响
- 异步处理:发送和接收可以异步进行,无需等待对方响应
- 实时交互:支持真正的实时双向交互
- 最大灵活性:结合了前三种模式的所有优势
与其他模式的对比: 双向流式 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)
}