Go云原生学习笔记二

博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

gRPC

gRPC是一种现代化开源的RPC框架,能够运行于任何环境之中,最初由谷歌进行开发,之前说过RPC是一种软性的规范,而不是硬性的协议。它的底层协议使用HTTP/2作为传输协议。这是因为HTTP 2协议经过优化之后速度已经足够快,并且HTTP2同样使用二进制的数据进行传输,和RPC不谋而合。

常见的负载均衡算法

1.轮询算法:按照顺序依次轮流将请求分配给后端服务器,直到轮询完所有的服务器之后重新开始

2.随机算法:随机选择一个后端服务器来处理请求

3.加权轮询:根据后端服务器的处理能力,为每个服务器分配一个权重值,然后按照权重依次轮询分配请求

4.加权随机:按照后端服务器的处理能力,为每个服务器分配一个权重值,然后按照权重值随机选择一个服务器来处理请求

5.最小连接数:讲请求分配给当前连接数最少的服务器,以保证负载均衡

6.IP哈希算法:根据客户端的IP地址,通过哈希算法计算出一个值,然后将这个值对服务器列表长度取值,得到要访问的服务器编号,类似于随机

7.故障转移:如果当前服务器出现故障或者无法处理请求,则自动切换到下一个可用的服务器

grpc实现了哪些负载均衡算法

1.轮询

2.最小连接数

3.故障转移

4.随机

这些负载均衡算法都可以在grpc的客户端配置中进行设置。默认情况下,使用的是轮询算法。

如果需要使用其他负载均衡算法,可以使用gprc提供的负载均衡器,比如grpclb。此外,grpc还提供了扩展借口来让用户自定义负载均衡算法

在gRPC中,客户端可以像调用本地方法一样直接调用其他机器上的服务端应用程序的方法,帮助你更容易创建分布式应用程序和服务。gRPC是基于定义一个服务,制定一个可以远程调用的带有参数和返回类型的方法。在服务端程序中实现这个接口并且运行gRPC服务处理客户端调用,在客户端,有一个stub提供和服务端相同的方法 image.png

为什么要用gRPC

gRPC可以帮助我们一次性的在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端,也就是说gRPC解决了不同语言以及环境间通信的复杂性。使用protocol buffer还能获得其他好处,包括高效的序列化,简单的IDL以及容易进行接口更新。总之,使用gRPC能够帮助我们更容易编写跨语言的分布式代码

IDL(Interface description Language) 是指接口描述语言,是用来描述软件组件接口的一种计算机语言,是跨平台开发的基础。IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流;比如,一个组件用C++写成,另一个组件用Go写成

使用gRPC进行开发的步骤

编写.proto文件定义服务

默认情况下gRPC使用protocol buffers作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构 在gRPC中可以定义四种类型的服务方法

普通rpc,客户端向服务器发送一个请求,然后得到一个响应,就像普通的函数调用一样 rpc SayHello(HelloRequest) returns (HelloResponse);

服务器流式rpc,其中客户端向服务器发送请求,并获得一个流来读取一系列消息。客户端从返回的流中读取,直到没有更多的消息,gRPC保证在单个RPC调用的消息是有序的 rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);

客户端流式rpc,其中客户端写入一系列消息并将其发送到服务器,同样使用提供的流。一旦客户端完成了消息的写入,它就等待服务器读取消息并返回响应,同样,gRPC保证单个RPC调用中的消息是有序的 rpc LostsOfGreetings(stream HelloRequest) returns(HelloResponse);

双向流式rpc,其中双方使用读写流发送一系列消息,这两个流独立运行,因此客户端和服务器可以按照自己喜欢的顺序读写;例如,服务器可以等待接受所有客户端消息后再写响应,或者可以交替读取消息然后写入消息,或者其他读写组合。每个流中的消息是有序的 rpc LostsOfGreetings(stream HelloRequest) returns(stream HelloResponse);

