RPC
是什么
目的:实现类似远程,跨内存空间的函数/方法调用
怎么实现呢?
| 问题 | 解决 |
|---|---|
| 怎么找到要执行的函数在内存中的位置 | 调用方和被调用方都需要维护一个{ function <-> ID }映射表,以确保调用正确的函数。 |
| 怎么传递参数:本地调用时通过堆内存 | 参数和返回值在传输期间序列化并转换为字节流 |
| 怎么进行网络传输 | function ID 和序列化字节流需要通过网络传输,因此,只要能够完成传输,调用方和被调用方就不受某个网络协议的限制 |
和API的区别
| RPC | API | |
|---|---|---|
| 调用 | 像调用本地方法一样 | 调用接口 |
| 性能 | 大多是TCP,性能更好 | HTTP协议 |
| 用途 | 微服务间调用 | 前后端交互 |
原理
gRPC和Protocol Buffer
定义
gRPC
一款rpc框架,能够运行在任意环境(跨语言) ,使用HTTP/2作为传输协议。gRPC是基于定义一个服务,指定一个可以远程调用的带有参数和返回类型的的方法。
优点:可以在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端
protocol buffer
Protocol Buffer是一个IDL库,IDL是接口描述语言,通过中立的语言,使不同语言编写的程序可以互相通信交流。语言无关、平台无关、可扩展的用于序列化结构化数据。
优点:
- 序列化高效,
- IDL简单,
- 容易进行接口更新。
一般都是两者结合使用。 也可以根据需要替换别的IDL库。
需要下载的包
grpc
go get google.golang.org/grpc@latest
protoc
protocol buffer语言的编译器
下载地址: github.com/google/prot…
bin目录添加到环境变量
插件
注意 go install 会把.exe下载到GOPATH/bin中,需要设置环境变量才能查看--version
生成Protocol Buffer go语言代码的插件:
go install google.golang.org/protobuf/cmd/protoc-gen-go@版本
该插件会根据.proto文件生成一个后缀为.pb.go的文件,包含所有.proto文件中定义的类型及其序列化方法
使用protocol buffer && grpc的插件:
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@版本
该插件会生成一个后缀为_grpc.pb.go的文件,其中包含:
- 一种接口类型(或存根) ,供客户端调用的服务方法。
- 服务器要实现的接口类型。
怎么使用gRPC
1. 编写 .proto
// 版本声明,使用Protocol Buffers v3版本
syntax = "proto3";
// 项目中要import生成的go代码 import的路径
// 包名也可以在命令行指定
option go_package = "hello_client/pb";
// 包名
package pb;
// 定义服务
service Greeter {
// SayHello 方法
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
// 请求消息
message HelloRequest {
string name = 1; //1是唯一序号,字段序号
}
// 响应消息
message HelloResponse {
string reply = 1;
}
2. 生成代码
客户端和服务端的option go_package需要改变
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/hello.proto
-
protoc这是 Protocol Buffers 的编译器,用于将.proto文件编译成目标语言的代码。 -
--go_out=.指定生成的 Go 代码的输出目录。这里的.表示当前目录。如果想要指定包名:
protoc --proto_path=src --go_opt=Mprotos/buzz.proto=example.com/project/protos/fizz --go_opt=Mprotos/bar.proto=example.com/project/protos/foo protos/buzz.proto protos/bar.proto -
--go_opt=paths=source_relative指定生成的 Go 文件的路径规则。paths=source_relative表示生成的 Go 文件的路径会相对于.proto文件的路径。常用paths=import:输出文件放在以 Go 包的导入路径命名的目录中。例如,protos/buzz.proto文件中带有example.com/project/protos/fizz的导入路径,则输出的生成文件会保存在example.com/project/protos/fizz/buzz.pb.go。如果未指定路径标志,这就是默认输出模式。
-
--go-grpc_out=.指定生成的 gRPC 相关 Go 代码的输出目录。这里的.表示当前目录。 -
--go-grpc_opt=paths=source_relative指定生成的 gRPC 相关 Go 文件的路径规则。paths=source_relative表示生成的 gRPC 文件的路径会相对于.proto文件的路径。 -
pb/hello.proto这是输入的.proto文件路径,定义了 Protocol Buffers 消息格式和 gRPC 服务。 -
--proto_path=src/ -I从src目录下读取pb/hello.proto文件
生成的文件:
hello.pb.go包含由hello.proto文件生成的 Go 代码,定义了消息结构体及其序列化/反序列化方法。hello_grpc.pb.go包含由hello.proto文件生成的 gRPC 服务代码,定义了客户端和服务端的接口。 主要看.proto有没有service
编译
Makefile
.PHONY: gen help
PROTO_DIR=proto
gen:
protoc \
--proto_path=$(PROTO_DIR) \
--go_out=$(PROTO_DIR) \
--go_opt=paths=source_relative \
--go-grpc_out=$(PROTO_DIR) \
--go-grpc_opt=paths=source_relative \
--grpc-gateway_out=$(PROTO_DIR) \
--grpc-gateway_opt=paths=source_relative \
$(shell find $(PROTO_DIR) -iname "*.proto")
help:
@echo "make gen - 生成pb及grpc代码"
3. 编写业务代码
服务端:
package main
import (
"context"
"fmt"
"net"
"hello_server/pb"
"google.golang.org/grpc"
)
// 支持 grpc
type server struct {
// 在 _grpc.pb.go中定义的
/** 没实现会报错
func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
*/
pb.UnimplementedGreeterServer
}
// SayHello 对外提供的服务,需要实现
// HelloRequest 是在.pb.go中定义的
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
reply := "hello " + in.GetName()
return &pb.HelloResponse{
Reply: reply,
}, nil
}
func main() {
l, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Println("failed to listen:", err)
return
}
// 创建grpc服务
s := grpc.NewServer()
// 注册服务
pb.RegisterGreeterServer(s, &server{})
// 启动服务
if err := s.Serve(l); err != nil {
fmt.Println("failed to serve:", err)
}
}
客户端:
package main
import (
"context"
"flag"
"log"
"time"
"hello_client/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
name:=flag.String("name","","设置name")
flag.Parse()
// 无认证的方式连接
conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer conn.Close()
// 创建客户端
c := pb.NewGreeterClient(conn)
// 调用SayHello方法
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", resp.GetReply())
}
Proto bufferv3的语法
Protocol Buffers V3中文语法指南翻译 | 李文周的博客
和v2的区别
去掉了optional,required可选字段
消息
// 必须指定语法版本,否则使用2 必须是第一行
syntax = "proto3";
// 可以定义多个消息
message SearchRequest {
// 右边的序号是唯一编号
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
序号
为了保证高效性,在传输的时候(消息二进制格式),并没有传输字段名,是通过编号确定的,使用后就不能再更改。
最好使用1-15 占一个字节 16-2047需要两个字节。
如果要删除某个字段,直接删除序号进行重写,有人下载了旧版本的.proto文件,会导致数据损坏,隐私漏洞等。因此要保留字段:
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
字段规则
singular: 格式正确的消息可以有这个字段的零个或一个(但不能多于一个)。这是 proto3语法的默认字段规则。repeated: 该字段可以在格式正确的消息中重复任意次数(包括零次)。重复值的顺序将被保留。 类似数组。
标量值类型
| .protp Type | Go Type | 说明 |
|---|---|---|
| double | float64 | |
| float | float 32 | |
| int32 | int32 | 可变长度编码 |
| int64 | int64 | 可变长度编码 |
| fixed32 | uint32 | 总是使用4个字节 |
| fixed64 | 总是使用八个字节 | |
| bool | ||
| string | 字符串必须始终包含 UTF-8编码的或7位 ASCII 文本,且不能长于232。 | |
| bytes | []byte | 可以包含任何不超过232字节的任意字节序列。 |
枚举
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0; // 第一个常量必须映射为0,枚举类的默认值
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
// 内部定义,可以通过SearchRequest.Corpus取出来
message MyMessage1 {
enum EnumAllowingAlias {
// 常数范围必须在32位整数内,传输过程中变长编码,不推荐使用负数
// 两个数值重复,要设置allow_alias,不然编译出错
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
使用其他消息类型
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
也可以导入
import "myproject/other_protos.proto";
Any
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
oneof
message SampleMessage {
// 只能使用其中的一个,所有字段共享内存
// oneof中不能写map字段和repeated字段
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
示例
message NoticeReaderRequest{
string msg = 1;
oneof notice_way{
string email = 2;
string phone = 3;
}
}
生成的.pb.go
type NoticeReaderRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Msg string `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"`
// Types that are assignable to NoticeWay:
//
// *NoticeReaderRequest_Email
// *NoticeReaderRequest_Phone
NoticeWay isNoticeReaderRequest_NoticeWay `protobuf_oneof:"notice_way"`
}
type isNoticeReaderRequest_NoticeWay interface {
isNoticeReaderRequest_NoticeWay()
}
type NoticeReaderRequest_Email struct {
Email string `protobuf:"bytes,2,opt,name=email,proto3,oneof"`
}
type NoticeReaderRequest_Phone struct {
Phone string `protobuf:"bytes,3,opt,name=phone,proto3,oneof"`
}
func (*NoticeReaderRequest_Email) isNoticeReaderRequest_NoticeWay() {}
func (*NoticeReaderRequest_Phone) isNoticeReaderRequest_NoticeWay() {}
客户端代码
req1:=&api.NoticeReaderRequest{
Msg: "notice",
NoticeWay: &api.NoticeReaderRequest_Email{
Email: "111@163.com",
},
}
req2:=&api.NoticeReaderRequest{
Msg: "notice",
NoticeWay: &api.NoticeReaderRequest_Phone{
Phone: "111",
},
}
服务端代码
// ... liwenzhou.com ...
// 根据`NoticeWay`的不同而执行不同的操作 类型断言
switch v := noticeReq.NoticeWay.(type) {
case *proto.NoticeReaderRequest_Email:
noticeWithEmail(v)
case *proto.NoticeReaderRequest_Phone:
noticeWithPhone(v)
}
// 发送通知相关的功能函数
func noticeWithEmail(in *proto.NoticeReaderRequest_Email) {
fmt.Printf("notice reader by email:%v\n", in.Email)
}
func noticeWithPhone(in *proto.NoticeReaderRequest_Phone) {
fmt.Printf("notice reader by phone:%v\n", in.Phone)
}
map
map<key_type, value_type> map_field = N;
- 映射字段不能重复。
- 当为
.proto生成文本格式时,映射按键排序。数字键按数字排序
WrapValue
例如int64的默认值为0,因此在服务端接收到0值,并不知道它是被赋值为0,还是未赋值。
import "google/protobuf/wrappers.proto";
message Book{
string name = 1;
string autor = 2;
google.protobuf.Int64Value price = 3;
google.protobuf.StringValue desc = 4;
}
实现的效果就是把类似与int64的类型包装在结构体中,这样就可以通过是否为空判断经过赋值。
生成的.pb.go
Price *wrapperspb.Int64Value `protobuf:"bytes,3,opt,name=price,proto3" json:"price,omitempty"`
Desc *wrapperspb.StringValue `protobuf:"bytes,4,opt,name=desc,proto3" json:"desc,omitempty"`
客户端代码:
import "google.golang.org/protobuf/types/known/wrapperspb"
book := proto.Book{
Title: "《跟七米学Go语言》",
Price: &wrapperspb.Int64Value{Value: 9900},
}
服务端进行判断:
if book.GetPrice() == nil { // price没赋值
fmt.Println("book with no price")
} else {
fmt.Printf("book with price:%v\n", book.GetPrice().GetValue())
}
optional
v3.15.0 版本之后
protobuf中使用oneof、WrapValue和FieldMask | 李文周的博客
FieldMask 增量更新
message UpdateBookRequest {
// 操作人
string op = 1;
// 要更新的书籍信息
Book book = 2;
// 要更新的字段 (price)
google.protobuf.FieldMask update_mask = 3;
}
//客户端
import "google.golang.org/protobuf/types/known/fieldmaskpb"
// 记录更新的字段路径,因为要更新的字段可能是嵌套在message的message中的
paths := []string{"price"}
updateReq := proto.UpdateBookRequest{
Book: &proto.Book{
Title: "《跟七米学Go语言》",
Read: true,
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: paths},
}
//服务端
import "github.com/golang/protobuf/protoc-gen-go/generator"
import fieldmask_utils "github.com/mennanov/fieldmask-utils"
// 取出mask 就是要改变的
mask, _ := fieldmask_utils.MaskFromProtoFieldMask(updateReq.UpdateMask, generator.CamelCase)
var bookDst = make(map[string]interface{})
// 将数据读取到map[string]interface{}
// fieldmask-utils支持读取到结构体等,更多用法可查看文档。
fieldmask_utils.StructToMap(mask, updateReq.Book, bookDst)
// do update with bookDst
fmt.Printf("bookDst:%#v\n", bookDst)
package
// demo/proto/book/price.proto
syntax = "proto3";
package book;
// 声明生成Go代码的导入路径(import path)
option go_package = "github.com/Q1mi/demo/proto/book";
message Price {
int64 market_price = 1; // 建议使用下划线的命名方式
int64 sale_price = 2;
}
// demo/proto/book/book.proto
syntax = "proto3";
// 声明protobuf中的包名
package book;
// 声明生成的Go代码的导入路径
option go_package = "github.com/Q1mi/demo/proto/book";
// 引入同目录下的protobuf文件(注意起始位置为proto_path的下层)
import "book/price.proto";
// 引入其他目录下的protobuf文件
import "author/author.proto";
// 引入google/protobuf/timestamp.proto文件
import "google/protobuf/timestamp.proto";
message Book {
string title = 1;
// 这里属于一个包book下,所以直接写Price(不需要写成book.Price)
Price price = 2;
author.Info authorInfo = 3; // 需要带package前缀
// Timestamp是大写T!大写T!大写T!
google.protobuf.Timestamp date = 4; // 注意包名前缀
}
// demo/proto/author/author.proto
syntax = "proto3";
// 声明protobuf中的包名
package author;
// 声明生成的Go代码的导入路径
option go_package = "github.com/Q1mi/demo/proto/author";
message Info {
string name = 1;
}
生成代码:
protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto
服务
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
流式RPC
服务端流式
客户端发出一个RPC请求,服务端与客户端之间建立一个单向的流,服务端可以向流中写入多个响应消息,最后主动关闭流;而客户端需要监听这个流,不断获取响应直到流关闭。
// 定义服务
service Greeter {
// 服务端流式
rpc LotsOfReplies (HelloRequest) returns (stream HelloResponse) {}
}
server同样需要实现接口中的方法
// LotsOfReplies 服务端流式的接口,多种语言打招呼
func (s *server) LotsOfReplies(in *pb.HelloRequest, stream pb.Greeter_LotsOfRepliesServer) error {
words := []string{
"hello",
"你好",
"こんにちは",
"안녕하세요",
}
for _, word := range words {
data := &pb.HelloResponse{
Reply: word + in.GetName(),
}
if err := stream.Send(data); err != nil {
return err
}
}
return nil
}
client:
// 调用LotsOfReplies方法(流式rpc)
stream, err := c.LotsOfReplies(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
for {
msg, err := stream.Recv()
if err == io.EOF{
break
}
if err != nil {
log.Fatalf("stream.Recv error: %v", err)
}
log.Printf("Greeting_stream: %s", msg.GetReply())
}
客户端流式
客户端传入多个请求对象,服务端返回一个响应结果。
service Greeter {
// 客户端流式
rpc LotsOfGreetings (stream HelloRequest) returns (HelloResponse) {}
}
// LotsOfGreetings 客户端流式的接口,多个招呼在一次返回
func (s *server) LotsOfGreetings(stream pb.Greeter_LotsOfGreetingsServer) error {
reply := "你好:"
for {
// 接收客户端发来的流式数据
res, err := stream.Recv()
if err == io.EOF {
// 最终统一回复
return stream.SendAndClose(&pb.HelloResponse{
Reply: reply,
})
}
if err != nil {
return err
}
reply += res.GetName()
}
}
// 客户端流式RPC
stream, err := c.LotsOfGreetings(ctx)
if err != nil {
log.Fatalf("c.LotsOfGreetings failed, err: %v", err)
}
names := []string{"七米", "q1mi", "沙河娜扎"}
for _, name := range names {
// 发送流式数据
err := stream.Send(&pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("c.LotsOfGreetings stream.Send(%v) failed, err: %v", name, err)
}
}
res, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("c.LotsOfGreetings failed: %v", err)
}
log.Printf("got reply: %v", res.GetReply())
双向流式
双向流式RPC即客户端和服务端均为流式的RPC,能发送多个请求对象也能接收到多个响应对象。
metadata
google.golang.org/grpc/metada…
元数据是指在处理RPC请求和响应过程中需要的信息,例如身份验证,采用键值对列表的形式,其中键是string类型,值通常是[]string类型,但也可以是二进制数据。
gRPC中的 metadata 类似于 HTTP headers中的键值对,元数据可以包含认证token、请求标识和监控标签等。
metadata中的键是大小写不敏感的,由字母、数字和特殊字符-、_、.组成并且不能以grpc-开头(gRPC保留自用),二进制值的键名必须以-bin结尾。
元数据对 gRPC 本身是不可见的,通常是在应用程序代码或中间件中处理元数据,不需要在.proto文件中指定元数据(不需要放在request中)。
客户端和服务端如何发送和获取metadata(流式/普通)博客里有写:
错误处理
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
不同的code码:gRPC教程 | 李文周的博客
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
defer func() {
// 发送trailer
trailer := metadata.New(map[string]string{"trailer": "trailer"})
grpc.SetTrailer(ctx, trailer)
}()
//check metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
/**
在grpc中使用的错误是status.New
但是在go语言中的错误需要转化为Err()
st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
ds, _ := st.WithDetails(
// proto.Message
)
return nil, ds.Err()
*/
// 或者直接像下面这样写
return nil, status.Error(codes.Unauthenticated, "metadata中没有token")
}
fmt.Printf("%#v\n", md)
val := md.Get("token")
if len(val) == 0 || val[0] != "app-test" {
return nil, status.Error(codes.Unauthenticated, "metadata中token不正确")
}
s.count[in.GetName()]++ //记录请求次数
if s.count[in.GetName()] > 2 {
st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
// 添加详细信息
ds, err := st.WithDetails(
&errdetails.QuotaFailure{
Violations: []*errdetails.QuotaFailure_Violation{
{
Subject: fmt.Sprintf("user:%s", in.GetName()),
Description: "每个name只能调用2次",
},
},
})
if err != nil {
return nil, st.Err()
}
return nil, ds.Err()
}
// 发送metadata header
header := metadata.New(map[string]string{"location": "beijing"})
grpc.SendHeader(ctx, header)
reply := "hello " + in.GetName()
return &pb.HelloResponse{
Reply: reply,
}, nil
}
客户端解error detai
resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name}, grpc.Header(&header), grpc.Trailer(&trailer))
if err != nil {
// 解detail
s := status.Convert(err)
for _, detail := range s.Details() {
switch info := detail.(type) {
case *errdetails.QuotaFailure:
fmt.Printf("Quota failure: %v\n", info)
default:
fmt.Printf("Unexpected type of error: %v\n", info)
}
}
//log.Printf("could not greet: %v", err)
return
}
安全连接
证书:
- 自签名证书(openssl)
- CA三方认证证书
服务端
func main() {
//// 普通开启服务
//l, err := net.Listen("tcp", ":8972")
//if err != nil {
// fmt.Println("failed to listen:", err)
// return
//}
//
//// 创建grpc服务
//s := grpc.NewServer()
//// 注册服务
//pb.RegisterGreeterServer(s, &server{count: make(map[string]int)})
//// 启动服务
//if err := s.Serve(l); err != nil {
// fmt.Println("failed to serve:", err)
//}
// 记载证书文件
creds, err := credentials.NewServerTLSFromFile("certs/server.crt", "certs/server.key")
if err != nil {
fmt.Println("failed to load credentials:", err)
return
}
s := grpc.NewServer(grpc.Creds(creds))
// 注册服务
pb.RegisterGreeterServer(s, &server{count: make(map[string]int)})
lis, _ := net.Listen("tcp", "127.0.0.1:8972")
// 启动服务
if err := s.Serve(lis); err != nil {
fmt.Println("failed to serve:", err)
}
}
客户端
creds, err := credentials.NewClientTLSFromFile("/certs/server.crt", "")
if err != nil {
log.Fatalf("Failed to create TLS credentials %v", err)
}
conn, err := grpc.NewClient("127.0.0.1:8972", grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
client := pb.NewGreeterClient(conn)
// 无认证的方式连接
//conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
//if err != nil {
// log.Fatalf("did not connect: %v", err)
// return
//}
// 创建客户端
//c := pb.NewGreeterClient(conn)
defer conn.Close()
拦截器/中间件
一元拦截器
拦截普通RPC调用
客户端
func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
要在 ClientConn 上安装一元拦截器,使用WithUnaryInterceptor的DialOption配置 Dial 。
func getConnWithInterceptor() (*grpc.ClientConn, error) {
creds, err := credentials.NewClientTLSFromFile("./certs/server.crt", "")
if err != nil {
return nil, err
}
// 创建连接,加入拦截器
conn, err := grpc.NewClient(
"127.0.0.1:8972",
//要携带类似请求头的token,就需要安全连接方式 不然报错 code = Unauthenticated desc = transport: cannot send secure credentials on an insecure connection
grpc.WithTransportCredentials(creds),
// 加入一元拦截器
grpc.WithUnaryInterceptor(unaryInterceptor),
)
return conn, err
}
- 预处理:通过检查传入的参数(如 RPC 上下文、方法字符串、要发送的请求和 CallOptions 配置)来获得有关当前 RPC 调用的信息。
- RPC调用:通过Invoker
- 调用后:一旦调用者返回应答和错误,用户就可以对 RPC 调用进行后处理。通常,它是关于处理返回的响应和错误的。
// unaryInterceptor 客户端一元拦截器
func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// 1. 检查调用rpc之前是否设置了token
var credsConfigured bool
for _, o := range opts {
_, ok := o.(grpc.PerRPCCredsCallOption)
if ok {
credsConfigured = true
break
}
}
// 如果没有配置认证信息,则添加 OAuth2 Token
if !credsConfigured {
// 创建一个静态的 TokenSource
tokenSource := oauth2.StaticTokenSource(
&oauth2.Token{
AccessToken: "your-access-token", // 替换为你的实际 Token
TokenType: "Bearer",
},
)
// 将 TokenSource 添加到 gRPC 调用选项中
opts = append(opts, grpc.PerRPCCredentials(oauth.TokenSource{
TokenSource: tokenSource,
}))
}
start := time.Now()
// 2. 调用rpc
err := invoker(ctx, method, req, reply, cc, opts...)
// 3. 调用rpc后进行的处理
end := time.Now()
fmt.Printf("RPC: %s, start time: %s, end time: %s, err: %v\n", method, start.Format(time.RFC3339), end.Format(time.RFC3339), err)
return err
}
最后的结果就是在服务端接收到的请求包含metadata:"authorization":[]string{"Bearer your-access-token"}
服务端
func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
若要为服务端安装一元拦截器,请使用 UnaryInterceptor 的ServerOption配置 NewServer。
本质和上面写的metadata获取MD中的token一样,只不过变成获取"authorization",也可以单独把这个处理过程写成拦截器:
// unaryInterceptor 服务端一元拦截器
func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 1. 处理前
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "missing metadata")
}
if !valid(md["authorization"]) {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
}
// 2. 处理
m, err := handler(ctx, req)
if err != nil {
fmt.Printf("RPC failed with error %v\n", err)
}
return m, err
}
流式拦截器
客户端
func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)
要为 ClientConn 安装流拦截器,使用WithStreamInterceptor的 DialOption 配置 Dial。
// getConnWithStreamInterceptor 连接中加入流式拦截器
func getConnWithStreamInterceptor() (*grpc.ClientConn, error) {
creds, err := credentials.NewClientTLSFromFile("./certs/server.crt", "")
if err != nil {
return nil, err
}
// 创建连接,加入拦截器
conn, err := grpc.NewClient(
"127.0.0.1:8972",
grpc.WithTransportCredentials(creds),
// 加入流式拦截器
grpc.WithStreamInterceptor(streamInterceptor),
)
return conn, err
}
- 预处理:通过检查传入的参数(如 RPC 上下文、方法字符串、要发送的请求和 CallOptions 配置)来获得有关当前 RPC 调用的信息。
- 流操作拦截:流拦截器并没有事后进行 RPC 方法调用和后处理,而是拦截了用户在流上的操作。首先,拦截器调用传入的
streamer以获取ClientStream,然后包装ClientStream并用拦截逻辑重载其方法。最后,拦截器将包装好的ClientStream返回给用户进行操作。
// wrappedStream 最好设置为私有,其他的包不能直接访问,只能通过newWrappedStream创建
type wrappedStream struct {
// 原生实现了grpc.ClientStream接口
grpc.ClientStream
}
func (w *wrappedStream) RecvMsg(m interface{}) error {
fmt.Printf("Receive a message (Type: %T) at %v\n", m, time.Now().Format(time.RFC3339))
return w.ClientStream.RecvMsg(m)
}
func (w *wrappedStream) SendMsg(m interface{}) error {
fmt.Printf("Send a message (Type: %T) at %v\n", m, time.Now().Format(time.RFC3339))
return w.ClientStream.SendMsg(m)
}
func newWrappedStream(s grpc.ClientStream) grpc.ClientStream {
return &wrappedStream{s}
}
// streamInterceptor 客户端流式拦截器
func streamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
var credsConfigured bool
for _, o := range opts {
_, ok := o.(*grpc.PerRPCCredsCallOption)
if ok {
credsConfigured = true
break
}
}
if !credsConfigured {
opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{
AccessToken: "some-secret-token",
})))
}
s, err := streamer(ctx, desc, cc, method, opts...)
if err != nil {
return nil, err
}
// 返回自定义的stream 既是在发送和接收请求时 增加日志输出
return newWrappedStream(s), nil
}
服务端
func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error
要为服务端安装流拦截器,使用 StreamInterceptor 的ServerOption来配置 NewServer。
s := grpc.NewServer(
grpc.Creds(creds),
grpc.UnaryInterceptor(unaryInterceptor),
grpc.StreamInterceptor(streamInterceptor),
)
// streamInterceptor 服务端流拦截器
func streamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
md, ok := metadata.FromIncomingContext(ss.Context())
if !ok {
return status.Errorf(codes.InvalidArgument, "missing metadata")
}
if !valid(md["authorization"]) {
return status.Errorf(codes.Unauthenticated, "invalid token")
}
err := handler(srv, newWrappedStream(ss))
if err != nil {
fmt.Printf("RPC failed with error %v\n", err)
}
return err
}
// wrappedStream 最好设置为私有,其他的包不能直接访问,只能通过newWrappedStream创建
type wrappedStream struct {
// 原生实现了grpc.ServerStream接口
grpc.ServerStream
}
func (w *wrappedStream) RecvMsg(m interface{}) error {
fmt.Printf("Receive a message (Type: %T) at %v\n", m, time.Now().Format(time.RFC3339))
return w.ServerStream.RecvMsg(m)
}
func (w *wrappedStream) SendMsg(m interface{}) error {
fmt.Printf("Send a message (Type: %T) at %v\n", m, time.Now().Format(time.RFC3339))
return w.ServerStream.SendMsg(m)
}
func newWrappedStream(s grpc.ServerStream) grpc.ServerStream {
return &wrappedStream{s}
}
中间件
gateway
原理
映射规则
- 优先匹配URL传递的字段
- body中的字段(如果为*,其余所有字段都在body中)
- 其余字段都在query string中
| 说明 | 代码 | HTTP请求 | grpc请求 |
|---|---|---|---|
| 路径匹配 支持原始类型 | option (google.api.http) = {get: "/v1/ {name=messages/*} "}; | GET /v1/messages/123456 | GetMessage(name: "messages/123456") |
| 重复原始类型 | repeated param | ...?param=A¶m=B | param[]{A,B} |
| 无重复的消息类型 | message SubMessage { string subfield = 1; } SubMessage sub = 3; // Mapped to URL query parameter sub.subfield. } | ...?sub.subfield=A | SubMessage{subfield:A} |
为一个RPC定义多个HTTP方法
option (google.api.http) = {
get: "/v1/messages/{message_id}"
additional_bindings {
get: "/v1/users/{user_id}/messages/{message_id}"
}
};
步骤
-
获取依赖文件
将GitHub - googleapis/googleapis: Public interface definitions of Google APIs.下google/api文件夹放到
ptotoc/include/google下 -
下载插件
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2 -
写protoc
rpc SayHello (HelloRequest) returns (HelloResponse) { // 写注释 option (google.api.http) = { post: "/v1/hello" body: "*" }; } -
生成代码
生成*.pb.gw.go
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative --grpc-gateway_out=. --grpc-gateway_opt=paths=source_relative pb/hello.proto
-
main函数
func startHTTPServer() { l, err := net.Listen("tcp", ":8972") if err != nil { fmt.Println("failed to listen:", err) return } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{count: make(map[string]int)}) // 单独的goroutine启动服务 go func() { if err := s.Serve(l); err != nil { fmt.Println("failed to serve:", err) } }() // 创建一个连接到上面启动的 gRPC 服务器的客户端连接 // gRPC-Gateway 就是通过它来代理请求(将HTTP请求转为RPC请求) conn, err := grpc.NewClient( "0.0.0.0:8972", grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { log.Fatalln("Failed to dial server:", err) } // runtime的包是"github.com/grpc-ecosystem/grpc-gateway/v2/runtime" gwmux := runtime.NewServeMux() // 注册Greeter err = pb.RegisterGreeterHandler(context.Background(), gwmux, conn) if err != nil { log.Fatalln("Failed to register gateway:", err) } gwServer := &http.Server{ Addr: ":8090", Handler: gwmux, } log.Fatalln(gwServer.ListenAndServe()) }
一个端口同时实现grpc和http服务
/ 一个端口同时处理grpc和http
// 1.创建grpc服务
srv := server{
bs: &bookstore{
db: db,
},
}
l, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Printf("cannot listen to port 8972,err: %s\n", err)
return
}
s := grpc.NewServer()
pb.RegisterBookstoreServer(s, &srv)
// 2. grpc-Gateway mux
gwmux := runtime.NewServeMux()
dops := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err = pb.RegisterBookstoreHandlerFromEndpoint(context.Background(), gwmux, "localhost:8972", dops)
if err != nil {
fmt.Printf("cannot register gateway,err: %s\n", err)
}
// 3. http mux
httpMux := http.NewServeMux()
httpMux.Handle("/", gwmux)
// 4. 定义Http server
gwServer := &http.Server{Addr: ":8972", Handler: grpcHandlerFunc(httpMux, s)}
// 5. 启动
gwServer.Serve(l)
func grpcHandlerFunc(otherHandler *http.ServeMux, grpcServer *grpc.Server) http.Handler {
return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
grpcServer.ServeHTTP(w, r)
} else {
otherHandler.ServeHTTP(w, r)
}
}), &http2.Server{})
}
名称解析和负载均衡
为了应对大的业务量,可能由多个机器处理来自同一接口的请求
- 客户端如何动态获取服务端地址?
- 客户端怎么知道要去调用哪个机器的端口?
名称解析
接受一个服务名称,返回后端的IP列表。map[service-name][]backend-ip
DNS解析器
gRPC默认DNS解析器,在Dail中提供域名,默认解析对应IP列表。
dns:[//authority/]host[:port]
conn, err := grpc.Dial("dns:///localhost:8972",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
自定义解析器
func main() {
flag.Parse()
conn, err := grpc.NewClient("q1mi:///resolver.liwenzhou.com", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(&q1miResolverBuilder{})) //这里指定解析器
if err != nil {
fmt.Println("连接失败", err)
}
defer conn.Close()
// 创建客户端
c := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
for i := 0; i < 10; i++ {
resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", resp.GetReply())
}
}
// 自定义name resolver
const (
myScheme = "q1mi"
myEndpoint = "resolver.liwenzhou.com"
)
var addrs = []string{"127.0.0.1:8080", "127.0.0.1:8973"}
// q1miResolver 自定义name resolver,实现Resolver接口
type q1miResolver struct {
target resolver.Target
cc resolver.ClientConn
addrsStore map[string][]string
}
func (r *q1miResolver) ResolveNow(o resolver.ResolveNowOptions) {
addrStrs := r.addrsStore[r.target.Endpoint()]
addrList := make([]resolver.Address, len(addrStrs))
for i, s := range addrStrs {
addrList[i] = resolver.Address{Addr: s}
}
r.cc.UpdateState(resolver.State{Addresses: addrList})
}
func (*q1miResolver) Close() {}
// q1miResolverBuilder 需实现 Builder 接口
type q1miResolverBuilder struct{}
func (*q1miResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
r := &q1miResolver{
target: target,
cc: cc,
addrsStore: map[string][]string{
myEndpoint: addrs,
},
}
r.ResolveNow(resolver.ResolveNowOptions{})
return r, nil
}
func (*q1miResolverBuilder) Scheme() string { return myScheme }
NewClient方法会调用解析器的Build()方法构建自定义的q1miResolver,并调用ResolveNow()方法获取到服务端地址。
// Builder creates a resolver that will be used to watch name resolution updates.
type Builder interface {
// Build creates a new resolver for the given target.
//
// gRPC dial calls Build synchronously, and fails if the returned error is
// not nil.
Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
// Scheme returns the scheme supported by this resolver. Scheme is defined
// at https://github.com/grpc/grpc/blob/master/doc/naming.md. The returned
// string should not contain uppercase characters, as they will not match
// the parsed target's scheme as defined in RFC 3986.
Scheme() string
}
// ResolveNowOptions includes additional information for ResolveNow.
type ResolveNowOptions struct{}
// Resolver watches for the updates on the specified target.
// Updates include address updates and service config updates.
type Resolver interface {
// ResolveNow will be called by gRPC to try to resolve the target name
// again. It's just a hint, resolver can ignore this if it's not necessary.
//
// It could be called multiple times concurrently.
ResolveNow(ResolveNowOptions)
// Close closes the resolver.
Close()
}
负载均衡
conn, err := grpc.NewClient(
"q1mi:///resolver.liwenzhou.com",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithResolvers(&q1miResolverBuilder{}),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`), // 这里设置初始策略
)
分页
定义
| 偏移量分页 | 游标分页 | |
|---|---|---|
| 实现 | GET ...?page**=1&size=**10 | ?after**=Y3Vyc29yOnYyOpK5MjAyOC3Lew&tab=**stars |
| url | 显式地拼接在路径中 | 不透明字符串(加密) |
| sql | LIMIT 10 OFFSET 10 | WHERE id > 10 ORDER BY id ASC LIMIT 10; |
| 跳页 | 支持 | 不支持 |
| 爬虫 | 容易 | 不容易 |
| 缺点 | 并发场景下可能出现重复元素 | 不适合多检索条件的场景 |