[qimi] Go语言微服务学习笔记 Character1

254 阅读20分钟

RPC

代码见:RPC原理与Go RPC | 李文周的博客

是什么

目的:实现类似远程,跨内存空间的函数/方法调用

怎么实现呢?

问题解决
怎么找到要执行的函数在内存中的位置调用方和被调用方都需要维护一个{ function <-> ID }映射表,以确保调用正确的函数。
怎么传递参数:本地调用时通过堆内存参数和返回值在传输期间序列化并转换为字节流
怎么进行网络传输function ID 和序列化字节流需要通过网络传输,因此,只要能够完成传输,调用方和被调用方就不受某个网络协议的限制

和API的区别

RPCAPI
调用像调用本地方法一样调用接口
性能大多是TCP,性能更好HTTP协议
用途微服务间调用前后端交互

原理

image-20250125102101879

gRPC和Protocol Buffer

定义

gRPC

一款rpc框架,能够运行在任意环境(跨语言) ,使用HTTP/2作为传输协议。gRPC是基于定义一个服务,指定一个可以远程调用的带有参数和返回类型的的方法。

优点:可以在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端

protocol buffer

Protocol Buffer是一个IDL库,IDL是接口描述语言,通过中立的语言,使不同语言编写的程序可以互相通信交流。语言无关、平台无关、可扩展的用于序列化结构化数据。

优点

  1. 序列化高效,
  2. IDL简单,
  3. 容易进行接口更新。

一般都是两者结合使用。 也可以根据需要替换别的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的区别

去掉了optionalrequired可选字段

消息

// 必须指定语法版本,否则使用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 TypeGo Type说明
doublefloat64
floatfloat 32
int32int32可变长度编码
int64int64可变长度编码
fixed32uint32总是使用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(流式/普通)博客里有写:

gRPC教程 | 李文周的博客

错误处理

"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调用

客户端

UnaryClientInterceptor

func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

要在 ClientConn 上安装一元拦截器,使用WithUnaryInterceptorDialOption配置 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
}
  1. 预处理:通过检查传入的参数(如 RPC 上下文、方法字符串、要发送的请求和 CallOptions 配置)来获得有关当前 RPC 调用的信息。
  2. RPC调用:通过Invoker
  3. 调用后:一旦调用者返回应答和错误,用户就可以对 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"}

服务端

UnaryServerInterceptor

func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

若要为服务端安装一元拦截器,请使用 UnaryInterceptorServerOption配置 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
}

流式拦截器

客户端

StreamClientInterceptor

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
}
  1. 预处理:通过检查传入的参数(如 RPC 上下文、方法字符串、要发送的请求和 CallOptions 配置)来获得有关当前 RPC 调用的信息。
  2. 流操作拦截:流拦截器并没有事后进行 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
}

服务端

StreamServerInterceptor

func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

要为服务端安装流拦截器,使用 StreamInterceptorServerOption来配置 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}
}

中间件

GitHub - grpc-ecosystem/go-grpc-middleware: Golang gRPC Middlewares: interceptor chaining, auth, logging, retries and more.

gateway

原理

image-20250208103919995

映射规则

  1. 优先匹配URL传递的字段
  2. body中的字段(如果为*,其余所有字段都在body中)
  3. 其余字段都在query string中
说明代码HTTP请求grpc请求
路径匹配 支持原始类型option (google.api.http) = {get: "/v1/ {name=messages/*} "};GET /v1/messages/123456GetMessage(name: "messages/123456")
重复原始类型repeated param...?param=A&param=Bparam[]{A,B}
无重复的消息类型message SubMessage { string subfield = 1; } SubMessage sub = 3; // Mapped to URL query parameter sub.subfield. }...?sub.subfield=ASubMessage{subfield:A}

为一个RPC定义多个HTTP方法

option (google.api.http) = {
get: "/v1/messages/{message_id}"
additional_bindings {
get: "/v1/users/{user_id}/messages/{message_id}"
}
};

步骤

  1. 获取依赖文件

    GitHub - googleapis/googleapis: Public interface definitions of Google APIs.下google/api文件夹放到ptotoc/include/google

  2. 下载插件

    go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2
    
  3. 写protoc

        rpc SayHello (HelloRequest) returns (HelloResponse) {
            // 写注释
            option (google.api.http) = {
                post: "/v1/hello"
                body: "*"
            };
        }
    
  4. 生成代码

    生成*.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
    
  1. 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{})
}

名称解析和负载均衡

为了应对大的业务量,可能由多个机器处理来自同一接口的请求

  1. 客户端如何动态获取服务端地址?
  2. 客户端怎么知道要去调用哪个机器的端口?

名称解析

接受一个服务名称,返回后端的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显式地拼接在路径中不透明字符串(加密)
sqlLIMIT 10 OFFSET 10WHERE id > 10 ORDER BY id ASC LIMIT 10;
跳页支持不支持
爬虫容易不容易
缺点并发场景下可能出现重复元素不适合多检索条件的场景