生成指定语言的代码(客户端一份、服务端一份).proto文件中定义好服务之后,gRPC提供了生成客户端和服务端代码的protocol buffers编译器插件。 我们使用这些插件可以根据需要生成Java Go C++ Python等语言的代码,我们通常会在客户端调用这些API,并且在服务器端实现对应的API

  • 在服务器端,服务器实现服务声明的方法,并运行一个gRPC服务器来处理客户端发来的调用请求。gRPC底层会对传入的请求进行编码,执行被调用的服务方法,并对服务响应进行编码
  • 在客户端,客户端有一个称为存根(stub)的本地对象,它实现了与服务相同的方法。然后,客户端可以在本地对象上调用这些方法,将调用的参数包装在适当的 protocol buffers消息类型中--gRPC在向服务器发送请求并返回服务器的 protocol buffers响应之后进行处理

编写业务逻辑代码 proto文件生成pb.go以及grpc.pb.go的命令 不指定proto路径 protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative yourpath 指定proto路径 protoc --proto_path=pb --go_out=pb --go_opt=paths=source_relative --go-grpc_out=pb --go-grpc_opt=paths=source_relative xxx.proto

使用grpc实现一个简单的hello服务

Server

type server struct {
 pb.UnimplementedGreeterServer //字段作用是 当没有完全实现proto中的所有方法时依旧可以运行起来
}

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:", err)
  return
 }
 // 注册服务
 s := grpc.NewServer()
 pb.RegisterGreeterServer(s, &server{})
 // 启动服务
 err = s.Serve(l)
 if err != nil {
  fmt.Println("failed to server,err:", err)
 }
}
syntax = "proto3";  //版本声明

option go_package="hello_server/pb";  // 项目中import导入生成go代码的模块

package  pb;  //proto文件模块

