grpc
2026年1月14日 · 734 字 · 4 分钟
讲解了grpc的简单实用,包括protoc,以及grpc的客户端,服务端,拦截器
grpc
grpc和http是对等的,都是属于应用层的方面
grpc通过protobuffer进行序列化,传输采用多路复用(适用于高并发的场景)
ps:这篇博客我代码的路径写的好烂,观看可能不是很好,但是懒得改了,凑活看吧(
proto语法与protoc工具
基本语法结构
syntax = "proto3"; //采用protubuffer v3版本的语法编写
package idl.student; //包名可以包含.但是不能有. 其他proto引用此proto时需要指定package,对成的go代码没有影响
option go_package = "idl/student;grpc_student"; //指定对成的go代码,分号前为go文件生成的路径(相对于go_out),分号后为包名
message student{//等同于go的结构体,转换为go代码会变成驼峰的形式
string name = 2; //2是字段编号,每个字段编号不能重复,不能为0
int64 id = 1;
repeated string hobbies = 3; //repeated表示list,对用go的切片
map<string,float> scores = 4; //map表示map,对用go的map
}
核心概念
| 语法元素 | 说明 |
|---|---|
syntax = "proto3" |
指定使用 Protocol Buffers v3 版本语法 |
package |
命名空间,用于避免消息类型命名冲突 |
option |
编译器选项,影响代码生成行为 |
message |
定义数据结构,等同于 Go 的 struct |
repeated |
表示数组/切片类型 |
map |
表示字典类型 |
protoc 编译命令
生成前的目录结构:
web/
└── grpcs/
└── student.proto
cd /web
protoc --go_out=./grpcs --proto_path=. ./grpcs/student.proto
| 参数 | 说明 |
|---|---|
--go_out |
指定生成的 Go 代码输出目录 |
--proto_path |
指定 proto 文件搜索目录(可简写为 -I) |
生成后的目录结构:
web/
├── grpcs/
│ ├── student.proto
│ └── idl/
│ └── student/
│ └── student.pb.go
高级protoc命令选项
场景:导入其他proto文件并生成gRPC服务代码
当我们需要在一个 proto 文件中导入另一个 proto 文件,并生成 gRPC 服务端/客户端代码时,需要使用更多的 protoc 选项。
目录结构示例
生成前的目录结构:
web/
└── grpcs/
├── student.proto # 基础消息定义
└── idl/
└── service/
└── student_service.proto # 服务定义(import student.proto)
proto 文件示例
student.proto (基础消息):
syntax = "proto3";
package idl.student;
option go_package = "idl/student;grpc_student";
message student {
string name = 1;
int64 id = 2;
}
student_service.proto (服务定义):
syntax = "proto3";
package service.student;
option go_package = "idl/service/student;grpc_service_student";
import "grpcs/student.proto"; // 导入其他 proto 文件
message QueryStudentRequest {
int64 Id = 1;
string name = 2;
}
message QueryStudentResponse {
repeated idl.student.student Students = 1; // 使用导入的消息类型
}
service student {
// Unary RPC - 一元调用
rpc QueryStudent(QueryStudentRequest) returns (QueryStudentResponse);
// Server streaming RPC - 服务端流式
rpc QueryStudents2(StudentIds) returns (stream idl.student.student);
// Client streaming RPC - 客户端流式
rpc QueryStudents3(stream StudentId) returns (QueryStudentResponse);
// Bidirectional streaming RPC - 双向流式
rpc QueryStudents4(stream StudentId) returns (stream idl.student.student);
}
编译命令
在 web/ 目录下执行:
protoc \
--go_out=./grpcs \
--go-grpc_out=./grpcs \
--proto_path=. \
--go_opt=Mgrpcs/student.proto=goStudy/web/grpcs/idl/student \
--go-grpc_opt=Mgrpcs/student.proto=goStudy/web/grpcs/idl/student \
./grpcs/idl/service/student_service.proto
参数详解
| 参数 | 说明 |
|---|---|
--go_out |
指定生成 .pb.go 文件(消息定义)的输出目录,与option分号前组合 |
--go-grpc_out |
指定生成 _grpc.pb.go 文件(gRPC 服务)的输出目录,与option分号前组合 |
--proto_path |
proto 文件的搜索根路径,可指定多个。import 语句会基于这些路径查找 |
--go_opt=M<proto路径>=<Go模块路径> |
修改导入的 proto 文件在生成的 Go 代码中的 import 路径映射 |
--go-grpc_opt=M<proto路径>=<Go模块路径> |
与 --go_opt 类似,但用于 gRPC 生成器 |
关键注意事项
-
proto_path 的作用
import "grpcs/student.proto"会在--proto_path指定的目录下查找- 如果
--proto_path=.,则会在当前目录下查找./grpcs/student.proto
-
路径映射的关键点
M后面必须是 proto 文件在 import 中使用的完整路径- ❌ 错误:
Mstudent.proto=...(仅文件名) - ✅ 正确:
Mgrpcs/student.proto=...(import 中的完整路径)
-
Go 模块路径映射
- 如果你的 Go 模块名是
goStudy - proto 文件的
go_package是idl/student - 则完整路径应该是:
goStudy/web/grpcs/idl/student
- 如果你的 Go 模块名是
生成后的目录结构
web/
└── grpcs/
├── student.proto
└── idl/
├── student/
│ └── student.pb.go # 消息定义代码
└── service/
├── student_service.proto
└── student/
├── student_service.pb.go # 消息定义代码
└── student_service_grpc.pb.go # gRPC 服务代码(接口定义)
grpc服务端
查看我们刚刚生成的文件
type StudentServer interface {
// Unary RPC
QueryStudent(context.Context, *QueryStudentRequest) (*QueryStudentResponse, error)
QueryStudents1(context.Context, *StudentIds) (*QueryStudentResponse, error)
// Server streaming RPC
QueryStudents2(*StudentIds, grpc.ServerStreamingServer[student.Student]) error
// Client streaming RPC
QueryStudents3(grpc.ClientStreamingServer[StudentId, QueryStudentResponse]) error
// Bidirectional streaming RPC
QueryStudents4(grpc.BidiStreamingServer[StudentId, student.Student]) error
mustEmbedUnimplementedStudentServer()
}
作为服务端,我们需要完成刚刚生成的接口中的函数
import (
"context"
"fmt"
grpc_service "goStudy/https/grpcs/idl/service/student"
gprc_model "goStudy/https/grpcs/idl/student"
)
type Student struct {
grpc_service.UnimplementedStudentServer
}
func (s *Student) QueryStudent(ctx context.Context, req *grpc_service.QueryStudentRequest) (*grpc_service.QueryStudentResponse, error) {
fmt.Println("QueryStudent", req)
resp := &grpc_service.QueryStudentResponse{
Students: []*gprc_model.Student{
{
Name: "John Doe",
Id: 1,
},
},
}
return resp, nil
}
运行
import (
grpc_service "goStudy/web/grpcs/idl/service/student"
"log"
"net"
"google.golang.org/grpc"
)
func main() {
lis, err := net.Listen("tcp", "127.0.0.1:50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
server := grpc.NewServer()
//注册实现
grpc_service.RegisterStudentServer(server, &Student{})
server.Serve(lis)
}
grpc客户端
这里开了3个协程调用
import (
"context"
"fmt"
"log"
grpc_service "goStudy/https/grpcs/idl/service/student"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
coon, err := grpc.NewClient("127.0.0.1:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()), //必须加这一行,要不然报错
//可以加一些全局选项
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024)), //设置最大接收消息大小为1kb
grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(1024)), //设置最大发送消息大小为1kb
)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer coon.Close()
studentClient := grpc_service.NewStudentClient(coon)
//开启三个协程,每个协程都调用一次QueryStudent,演示grpc的多路复用
done := make(chan interface{}, 3)
for i := 0; i < 3; i++ {
go func() {
ctx := context.Background()
resp, err := studentClient.QueryStudent(ctx, &grpc_service.QueryStudentRequest{
Id: 1,
},
//设置其他内容
grpc.MaxCallRecvMsgSize(1024), //设置最大接收消息大小为1kb
)
if err != nil {
log.Fatalf("failed to query student: %v", err)
}
fmt.Println(resp)
done <- struct{}{} //完成了一个请求
}()
}
for range 3 {
<-done //阻塞,直到收到3个请求完成
}
fmt.Println("所有请求完成")
}
拦截器
有点类似于中间件
服务端
func timer(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
begin := time.Now()
resp, err = handler(ctx, req) //指的具体的接口实现
elapsed := time.Since(begin)
fmt.Printf("method %s took %s\n", info.FullMethod, elapsed)
return
}
之后调用
这里演示普通的拦截器,其实也可以链式调用拦截器,在客户端演示
server := grpc.NewServer(
grpc.UnaryInterceptor(timer),
)
客户端
func timer(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
begin := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
elapsed := time.Since(begin)
fmt.Printf("method %s took %s\n", method, elapsed)
return err
}
调用
coon, err := grpc.NewClient("127.0.0.1:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()), //必须加这一行,要不然报错
grpc.WithUnaryInterceptor(timer), //调用拦截器
grpc.WithChainUnaryInterceptor(timer, timer), //链式调用拦截器
)
因为普通调用拦截器一次,链式调用2次,一共有3个协程,所以将会打印9次耗时日志
method /service.student.student/QueryStudent took 2.682709ms
method /service.student.student/QueryStudent took 2.691791ms
method /service.student.student/QueryStudent took 2.694834ms
method /service.student.student/QueryStudent took 2.671666ms
method /service.student.student/QueryStudent took 2.687ms
method /service.student.student/QueryStudent took 3.146792ms
Students:{name:"John Doe" id:1}
Students:{name:"John Doe" id:1}
method /service.student.student/QueryStudent took 2.518333ms
method /service.student.student/QueryStudent took 2.549041ms
method /service.student.student/QueryStudent took 2.553334ms
Students:{name:"John Doe" id:1}
所有请求完成