// 定义服务
service Greeter {
  // 定义方法
  rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 定义消息
message HelloRequest {
  string name = 1;   //字段的序号
}

message  HelloResponse {
  string reply = 1;
}

Client

func main() {
 //连接server 带加密连接
 conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
 if err != nil {
  log.Fatalf("grpc.Dial failed,err:%v", err)
  return
 }
 defer conn.Close()
 //创建客户端
 c := proto.NewGreeterClient(conn)
 //使用context进行控制,传入background和超时时间一秒钟
 ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
 defer cancel()
 name := "xiaocheng"
 resp, err := c.SayHello(ctx, &proto.HelloRequest{Name: name})
 if err != nil {
  log.Printf("c.SayHello failed, err:%v", err)
  return
 }
 // 拿到RPC响应
 log.Printf("resp:%v", resp.GetReply())
}

// 应该是同一份proto文件
syntax = "proto3";  //版本声明

option go_package="hello_client/proto";  // 项目中import导入生成go代码的模块

package  pb;  //proto文件模块 必须与server端一致

// 定义服务
service Greeter {
  // 定义方法
  rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 定义消息
message HelloRequest {
  string name = 1;   //字段的序号
}

message  HelloResponse {
  string reply = 1;
}

使用grpc实现一个简单的add服务

type server struct {
 pb.UnimplementedAddMethodServer
}

func (s *server) Add(ctx context.Context, in *pb.AddRequest) (*pb.AddResponse, error) {
 reply := in.GetArgs1() + in.GetArgs2()
 return &pb.AddResponse{Number: reply}, nil
}

func main() {
 //启动服务
 l, err := net.Listen("tcp"":9999")
 if err != nil {
  fmt.Println("net listen failed,err:", err)
  return
 }
 s := grpc.NewServer()
 pb.RegisterAddMethodServer(s, &server{})
 err = s.Serve(l)
 if err != nil {
  fmt.Println("failed to server,err:", err)
 }
}

proto文件应该在客户端和服务端都有一份

syntax="proto3";
option go_package="server/pb";
package pb;

service AddMethod {
   rpc Add(AddRequest) returns (AddResponse) {}
}

message AddRequest {
  int32 args1 =1;
  int32 args2 =2;
}

message AddResponse {
  int32 number =1;
}
func main() {
 conn, err := grpc.Dial("127.0.0.1:9999", grpc.WithTransportCredentials(insecure.NewCredentials()))
 if err != nil {
  fmt.Println("grpc dail failed,err:", err)
  return
 }
 defer conn.Close()
 c := pb.NewAddMethodClient(conn)
 ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
 defer cancel()
 var args1, args2 int32
 args1 = 1
 args2 = 2
 resp, err := c.Add(ctx, &pb.AddRequest{Args1: args1, Args2: args2})
 if err != nil {
  fmt.Println("c.Add failed,err:", err)
  return
 }
 log.Println("Add Response:", resp)
}

protobuf语法

protobuf为什么体积小、解析快

protobuf是google提出的数据交换格式,同一条消息数据,使用Protobuf序列化之后占用空间是JSON的1/10,但是性能却是几十倍

原因如下

  • 编解码大多采用位运算,比JSON/XML的字符匹配效率更高
  • pb定义了 varint类型,使用变长编码压缩数值类型。值越小的数字,使用的字节数就越少
  • 采用Tag-value类型,没有冗余字符,而JSON有很多冗余的部分,这是为了方便人类阅读才加上的

定义一个消息类型

syntax="proto3";

message SearchRequest {
 string query =1;
 int32 page_number=2; 
}

//文件的第一行指定使用proto3语法,如果不这么写
//pb的编译器默认使用proto2

//SearchRequest定义了一个消息,使用了两个字段
//每个字段需要定义类型 名字 和编号

字段编号

消息定义中的每个字段都要有一个唯一的编号,这些编号用来在消息二进制格式中标识字段,在消息类型使用后就不能更改

在范围1到15中的字段需要一个字节进行编码,而16-2047的字段采用两个字节。所以应该为经常使用的消息元素保留数字1到15的编号,也要为将来可能添加的经常使用的元素留出一些编号

指定字段规则 消息字段可以是下列字段之一

  • singular:格式正确的消息可以有这个字段的0个或者一个,默认使用singular字段
  • repeated:该字段可以在格式正确的消息中重复任意次数(包括0次),重复值的顺序将被保留
  • optional:该字段在传递的时候可选也可不选

保留字段

如果你通过完全删除字段或者将其注释来更新消息类型,那么未来的用户在对该类型进行自己的更新的时候就可以重用字段号,如果其他人以后加载旧版本的相同.proto文件,这可能就会导致严重的问题,比如数据损坏、隐私漏洞等等。

解决方法是通过reserved来指定已经删除的字段的字段编号,如果将来有用户尝试使用这些字段标识符,protocol buffer编译器将发出提示

message Foo {
reserved 2,15,9 to 11;
}

值类型

.proto TypeNotesC++ TypeJava/Kotlin Type[1]Python Type[3]Go TypePHP Type
doubledoubledoublefloatfloat64float
floatfloatfloatfloatfloat32float
int32使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,则使用 sint32代替。int32intintint32integer
int64使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,则使用 sint64代替。int64longint/long[4]int64integer/string[6]
uint32使用变长编码。uint32int[2]int/long[4]uint32integer
uint64使用变长编码。uint64long[2]int/long[4]uint64integer/string[6]
sint32使用可变长度编码。带符号的 int 值。这些编码比普通的 int32更有效地编码负数。int32intintint32integer
sint64使用可变长度编码。带符号的 int 值。这些编码比普通的 int64更有效地编码负数。int64longint/long[4]int64integer/string[6]
fixed32总是四个字节。如果值经常大于228,则比 uint32更有效率。uint32int[2]int/long[4]uint32integer
fixed64总是8字节。如果值经常大于256,则比 uint64更有效率。uint64integer/string[6]
sfixed32总是四个字节。int32intintint32integer
sfixed64总是八个字节。int64integer/string[6]
boolboolbooleanboolboolboolean
string字符串必须始终包含 UTF-8编码的或7位 ASCII 文本,且不能长于232。stringStringstr/unicode[5]stringstring
bytes可以包含任何不超过232字节的任意字节序列。stringByteStringstr (Python 2) bytes (Python 3)[]bytestring

枚举

在定义消息类型的时候,可能希望其中的一个字段只能是预定义的值列表中的一个值。下面是一个栗子,通过enum来保证Conrpus字段的值只能是其中的一个

message SearchRequest {
string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}
 

嵌套消息类型

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

Any

Any类型允许你将消息作为嵌入类型使用,使用Any类型需要导入google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

oneof

如果你有一条包含多个字段的消息,并且同时最多设置其中的一个字段,那么可以通过oneof来实现并节省内存,可以通过case()或者WihchOneOf()来检查one of 中的哪个值被设置(如果有)

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}
 SampleMessage message;
  message.set_name("name");
  CHECK(message.has_name());
  message.mutable_sub_message();   // Will clear name field.
  CHECK(!message.has_name());

Maps

如果想创建一个关联映射作为数据定义的一部分,可以使用这个map

map<key_type, value_type> map_field = N;

protobuf实战

oneof字段

oneof中的值只能选择其中的一个

message NoticeReaderRequest {
  string msg=1;
  oneof notice_way{
    string email=2;
    string phone=3;
      }
}

对应的服务端代码

func oneofDemo() {
 req := &book.NoticeReaderRequest{
  Msg: "here is chengxisheng",
  NoticeWay: &book.NoticeReaderRequest_Email{
   Email: "xxx",
  },
 }
 req2 := &book.NoticeReaderRequest{
  Msg: "here is xishengcheng",
  NoticeWay: &book.NoticeReaderRequest_Phone{
   Phone: "1008611",
  },
 }
 switch v := req.NoticeWay.(type) {
 case *book.NoticeReaderRequest_Email:
  noticeWithEmail(v)
 case *book.NoticeReaderRequest_Phone:
  noticeWithPhone(v)
 }
 switch v := req2.NoticeWay.(type) {
 case *book.NoticeReaderRequest_Email:
  noticeWithEmail(v)
 case *book.NoticeReaderRequest_Phone:
  noticeWithPhone(v)
 }
}
func noticeWithEmail(in *book.NoticeReaderRequest_Email) {
 fmt.Printf("notice reader by email:%v\n", in.Email)
}
func noticeWithPhone(in *book.NoticeReaderRequest_Phone) {
 fmt.Printf("notice reader by phone:%v\n", in.Phone)
}

//这里必须使用类型断言+switch case 
//来进行one of 字段的确认

wrapvalue类型

首先让我们想一想Go中区分一个MySQL的int类型是默认值还是0值该怎么做? 其实就只有以下两种方法

price sql.NullInt64
price *int64
//第一种方式是一个定义好的结构体 
//里面有一个字段是 该结构体是否被赋值
//第二种方式是直接用指针来做
//对指针解引用,如果为0则赋值,如果为Nil则是默认值

在RPC中也是如此,我们可以通过wrapvalue来确定这个字段是否被赋值

func wrapValueDemo() {
 // client
  book:=book.Book{
   Title: "learning go language",
   Price: &wrapperspb.Int64Value{Value: 600},
   Memo: &wrapperspb.StringValue{Value: "学"},
  }
  // server 
  if book.GetPrice()==nil {
   fmt.Println("is not assigned")
  } else {
   fmt.Println(book.GetPrice().GetValue())
  }
  if book.GetMemo()==nil {
   fmt.Println("is not assigned")
  } else {
   fmt.Println(book.GetMemo().GetValue())
  }
}

FieldMask类型

当我们更新的时候,定义了很多字段,不可能全部进行全量更新Book的每个字段,因为通常操作只会更新1到2个字段。

当我们想知道更新操作涉及到的具体字段,就需要使用到filedmask类型

message UpdateBookRequest {
  // 操作人
  string op=1;
  // 要更新的书籍信息
  Book book=2;
  // 要更新的字段
  google.protobuf.FieldMask update_mask=3;
}
func fieldMaskDemo() {
 //client
 paths := []string{"price"}
 req := &book.UpdateBookRequest{
  Op: "chengxisheng",
  Book: &book.Book{
   Price: &wrapperspb.Int64Value{Value: 8800},
  },
  UpdateMask: &fieldmaskpb.FieldMask{Paths: paths},
 }
 mask, _ := fieldmask_utils.MaskFromProtoFieldMask(req.UpdateMask, generator.CamelCase)
 var bookDst = make(map[string]interface{})
 fieldmask_utils.StructToMap(mask, book.UpdateBookRequest{}.Book, bookDst)
 fmt.Printf("bookDst:%#v\n", bookDst)
}

服务端流式RPC

对应的proto(client和server)中添加一个流式方法 rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);

Server添加一个新的方法

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(),
  }
  // 使用Send方法发送多个数据 每当有一个data就send一次数据
  if err := stream.Send(data); err != nil {
   return err
  }
 }
 return nil
}

Client端添加一个新的函数

func callLotsOfReplies(c proto.GreeterClient) {
 ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
 defer cancel()
 stream, err := c.LotsOfReplies(ctx, &proto.HelloRequest{Name: *name})
 if err != nil {
  log.Fatalf("c.LotsOfReplies failed,err:%v", err)
 }
 for {
  //依次从流式响应中读取返回的响应数据
  res, err := stream.Recv()
  if err == io.EOF {
   break
  }
  if err != nil {
   log.Fatalf("c.LotsOfReplies failed,err:%v", err)
  }
  log.Printf("got reply: %q\n", res.GetReply())
 }
}

客户端流式RPC

hello.proto中添加这么一个新的方法

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);

在server端添加

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()
   }
}

在Client端中添加

func runLotsOfGreeting(c proto.GreeterClient) {
 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 defer cancel()
 // 客户端要流式的发送请求消息
 stream, err := c.LotsOfGreetings(ctx)
 if err != nil {
  log.Printf("c.LotsOfGreetings failed,err:%v\n", err)
  return
 }
 names := []string{"张三""李四""王五"}
 for _, name := range names {
  stream.Send(&proto.HelloRequest{Name: name})
  time.Sleep(200 * time.Millisecond)
 }
 //关闭流
 res, err := stream.CloseAndRecv()
 log.Printf("res:%v\n", res)
}

双向流式RPC

在proto中添加

rpc BidiHello(stream HelloRequest) returns(stream HelloResponse);

在client中添加

func (s *server) BidiHello(stream pb.Greeter_BidiHelloServer) error {
 for {
  //接受流式请求
  in, err := stream.Recv()
  if err == io.EOF {
   return nil
  }
  if err != nil {
   return err
  }
  reply := magic(in.GetName())
  //返回流式响应
  if err := stream.SendAndClose(&pb.HelloResponse{Reply: reply}); err != nil {
   return err
  }

 }
}

在Server端中添加

func (s *server) BidiHello(stream pb.Greeter_BidiHelloServer) error {
 for {
  //接受流式请求
  in, err := stream.Recv()
  if err == io.EOF {
   return nil
  }
  if err != nil {
   return err
  }
  reply := magic(in.GetName())
  //返回流式响应
  if err := stream.SendAndClose(&pb.HelloResponse{Reply: reply}); err != nil {
   return err
  }

 }
}


// magic 一段价值连城的“人工智能”代码
func magic(s string) string {
 s = strings.ReplaceAll(s, "吗""")
 s = strings.ReplaceAll(s, "吧""")
 s = strings.ReplaceAll(s, "你""我")

 s = strings.ReplaceAll(s, "?""!")
 s = strings.ReplaceAll(s, "?""!")
 return s
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值