当前位置: 首页 > news >正文

G0第26章:微服务概述与gRPCprotocol buffers

Go微服务与云原生

1、微服务架构介绍

请添加图片描述

单体架构(电商)

请添加图片描述

SOA架构(电商)

请添加图片描述

微服务架构(电商)

请添加图片描述

请添加图片描述

优势

请添加图片描述

挑战

请添加图片描述

拆分

请添加图片描述

发展史

请添加图片描述

第一代:基于RPC的传统服务架构

请添加图片描述

第二代:Service Mesh(istio)

请添加图片描述

微服务架构分层

请添加图片描述

核心组件

请添加图片描述

Summary

请添加图片描述

2、RPC

2、什么是RPC

RPC (Remote Procedure Call),即远程过程调用。它允许像调用本地服务一样调用远程服务。
RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
首先与RPC(远程过程调用)相对应的是本地调用

本地调用

请添加图片描述

RPC调用

本地过程调用发生在同一进程中——定义add函数的代码和调用add函数的代码共享同一个内存空间,所以调用能够正常执行。请添加图片描述
但是我们无法直接在另一个程序——app2中调用add函数,因为它们是两个程序——内存空间是相互隔离的。(app1和app2可能部署在同一台服务器上也可能部署在互联网的不同服务器上。)
请添加图片描述
RPC就是为了解决类似远程、跨内存空间、的函数/方法调用的。要实现RPC就需要解决以下三个问题。

  • 1、如何确定要执行的函数?在本地调用中,函数主体通过函数指针函数指定,然后调用add函数,编译器通过函数指针函数自动确定add函数在内存中的位置。但是在RPC中,调用不能通过函数指针完成,因为他们的内存地址可能完全不同。因此,调用方和被调用方都需要维护 一个{function <->ID}映射表,以确保调用正确的函数。
  • 2、如何表达参数?本地过程调用中传递的参数是通过堆栈内存结构实现的,但是RPC不能直接使用内存传递参数,因此参数或返回值需要在传输期间序列化并转换成字节流,反之亦然。
  • 3、如何进行网络传输? 函数的调用方河北调用方式通过网络连接的,也就是说,function ID和序列化字节流需要通过网络传输,因此,只要能够完成传输,调用方河北调用方就不熟某个网络协议的限制。例如,一些RPC框架使用TCP协议,一些使用HTTP。

以往实现跨服务调用的时候,我们会采用RESTful API的方式,被调用方会对外提供一个HTTP接口,调用方按要求发起HTTP请求并接受APT交界口返回的相应数据。下面的示例是将add函数包装秤一个RESTful API。

HTTP调用RESTful API

首先,我们编写一个基于HTTP的server服务,它将接收其他程序发来的HTTP请求,执行特定的程序并将结果返回。

package mainimport("encoding/json""io/ioutil""log"“net/http”
)type addParam struct{X int `json:"x"`Y int `json:"y"`
}type addResult struct{Code int `json:"code"`Data int `json:"data"`
}func add(x. y int) int{return x + y
}func addHandler(w http.ResponseWriter, r *http.Request){//解析参数b , _ := ioutil.ReadAll(r.Body)var param addParamjson.Unmarshal(b , &param)//业务逻辑ret := add(param.X,param.Y)//返回响应respBytes, _ := json.Marshal(addResult{Code:0, Data: ret})w.Write(respBytes)
}func main(){http.HandleFunc("/add", addHandler)log.Fatal(http.ListenAndServe(":9090",nil))}

我们编写一个客户端来请求上述HTTP服务,传递x和y两个整数,等待返回结果。

package mainimport ("bytes""encoding/json""fmt""io/ioutil""net/http"
)type addParam struct{X int `json:"x"`Y int `json:"y"`
}type addResult struct {Code int `json:"code"`Data int `json:"data"`
}func main(){//通过HTTP请求调用其他服务器上的add服务url := "http://127.0.0.1:9090/add"param := &addParam{X:20,Y:20,}paramBytes, _ := json.Marshal(param)resp, _ := http.Post(url,"application/json", bytes.NewReader(paramBytes))defer resp.Body.Close()respBytes, _ := ioutil.ReadAll(resp.Body)var respData addResultjson.Unmarshal(respBytes, &respData)fmt.Println(respData.data) //30
}

这种模式是我们目前比较常见的跨服务或跨语言之间基于RESTful API的服务调用模式。 既然使用API调用也能实现类似远程调用的目的,为什么还要用RPC呢?

使用 RPC 的目的是让我们调用远程方法像调用本地方法一样无差别。并且基于RESTful API通常是基于HTTP协议,传输数据采用JSON等文本协议,相较于RPC 直接使用TCP协议,传输数据多采用二进制协议来说,RPC通常相比RESTful API性能会更好。

RESTful API多用于前后端之间的数据传输,而目前微服务架构下各个微服务之间多采用RPC调用。

net/rpc

基础RPC示例

Go语言的 rpc 包提供对通过网络或其他 i/o 连接导出的对象方法的访问,服务器注册一个对象,并把它作为服务对外可见(服务名称就是类型名称)。注册后,对象的导出方法将支持远程访问。服务器可以注册不同类型的多个对象(服务) ,但是不支持注册同一类型的多个对象。
请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述

基于TCP协议的RPC

请添加图片描述
请添加图片描述

使用JSON协议的RPC

请添加图片描述

请添加图片描述

Python调用RPC

请添加图片描述

RPC原理

请添加图片描述
① 服务调用方(client)以本地调用方式调用服务;

② client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;

③ client stub找到服务地址,并将消息发送到服务端;

④ server 端接收到消息;

⑤ server stub收到消息后进行解码;

⑥ server stub根据解码结果调用本地的服务;

⑦ 本地服务执行并将结果返回给server stub;

⑧ server stub将返回结果打包成能够进行网络传输的消息体;

⑨ 按地址将消息发送至调用方;

⑩ client 端接收到消息;

⑪ client stub收到消息并进行解码;

⑫ 调用方得到最终结果。

使用RPC框架的目标是只需要关心第1步和最后1步,中间的其他步骤统统封装起来,让使用者无需关心。例如社区中各式RPC框架(grpc、thrift等)就是为了让RPC调用更方便。

4、protocol Buffers V3中文语法指南

指南原版

定义一个消息类型

首先让我们看一个非常简单的例子。假设你想要定义一个搜索请求消息格式,其中每个搜索请求都包含一个查询词字符串、你感兴趣的查询结果所在的特定页码数和每一页应展示的结果数。

下面是用于定义这个消息类型的 .proto 文件。

syntax = "proto3";message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;
}
  • 文件的第一行指定使用 proto3 语法: 如果不这样写,protocol buffer编译器将假定你使用 proto2。这个声明必须是文件的第一个非空非注释行。
  • SearchRequest 消息定义指定了三个字段(名称/值对) ,每个字段表示希望包含在此类消息中的每一段数据。每个字段都有一个名称和一个类型

指定字段类型
在上面的示例中,所有字段都是标量类型(scalar types): 两个整数(page_number和 result_per_page)和一个字符串(query)。但是也可以为字段指定组合类型,包括枚举和其他消息类型。

分配字段编号
请添加图片描述

指定字段规则
请添加图片描述

请添加图片描述

你可以在 Protocol Buffer Encoding](https://developers.google.com/protocol-buffers/docs/encoding#packed) 中找到关于packed编码的更多信息。

添加更多消息类型
可以在一个.proto 文件中定义多个消息类型。如果你正在定义多个相关的消息,这是非常有用的——例如,如果想定义与 SearchRequest 消息类型对应的应答消息格式SearchResponse,你就可以将其添加到同一个.proto文件中。

message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;
}message SearchResponse {...
}

添加注释

要给你的.proto文件添加注释,需要使用C/C++风格的//和/* … */语法。

/* SearchRequest 表示一个分页查询 * 其中有一些字段指示响应中包含哪些结果 */message SearchRequest {string query = 1;int32 page_number = 2;  // 页码数int32 result_per_page = 3;  // 每页返回的结果数
}

保留字段
如果你通过完全删除字段或将其注释掉来更新消息类型,那么未来的用户在对该类型进行自己的更新时可以重用字段号。如果其他人以后加载旧版本的相同.proto文件,这可能会导致严重的问题,包括数据损坏,隐私漏洞等等。确保这种情况不会发生的一种方法是指定已删除字段的字段编号(和/或名称,这也可能导致 JSON 序列化问题)是保留的(reserved)。如果将来有任何用户尝试使用这些字段标识符,protocol buffer编译器将发出提示。

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

请添加图片描述
请添加图片描述
注意,不能在同一个reserved语句中混合字段名和字段编号。

从你的.proto文件生成了什么?
请添加图片描述

标量值类型

标量消息字段可以具有以下类型之一——该表显示了.proto文件,以及自动生成类中的对应类型(省略了Ruby、C#和Dart):
请添加图片描述
[1] Kotlin 使用来自 Java 的相应类型,甚至是无符号类型,以确保混合 Java/Kotlin 代码库的兼容性。

[2] 在 Java 中,无符号的32位和64位整数使用它们的有符号对应项来表示,最高位存储在有符号位中。

[3] 在任何情况下,为字段设置值都将执行类型检查,以确保其有效。

[4] 64位或无符号的32位整数在解码时总是表示为 long ,但如果在设置字段时给出 int,则可以表示为 int。在任何情况下,值必须与设置时表示的类型相匹配。见[2]。

[5] Python 字符串在解码时表示为 unicode,但如果给出了 ASCII 字符串,则可以表示为 str (这可能会更改)。

[6] 整数用于64位机器,字符串用于32位机器。

默认值

当解析消息时,如果编码消息不包含特定的 singular 元素,则解析对象中的相应字段将设置为该字段的默认值。

请添加图片描述

枚举

在定义消息类型时,你可能希望其中一个字段只能是预定义的值列表中的一个值。例如,假设你想为每个 SearchRequest 添加一个语料库字段,其中语料库可以是 UNIVERSAL、 WEB、 IMAGES、 LOCAL、 NEWS、 PRODUCTS 或 VIDEO。你可以通过在消息定义中添加一个枚举,为每个可能的值添加一个常量来非常简单地完成这项工作。

在下面的例子中,我们添加了一个名为 Corpus 的enum,包含所有可能的值,以及一个类型为 Corpus 的字段:

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 MyMessage1 {enum EnumAllowingAlias {option allow_alias = true;UNKNOWN = 0;STARTED = 1;RUNNING = 1;}
}
message MyMessage2 {enum EnumNotAllowingAlias {UNKNOWN = 0;STARTED = 1;// RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.}
}

请添加图片描述
预留值

请添加图片描述

使用其他消息类型

你可以使用其他消息类型作为字段类型。例如,假设你希望在每个 SearchResponse消息中包含UI个 Result消息——为了做到这一点,你可以在同一个.proto文件中定义 Result消息类型。然后在 SearchResponse中指定 Result 类型的字段。

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

导入定义

在上面的示例中,Result消息类型定义在与 SearchResponse相同的文件中——如果你希望用作字段类型的消息类型已经在另一个.proto文件中定义了,该怎么办?

你可以通过 import 来使用来自其他 .proto 文件的定义。要导入另一个.proto 的定义,你需要在文件顶部添加一个 import 语句:

import "myproject/other_protos.proto";

默认情况下,只能从直接导入的 .proto 文件中使用定义。但是,有时你可能需要将 .proto 文件移动到新的位置。你可以在旧目录放一个占位的.proto文件使用import public 概念将所有导入转发到新位置,而不必直接移动.proto文件并修改所有的地方。
请添加图片描述
使用proto2消息类型

导入 proto2消息类型并在 proto3消息中使用它们是可能的,反之亦然。然而,proto2 enum 不能直接在 proto3语法中使用(如果一个导入的 proto2消息使用了它们,那没问题)。

嵌套类型

请添加图片描述

更新消息类型

如果现有的消息类型不再满足你的所有需要——例如,你希望消息格式有一个额外的字段——但是你仍然希望使用用旧格式创建的代码,不要担心!在不破坏任何现有代码的情况下更新消息类型非常简单,只需记住以下规则:

请添加图片描述

未知字段

未知字段是格式良好的协议缓冲区序列化数据,表示解析器不识别的字段。例如,当旧二进制解析由新二进制发送的带有新字段的数据时,这些新字段将成为旧二进制中的未知字段。

最初,proto3消息在解析过程中总是丢弃未知字段,但在3.5版本中,我们重新引入了未知字段的保存来匹配 proto2行为。在3.5及以后的版本中,解析期间保留未知字段,并将其包含在序列化输出中。

Any

请添加图片描述

oneof

如果你有一条包含多个字段的消息,并且最多同时设置其中一个字段,那么你可以通过使用oneof来实现并节省内存。

oneof字段类似于常规字段,只不过oneof中的所有字段共享内存,而且最多可以同时设置一个字段。设置其中的任何成员都会自动清除所有其他成员。根据所选择的语言,可以使用特殊 case()或 WhichOneof() 方法检查 one of 中的哪个值被设置(如果有的话)。

请添加图片描述
请添加图片描述
请添加图片描述
请添加图片描述

Maps

请添加图片描述
请添加图片描述
请添加图片描述

Packages

请添加图片描述
请添加图片描述

定义服务

请添加图片描述

JSON 映射

proto3支持 JSON 的规范编码,使得系统之间更容易共享数据。下表按类型逐一描述了编码。

如果 json 编码的数据中缺少某个值,或者该值为 null,那么在解析为 protocol buffer 时,该值将被解释为适当的默认值。如果一个字段在 protocol buffer 中具有默认值,为了节省空间,默认情况下 json 编码的数据中将省略该字段。具体实现可以提供在JSON编码中可选的默认值。

请添加图片描述
请添加图片描述

5、protocol buffers使用指南

protobuf介绍

Protobuf全称Protocol Buffer,是 Google 公司于2008年开源的一种语言无关、平台无关、可扩展的用于序列化结构化数据——类似于XML,但比XML更小、更快、更简单,它可用于(数据)通信协议、数据存储等。你只需要定义一次你想要的数据结构,然后你就可以使用特殊生成的源代码来轻松地从各种数据流和各种语言中写入和读取你的结构化数据。目前 Protobuf 被广泛用作微服务中的通信协议。

Go语言使用protoc示例

我们新建一个名为demo的项目,并且将项目中定义的.proto文件都保存在proto目录下。

本文后续的操作命令默认都在demo目录下执行。

普通编译

请添加图片描述
请添加图片描述
请添加图片描述

protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/price.proto

请添加图片描述

protoc -I=proto --go_out=proto --go_opt=paths=source_relative book/price.proto

请添加图片描述

protoc --go_out=. --go_opt=paths=source_relative proto/book/price.proto

上面的命令都是将代码生成到demo/proto目录,如果想要将生成的Go代码保存在其他文件夹中(例如pb文件夹),那么我们需要先在demo目录下创建一个pb文件夹。然后在命令行通过–go_out=pb指定生成的Go代码保存的路径。完整命令如下:

protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative book/price.proto

请添加图片描述

import同目录下protobuf文件

随着业务的复杂度上升,我们可能会定义多个.proto源文件,然后根据需要引入其他的protobuf文件。

在这个示例中,我们在demo/proto/book目录下新建一个book.proto文件,它通过import “book/price.proto”;语句引用了同目录下的price.proto文件。

// demo/proto/book/book.protosyntax = "proto3";// 声明protobuf中的包名
package book;// 声明生成的Go代码的导入路径
option go_package = "github.com/Q1mi/demo/proto/book";// 引入同目录下的protobuf文件(注意起始位置为proto_path的下层)
import "book/price.proto";message Book {string title = 1;Price price = 2;
}

编译命令如下:

protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto

请添加图片描述

import其他目录下文件

请添加图片描述
请添加图片描述

protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto

请添加图片描述

import google proto文件

请添加图片描述
请添加图片描述

protoc --proto_path=/Users/liwenzhou/workspace/go/pkg/mod/github.com/protocolbuffers/protobuf@v3.21.2+incompatible/src/ --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto

请添加图片描述

protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative book/book.proto book/price.proto author/author.proto#### 生成gRPC代码

生成gRPC代码

请添加图片描述

protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative --go-grpc_out=proto --go-grpc_opt=paths=source_relative book/book.proto book/price.proto author/author.proto

请添加图片描述

gRPC-Gateway

gRPC-Gateway也是日常开发中比较常用的一个工具,它同样也是根据 protobuf 生成相应的代码。

安装工具
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
为protobuf文件添加注释

请添加图片描述

请添加图片描述

编译

这一次编译命令在之前的基础上要继续加上 gRPC-Gateway相关的 --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative 参数。

完整的编译命令如下:

protoc --proto_path=proto --go_out=proto --go_opt=paths=source_relative --go-grpc_out=proto --go-grpc_opt=paths=source_relative --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative book/book.proto book/price.proto author/author.proto

请添加图片描述
为了方便编译可以在项目下定义Makefile。

.PHONY: gen helpPROTO_DIR=protogen: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代码"

后续想要编译只需在项目目录下执行make gen即可。

管理 protobuf

在企业的项目开发中,我们通常会把 protobuf 文件存储到一个单独的代码库中,并在具体项目中通过git submodule引入。这样做的好处是能够将 protobuf 文件统一管理和维护,避免因 protobuf 文件改动导致的问题。

本文示例代码已上传至github仓库:https://github.com/Q1mi/demo,请点击查看完整代码。

6、protobuf中使用oneof、WrapValue和FieldMask

本文介绍了在Go语言中如何使用oneof字段以及如何通过使用google/protobuf/wrappers.proto中定义的类型区分默认值和没有传值;最后演示了Go语言中借助fieldmask-utils库使用google/protobuf/field_mask.proto实现部分更新的方法。

oneof

如果你有一条包含多个字段的消息,并且最多同时设置其中一个字段,那么你可以通过使用oneof来实现并节省内存。

oneof字段类似于常规字段,只不过oneof中的所有字段共享内存,而且最多可以同时设置一个字段。设置其中的任何成员都会自动清除所有其他成员。根据所选择的语言,可以使用特殊 case()或 WhichOneof() 方法检查 one of 中的哪个值被设置(如果有的话)。

protobuf定义

请添加图片描述

serveri端代码

请添加图片描述

WrapValue

Golang判断是息定义零值还是默认零值敀方法

请添加图片描述

请添加图片描述

protobuf定义

请添加图片描述

client端代码

请添加图片描述

serveri端代码

请添加图片描述

syntax = "proto3";package api;option go_package = "protobuffers02/api";import "google/protobuf/wrappers.proto";// 通知读者的消息
message NoticeReaderRequest{string msg = 1;oneof notice_way{string email = 2;string phone = 3;}
}message Book{string title =1;string author = 2;// int64 price = 3;google.protobuf.Int64Value price = 3; // int64google.protobuf.DoubleValue sale_price = 4; // float64google.protobuf.StringValue memo = 5; // string
}
protoc --proto_path=api \
--go_out=api --go_opt=paths=source_relative \
notice.proto
package mainimport ("fmt""google.golang.org/protobuf/types/known/wrapperspb""protobuffers02/api"
)
// Go语言中判断是自定义零值还是默认零值的方法
//type Book struct{
//	//Price int64 // ?区分默认值和0
//	// Price sql.NullInt64 // 第一种:自定义结构体
//	Price *int64 // 第二种:指针
//}
//
//func foo(){
//	var book Book
//	if book.Price == nil{
//
//	}else{
//
//	}
//}func oneofDemo(){// clientres1 := &api.NoticeReaderRequest{Msg: "田毅的博客更新了",NoticeWay: &api.NoticeReaderRequest_Email{Email: "123@xxx.com",},}//res2 := &api.NoticeReaderRequest{//	Msg: "李文周的博客更新了",//	NoticeWay: &api.NoticeReaderRequest_Phone{//		Phone: "12345645678",//	},//}// serverreq := res1// 类型断言switch v := req.NoticeWay.(type){case *api.NoticeReaderRequest_Email:noticeWithEmail(v)case *api.NoticeReaderRequest_Phone:noticeWithPhone(v)}
}func noticeWithEmail(in *api.NoticeReaderRequest_Email){fmt.Printf("notice reader by emali:%v\n",in.Email)
}func noticeWithPhone(in *api.NoticeReaderRequest_Phone){fmt.Printf("notice reader by phone:%v\n",in.Phone)
}func WrapperValueDemo(){// clientbook := api.Book{Title: "跟着天意学Go语言",//Price: &wrapperspb.Int64Value{Value: 9900},Memo: &wrapperspb.StringValue{Value: "学就完事了"},}// serverif book.GetPrice() == nil{fmt.Println("没有给price赋值")}else {fmt.Println("拿到值了", book.GetPrice().GetValue())}if book.GetMemo() == nil{fmt.Println("没有给Memo赋值")}else {fmt.Println("拿到值了", book.GetMemo().GetValue())}
}func main(){oneofDemo()WrapperValueDemo()
}

optional

请添加图片描述

client端代码

将值通过proto包转换成指针类型以适应 option
请添加图片描述

server端代码

请添加图片描述

FieldMask

请添加图片描述
请添加图片描述

client端代码

我们通过paths记录本次更新的字段路径,如果是嵌套的消息类型则通过x.y的方式标识。

// clientpaths := []string{"price", "info.b","author","info.a"} // 记录更新的字段路径updateReq := api.UpdateBookRequest{Book: &api.Book{Author: "七米2号",Price: proto.Int64(8800),Info: &api.Book_Info{B:"bbbb",A: "aaaa",},},UpdateMask: &fieldmaskpb.FieldMask{Paths: paths},}
server端代码

在收到更新消息后,我们需要根据UpdateMask字段中记录的更新路径去读取更新数据。这里借助第三方库github.com/mennanov/fieldmask-utils实现。

// servermask, _ := 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 bookDstfmt.Printf("bookDst:%#v\n", bookDst)

2022-11-20更新:由于github.com/golang/protobuf/protoc-gen-go/generator包已弃用,而MaskFromProtoFieldMask函数(签名如下)

func MaskFromProtoFieldMask(fm *field_mask.FieldMask, naming func(string) string) (Mask, error)

接收的naming参数本质上是一个将字段掩码字段名映射到 Go 结构中使用的名称的函数,它必须根据你的实际需求实现。

例如在我们这个示例中,还可以使用github.com/iancoleman/strcase包提供的ToCamel方法:

import "github.com/iancoleman/strcase"
import fieldmask_utils "github.com/mennanov/fieldmask-utils"mask, _ := fieldmask_utils.MaskFromProtoFieldMask(updateReq.UpdateMask, strcase.ToCamel)
var bookDst = make(map[string]interface{})
// 将数据读取到map[string]interface{}
// fieldmask-utils支持读取到结构体等,更多用法可查看文档。
fieldmask_utils.StructToMap(mask, updateReq.Book, bookDst)
// do update with bookDst![请添加图片描述](https://img-blog.csdnimg.cn/6dc9db34371040e397a9128d883842e2.png)fmt.Printf("bookDst:%#v\n", bookDst)

3、GRPC

gRPC是什么

gRPC是一种现代化开源的高性能RPC框架,能够运行于任意环境之中。最初由谷歌进行开发。它使用HTTP/2作为传输协议。
HTTP/2 相比 1.0 有哪些重大改进?

在gRPC里,客户端可以像调用本地方法一样直接调用其他机器上的服务端应用程序的方法,帮助你更容易创建分布式应用程序和服务。与许多RPC系统一样,gRPC是基于定义一个服务,指定一个可以远程调用的带有参数和返回类型的的方法。在服务端程序中实现这个接口并且运行gRPC服务处理客户端调用。在客户端,有一个stub提供和服务端相同的方法。
请添加图片描述

为什么要用gRPC

使用gRPC, 我们可以一次性的在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端,反过来,它们可以应用在各种场景中,从Google的服务器到你自己的平板电脑—— gRPC帮你解决了不同语言及环境间通信的复杂性。使用protocol buffers还能获得其他好处,包括高效的序列化,简单的IDL以及容易进行接口更新。总之一句话,使用gRPC能让我们更容易编写跨语言的分布式代码。

使用gRPC, 我们可以一次性的在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端,反过来,它们可以应用在各种场景中,从Google的服务器到你自己的平板电脑—— gRPC帮你解决了不同语言及环境间通信的复杂性。使用protocol buffers还能获得其他好处,包括高效的序列化,简单的IDL以及容易进行接口更新。总之一句话,使用gRPC能让我们更容易编写跨语言的分布式代码。

安装gRPC

请添加图片描述

安装Protocol Buffers v3

安装用于生成gRPC服务代码的协议编译器,最简单的方法是从下面的链接:https://github.com/google/protobuf/releases下载适合你平台的预编译好的二进制文件(protoc--.zip)。

  • 适用Windows 64位protoc-3.20.1-win64.zip
  • 适用于Mac Intel 64位protoc-3.20.1-osx-x86_64.zip
  • 适用于Mac ARM 64位protoc-3.20.1-osx-aarch_64.zip
  • 适用于Linux 64位protoc-3.20.1-linux-x86_64.zip
    例如,我使用 Intel 芯片的 Mac 系统则下载 protoc-3.20.1-osx-x86_64.zip 文件,解压之后得到如下内容。

请添加图片描述
请添加图片描述

安装插件

请添加图片描述

检查

请添加图片描述

gRPC的开发方式

请添加图片描述

编写.proto文件定义服务(grpc有1四种工作模式)

像许多 RPC 系统一样,gRPC 基于定义服务的思想,指定可以通过参数和返回类型远程调用的方法。默认情况下,gRPC 使用 protocol buffers作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构。可以根据需要使用其他的IDL代替。

例如,下面使用 protocol buffers 定义了一个HelloService服务。

service HelloService {rpc SayHello (HelloRequest) returns (HelloResponse);
}message HelloRequest {string greeting = 1;
}message HelloResponse {string reply = 1;
}

请添加图片描述

生成指定语言的代码

请添加图片描述

编写业务逻辑代码

gRPC 帮我们解决了 RPC 中的服务调用、数据传输以及消息编解码,我们剩下的工作就是要编写业务逻辑代码。

在服务端编写业务代码实现具体的服务方法,在客户端按需调用这些方法。

gRPC入门示例

编写proto代码

Protocol Buffers是一种与语言无关,平台无关的可扩展机制,用于序列化结构化数据。使用Protocol Buffers可以一次定义结构化的数据,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。

关于Protocol Buffers的教程可以查看Protocol Buffers V3中文指南,本文后续内容默认读者熟悉Protocol Buffers。

syntax = "proto3"; // 版本声明option go_package = "hello_server/proto"; // 项目中import导入生成的Go代码的名称package proto; // proto文件模块// 定义服务
service Greeter {// 定义方法rpc SayHello (HelloRequest)returns(HelloResponse){}
}// 定义请求消息
message HelloRequest{string name = 1; // 字段序号
}
// 定义相应消息
message HelloResponse{string reply = 1;
}
编写Server端Go代码

我们新建一个hello_server项目,在项目根目录下执行go mod init hello_server。

再新建一个proto文件夹,将上面的 proto 文件保存为hello.proto,将go_package按如下方式修改。

// ...option go_package = "hello_server/proto";// ...

此时,项目的目录结构为:

hello_server
├── go.mod
├── go.sum
├── main.go
![请添加图片描述](https://img-blog.csdnimg.cn/85a35db64d28484ebcb60b5a2fe84986.png)
└── pb└── hello.proto

在项目根目录下执行以下命令,根据hello.proto生成 go 源码文件。

protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
pb/hello.proto
package mainimport ("context""fmt""google.golang.org/grpc""hello_server/proto""net"
)
// grpc servertype server struct{proto.UnimplementedGreeterServer
}// SayHello 是我们需要实现的方法
// 这个方法是我们对外提供的服务
func (s *server)SayHello(ctx context.Context, in *proto.HelloRequest) (*proto.HelloResponse, error){reply := "hello" + in.GetName()return &proto.HelloResponse{Reply: reply}, nil
}func main(){// 启动服务l, err := net.Listen("tcp",":8972")if err != nil {fmt.Println("net.Listen falied, err=", err)return}// 创建grpc服务s := grpc.NewServer()// 注册服务proto.RegisterGreeterServer(s,&server{})// 启动服务err = s.Serve(l)if err != nil {fmt.Println("s.Serve(l) failed,err=", err)return}
}
编写Client端Go代码

请添加图片描述

请添加图片描述
在http_client/main.go文件中按下面的代码调用http_server提供的 SayHello RPC服务。

package mainimport ("context""flag""google.golang.org/grpc""google.golang.org/grpc/credentials/insecure""hello_client/proto""log""time"
)var name = flag.String("name","天意","通过-name 告诉server你是谁?")// grpc 客户端
// 调用server端的 SayHello 方法func main(){flag.Parse() // 解析命令行参数// 连接server端conn,err:=grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))if err != nil {log.Printf("grpc.Dial failed,err=%v\n", err)return}defer conn.Close()// 创建客户端c := proto.NewGreeterClient(conn) // 使用生成的Go代码// 调用rpc方法ctx, cancel := context.WithTimeout(context.Background(),time.Second)defer cancel()resp,err := c.SayHello(ctx, &proto.HelloRequest{Name: *name})if err != nil {log.Printf("c.SayHello failed,err=%v\n", err)return}// 拿到了RPC响应log.Printf("resp:%v\n",resp.GetReply())
}![请添加图片描述](https://img-blog.csdnimg.cn/247396d19ee84e7eb1bc2f4dada0c0d1.png)
gRPC跨语言调用

请添加图片描述

生成Python代码

新建一个py_client目录,将hello.proto文件保存到py_client/pb/目录下。 在py_client目录下执行以下命令,生成python源码文件。

cd py_cleint
python3 -m grpc_tools.protoc -Ipb --python_out=. --grpc_python_out=. pb/hello.proto
编写Python版RPC客户端

将下面的代码保存到py_client/client.py文件中。

from __future__ import print_functionimport loggingimport grpc
import hello_pb2
import hello_pb2_grpcdef run():# NOTE(gRPC Python Team): .close() is possible on a channel and should be# used in circumstances in which the with statement does not fit the needs# of the code.with grpc.insecure_channel('127.0.0.1:8972') as channel:stub = hello_pb2_grpc.GreeterStub(channel)resp = stub.SayHello(hello_pb2.HelloRequest(name='q1mi'))print("Greeter client received: " + resp.reply)if __name__ == '__main__':logging.basicConfig()run()

请添加图片描述

Python RPC 调用

请添加图片描述
gRPC_demo完整代码

4、gRPC流式示例

在上面的示例中,客户端发起了一个RPC请求到服务端,服务端进行业务处理并返回响应给客户端,这是gRPC最基本的一种工作方式(Unary RPC)。除此之外,依托于HTTP2,gRPC还支持流式RPC(Streaming RPC)。

服务端流式RPC

客户端发出一个RPC请求,服务端与客户端之间建立一个单向的流,服务端可以向流中写入多个响应消息,最后主动关闭流;而客户端需要监听这个流,不断获取响应直到流关闭。应用场景举例:客户端向服务端发送一个股票代码,服务端就把该股票的实时数据源源不断的返回给客户端。

我们在此编写一个使用多种语言打招呼的方法,客户端发来一个用户名,服务端分多次返回打招呼的信息。

1、定义服务

请添加图片描述

2、服务端需要实现 LotsOfReplies 方法
// 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(),}// 使用Send方法返回多个数据if err := stream.Send(data); err != nil {return err}}return nil
}
3.客户端调用LotsOfReplies 并将收到的数据依次打印出来
func runLotsOfReplies(c pb.GreeterClient) {// server端流式RPCctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()stream, err := c.LotsOfReplies(ctx, &pb.HelloRequest{Name: *name})if err != nil {log.Fatalf("c.LotsOfReplies failed, err: %v", err)}for {// 接收服务端返回的流式数据,当收到io.EOF或错误时退出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

客户端传入多个请求对象,服务端返回一个响应结果。典型的应用场景举例:物联网终端向服务器上报数据、大数据流式计算等。

在这个示例中,我们编写一个多次发送人名,服务端统一返回一个打招呼消息的程序。

1.定义服务

请添加图片描述

1.2.服务端实现LotsOfGreetings方法
// 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()}
}  
3.客户端调用LotsOfGreetings方法,向服务端发送流式请求数据,接收返回值并打印
func runLotsOfGreeting(c pb.GreeterClient) {ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()// 客户端流式RPCstream, 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即客户端和服务端均为流式的RPC,能发送多个请求对象也能接收到多个响应对象。典型应用示例:聊天应用等。

我们这里还是编写一个客户端和服务端进行人机对话的双向流式RPC示例。

1.定义服务

请添加图片描述

2.服务端实现BidiHello方法
// BidiHello 双向流式打招呼
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.Send(&pb.HelloResponse{Reply: reply}); err != nil {return err}}
}

这里我们还定义了一个处理数据的magic函数,其内容如下。

// 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
}
3.客户端调用BidiHello方法,一边从终端获取输入的请求数据发送至服务端,一边从服务端接收流式响应
func runBidiHello(c pb.GreeterClient) {ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)defer cancel()// 双向流模式stream, err := c.BidiHello(ctx)if err != nil {log.Fatalf("c.BidiHello failed, err: %v", err)}waitc := make(chan struct{})go func() {for {// 接收服务端返回的响应in, err := stream.Recv()if err == io.EOF {// read done.close(waitc)return}if err != nil {log.Fatalf("c.BidiHello stream.Recv() failed, err: %v", err)}fmt.Printf("AI:%s\n", in.GetReply())}}()// 从标准输入获取用户输入reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象for {cmd, _ := reader.ReadString('\n') // 读到换行cmd = strings.TrimSpace(cmd)if len(cmd) == 0 {continue}if strings.ToUpper(cmd) == "QUIT" {break}// 将获取到的数据发送至服务端if err := stream.Send(&pb.HelloRequest{Name: cmd}); err != nil {log.Fatalf("c.BidiHello stream.Send(%v) failed: %v", cmd, err)}}stream.CloseSend()<-waitc
}

请添加图片描述

5、metadata

元数据(metadata)是指在处理RPC请求和响应过程中需要但又不属于具体业务(例如身份验证详细信息)的信息,采用键值对列表的形式,其中键是string类型,值通常是[]string类型,但也可以是二进制数据。gRPC中的 metadata 类似于我们在 HTTP headers中的键值对,元数据可以包含认证token、请求标识和监控标签等。

metadata中的键是大小写不敏感的,由字母、数字和特殊字符-、_、.组成并且不能以grpc-开头(gRPC保留自用),二进制值的键名必须以-bin结尾。

元数据对 gRPC 本身是不可见的,我们通常是在应用程序代码或中间件中处理元数据,我们不需要在.proto文件中指定元数据。
如何访问元数据取决于具体使用的编程语言。 在Go语言中我们是用google.golang.org/grpc/metadata这个库来操作metadata。

请添加图片描述
元数据可以像普通map一样读取。注意,这个 map 的值类型是[]string,因此用户可以使用一个键附加多个值。

创建新的metadata

请添加图片描述

元数据中存储二进制数据

请添加图片描述

从请求上下文中获取元数据

请添加图片描述

发送和接收元数据-客户端

发送metadata

请添加图片描述
请添加图片描述

接收metadata

请添加图片描述

请添加图片描述

发送和接收元数据-服务器端

请添加图片描述

请添加图片描述

普通RPC调用metadata示例

client端的metadata操作

// unaryCallWithMetadata 普通RPC调用客户端metadata操作
func unaryCallWithMetadata(c pb.GreeterClient, name string) {fmt.Println("--- UnarySayHello client---")// 创建metadatamd := metadata.Pairs("token", "app-test-q1mi","request_id", "1234567",)// 基于metadata创建context.ctx := metadata.NewOutgoingContext(context.Background(), md)// RPC调用var header, trailer metadata.MDr, err := c.SayHello(ctx,&pb.HelloRequest{Name: name},grpc.Header(&header),   // 接收服务端发来的headergrpc.Trailer(&trailer), // 接收服务端发来的trailer)if err != nil {log.Printf("failed to call SayHello: %v", err)return}// 从header中取locationif t, ok := header["location"]; ok {fmt.Printf("location from header:\n")for i, e := range t {fmt.Printf(" %d. %s\n", i, e)}} else {log.Printf("location expected but doesn't exist in header")return}// 获取响应结果fmt.Printf("got response: %s\n", r.Reply)// 从trailer中取timestampif t, ok := trailer["timestamp"]; ok {fmt.Printf("timestamp from trailer:\n")for i, e := range t {fmt.Printf(" %d. %s\n", i, e)}} else {log.Printf("timestamp expected but doesn't exist in trailer")}
}

server端metadata操作
下面的代码片段演示了server端如何设置和获取metadata。

// UnarySayHello 普通RPC调用服务端metadata操作
func (s *server) UnarySayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {// 通过defer中设置trailer.defer func() {trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))grpc.SetTrailer(ctx, trailer)}()// 从客户端请求上下文中读取metadata.md, ok := metadata.FromIncomingContext(ctx)if !ok {return nil, status.Errorf(codes.DataLoss, "UnarySayHello: failed to get metadata")}if t, ok := md["token"]; ok {fmt.Printf("token from metadata:\n")if len(t) < 1 || t[0] != "app-test-q1mi" {return nil, status.Error(codes.Unauthenticated, "认证失败")}}// 创建和发送header.header := metadata.New(map[string]string{"location": "BeiJing"})grpc.SendHeader(ctx, header)fmt.Printf("request received: %v, say hello...\n", in)return &pb.HelloResponse{Reply: in.Name}, nil
}

流式RPC调用metadata示例

请添加图片描述

// bidirectionalWithMetadata 流式RPC调用客户端metadata操作
func bidirectionalWithMetadata(c pb.GreeterClient, name string) {// 创建metadata和context.md := metadata.Pairs("token", "app-test-q1mi")ctx := metadata.NewOutgoingContext(context.Background(), md)// 使用带有metadata的context执行RPC调用.stream, err := c.BidiHello(ctx)if err != nil {log.Fatalf("failed to call BidiHello: %v\n", err)}go func() {// 当header到达时读取header.header, err := stream.Header()if err != nil {log.Fatalf("failed to get header from stream: %v", err)}// 从返回响应的header中读取数据.if l, ok := header["location"]; ok {fmt.Printf("location from header:\n")for i, e := range l {fmt.Printf(" %d. %s\n", i, e)}} else {log.Println("location expected but doesn't exist in header")return}// 发送所有的请求数据到server.for i := 0; i < 5; i++ {if err := stream.Send(&pb.HelloRequest{Name: name}); err != nil {log.Fatalf("failed to send streaming: %v\n", err)}}stream.CloseSend()}()// 读取所有的响应.var rpcStatus errorfmt.Printf("got response:\n")for {r, err := stream.Recv()if err != nil {rpcStatus = errbreak}fmt.Printf(" - %s\n", r.Reply)}if rpcStatus != io.EOF {log.Printf("failed to finish server streaming: %v", rpcStatus)return}// 当RPC结束时读取trailertrailer := stream.Trailer()// 从返回响应的trailer中读取metadata.if t, ok := trailer["timestamp"]; ok {fmt.Printf("timestamp from trailer:\n")for i, e := range t {fmt.Printf(" %d. %s\n", i, e)}} else {log.Printf("timestamp expected but doesn't exist in trailer")}
}

请添加图片描述

// BidirectionalStreamingSayHello 流式RPC调用客户端metadata操作
func (s *server) BidirectionalStreamingSayHello(stream pb.Greeter_BidiHelloServer) error {// 在defer中创建trailer记录函数的返回时间.defer func() {trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))stream.SetTrailer(trailer)}()// 从client读取metadata.md, ok := metadata.FromIncomingContext(stream.Context())if !ok {return status.Errorf(codes.DataLoss, "BidirectionalStreamingSayHello: failed to get metadata")}if t, ok := md["token"]; ok {fmt.Printf("token from metadata:\n")for i, e := range t {fmt.Printf(" %d. %s\n", i, e)}}// 创建和发送header.header := metadata.New(map[string]string{"location": "X2Q"})stream.SendHeader(header)// 读取请求数据发送响应数据.for {in, err := stream.Recv()if err == io.EOF {return nil}if err != nil {return err}fmt.Printf("request received %v, sending reply\n", in)if err := stream.Send(&pb.HelloResponse{Reply: in.Name}); err != nil {return err}}

6、错误处理

gRPC code

类似于HTTP定义了一套响应状态码,gRPC也定义有一些状态码。Go语言中此状态码由codes定义,本质上是一个uint32。
请添加图片描述

gRPC Status

Go语言使用的gRPC Status 定义在google.golang.org/grpc/status,使用时需导入。

import “google.golang.org/grpc/status”
RPC服务的方法应该返回 nil 或来自status.Status类型的错误。客户端可以直接访问错误。

请添加图片描述

代码示例

我们现在要为hello服务设置访问限制,每个name只能调用一次SayHello方法,超过此限制就返回一个请求超过限制的错误。

服务端

使用map存储每个name的请求次数,超过1次则返回错误,并且记录错误详情。

**package mainimport ("context""fmt""hello_server/pb""net""sync""google.golang.org/genproto/googleapis/rpc/errdetails""google.golang.org/grpc""google.golang.org/grpc/codes""google.golang.org/grpc/status"
)// grpc servertype server struct {pb.UnimplementedGreeterServermu    sync.Mutex     // count的并发锁count map[string]int // 记录每个name的请求次数
}// SayHello 是我们需要实现的方法
// 这个方法是我们对外提供的服务
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {s.mu.Lock()defer s.mu.Unlock()s.count[in.Name]++ // 记录用户的请求次数// 超过1次就返回错误if s.count[in.Name] > 1 {st := status.New(codes.ResourceExhausted, "Request limit exceeded.")ds, err := st.WithDetails(&errdetails.QuotaFailure{Violations: []*errdetails.QuotaFailure_Violation{{Subject:     fmt.Sprintf("name:%s", in.Name),Description: "限制每个name调用一次",}},},)if err != nil {return nil, st.Err()}return nil, ds.Err()}// 正常返回响应reply := "hello " + in.GetName()return &pb.HelloResponse{Reply: reply}, nil
}func main() {// 启动服务l, err := net.Listen("tcp", ":8972")if err != nil {fmt.Printf("failed to listen, err:%v\n", err)return}s := grpc.NewServer() // 创建grpc服务// 注册服务,注意初始化countpb.RegisterGreeterServer(s, &server{count: make(map[string]int)})// 启动服务err = s.Serve(l)if err != nil {fmt.Printf("failed to serve,err:%v\n", err)return}
}
客户端

当服务端返回错误时,尝试从错误中获取detail信息。

package mainimport ("context""flag""fmt""google.golang.org/grpc/status""hello_client/pb""log""time""google.golang.org/genproto/googleapis/rpc/errdetails""google.golang.org/grpc""google.golang.org/grpc/credentials/insecure"
)// grpc 客户端
// 调用server端的 SayHello 方法var name = flag.String("name", "七米", "通过-name告诉server你是谁")func main() {flag.Parse() // 解析命令行参数// 连接serverconn, 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 := pb.NewGreeterClient(conn) // 使用生成的Go代码// 调用RPC方法ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})if err != nil {s := status.Convert(err)        // 将err转为statusfor _, d := range s.Details() { // 获取detailsswitch info := d.(type) {case *errdetails.QuotaFailure:fmt.Printf("Quota failure: %s\n", info)default:fmt.Printf("Unexpected type: %s\n", info)}}fmt.Printf("c.SayHello failed, err:%v\n", err)return}// 拿到了RPC响应log.Printf("resp:%v\n", resp.GetReply())
}

7、加密或认证

无加密认证

在上面的示例中,我们都没有为我们的 gRPC 配置加密或认证,属于不安全的连接(insecure connection)。
Client端:

conn, _ := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
client := pb.NewGreeterClient(conn)

Server端:

s := grpc.NewServer()
lis, _ := net.Listen("tcp", "127.0.0.1:8972")
// error handling omitted
s.Serve(lis)

使用服务器身份验证 SSL/TLS

gRPC 内置支持 SSL/TLS,可以通过 SSL/TLS 证书建立安全连接,对传输的数据进行加密处理。

这里我们演示如何使用自签名证书进行server端加密。

生成证书

生成私钥

请添加图片描述
生成自签名的证书
请添加图片描述

transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0

为了在证书中添加SANs信息,我们将下面自定义配置保存到server.cnf文件中。

[ req ]
default_bits       = 4096
default_md		= sha256
distinguished_name = req_distinguished_name
req_extensions     = req_ext[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = BEIJING
localityName                = Locality Name (eg, city)
localityName_default        = BEIJING
organizationName            = Organization Name (eg, company)
organizationName_default    = DEV
commonName                  = Common Name (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = liwenzhou.com[ req_ext ]
subjectAltName = @alt_names[alt_names]
DNS.1   = localhost
DNS.2   = liwenzhou.com
IP      = 127.0.0.1

执行下面的命令生成自签名证书——server.crt。

openssl req -nodes -new -x509 -sha256 -days 3650 -config server.cnf -extensions 'req_ext' -key server.key -out server.crt 

建立安全连接

请添加图片描述

8、拦截器(中间件)

gRPC 为在每个 ClientConn/Server 基础上实现和安装拦截器提供了一些简单的 API。 拦截器拦截每个 RPC 调用的执行。用户可以使用拦截器进行日志记录、身份验证/授权、指标收集以及许多其他可以跨 RPC 共享的功能。

在 gRPC 中,拦截器根据拦截的 RPC 调用类型可以分为两类。第一个是普通拦截器(一元拦截器),它拦截普通RPC 调用。另一个是流拦截器,它处理流式 RPC 调用。而客户端和服务端都有自己的普通拦截器和流拦截器类型。因此,在 gRPC 中总共有四种不同类型的拦截器。

客户端端拦截器

普通拦截器/一元拦截器

UnaryClientInterceptor是客户端一元拦截器的类型,它的函数前面如下:

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

请添加图片描述

流拦截器

StreamClientInterceptor是客户端流拦截器的类型。它的函数签名是

func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)

请添加图片描述

server端拦截器

服务器端拦截器与客户端类似,但提供的信息略有不同。

普通拦截器/一元拦截器

UnaryServerInterceptor是服务端的一元拦截器类型,它的函数签名是

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

StreamServerInterceptor是服务端的一元拦截器类型,它的函数签名是

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

实现细节类似于客户端流拦截器部分。

若要为服务端安装流拦截器,请使用 StreamInterceptor 的ServerOption来配置 NewServer。

拦截器示例

下面将演示一个完整的拦截器示例,我们为一元RPC和流式RPC服务都添加上拦截器。

我们首先定义一个名为valid的校验函数。

// valid 校验认证信息.
func valid(authorization []string) bool {if len(authorization) < 1 {return false}token := strings.TrimPrefix(authorization[0], "Bearer ")// 执行token认证的逻辑// 这里是为了演示方便简单判断token是否与"some-secret-token"相等return token == "some-secret-token"
}
客户端拦截器定义

一元拦截器

// unaryInterceptor 客户端一元拦截器
func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {var credsConfigured boolfor _, o := range opts {_, ok := o.(grpc.PerRPCCredsCallOption)if ok {credsConfigured = truebreak}}if !credsConfigured {opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{AccessToken: "some-secret-token",})))}start := time.Now()err := invoker(ctx, method, req, reply, cc, opts...)end := time.Now()fmt.Printf("RPC: %s, start time: %s, end time: %s, err: %v\n", method, start.Format("Basic"), end.Format(time.RFC3339), err)return err
}

流式拦截器

请添加图片描述

func (w *wrappedStream) RecvMsg(m interface{}) error {logger("Receive a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))return w.ClientStream.RecvMsg(m)
}func (w *wrappedStream) SendMsg(m interface{}) error {logger("Send a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))return w.ClientStream.SendMsg(m)
}func newWrappedStream(s grpc.ClientStream) grpc.ClientStream {return &wrappedStream{s}
}

这里的wrappedStream嵌入了grpc.ClientStream接口类型,然后又重新实现了一遍grpc.ClientStream接口的方法。

下面就定义一个流式拦截器,最后返回上面定义的wrappedStream。

// 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 boolfor _, o := range opts {_, ok := o.(*grpc.PerRPCCredsCallOption)if ok {credsConfigured = truebreak}}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}return newWrappedStream(s), nil
}
服务端拦截器定义

一元拦截器
服务端定义一个一元拦截器,对从请求元数据中获取的authorization进行校验。

// unaryInterceptor 服务端一元拦截器
func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {// authentication (token verification)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")}m, err := handler(ctx, req)if err != nil {fmt.Printf("RPC failed with error %v\n", err)}return m, err
}

流拦截器
同样为流RPC也定义一个从元数据中获取认证信息的流式拦截器。

// streamInterceptor 服务端流拦截器
func streamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {// authentication (token verification)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
}
注册拦截器

客户端注册拦截器

conn, err := grpc.Dial("127.0.0.1:8972",grpc.WithTransportCredentials(creds),grpc.WithUnaryInterceptor(unaryInterceptor),grpc.WithStreamInterceptor(streamInterceptor),
)

服务端注册拦截器

s := grpc.NewServer(grpc.Creds(creds),grpc.UnaryInterceptor(unaryInterceptor),grpc.StreamInterceptor(streamInterceptor),
)

go-grpc-middleware

社区中有很多开源的常用的grpc中间件— https://github.com/grpc-ecosystem/go-grpc-middleware 可以根据需要选择使用。

9、gRPC-Gateway

gRPC-Gateway介绍

gRPC-Gateway 是一个 protoc 插件。它读取 gRPC 服务定义并生成一个反向代理服务器,该服务器将 RESTful JSON API 转换为 gRPC。此服务器根据 gRPC 定义中的自定义选项生成。

鉴于复杂的外部环境 gRPC 并不是万能的工具。在某些情况下,我们仍然希望提供传统的 HTTP/JSON API,来满足维护向后兼容性或者那些不支持 gRPC 的客户端。但是为我们的RPC服务再编写另一个服务只是为了对外提供一个 HTTP/JSON API,这是一项相当耗时和乏味的任务。

GRPC-Gateway 能帮助你同时提供 gRPC 和 RESTful 风格的 API。GRPC-Gateway 是 Google protocol buffers 编译器 protoc 的一个插件。它读取 Protobuf 服务定义并生成一个反向代理服务器,该服务器将 RESTful HTTP API 转换为 gRPC。该服务器是根据服务定义中的 google.api.http 注释生成的。

请添加图片描述

基本使用示例

使用protobuf定义 gRPC 服务

新建一个项目greeter,在项目目录下执行go mod init命令完成go module初始化。

在项目目录下创建一个proto/helloworld/hello_world.proto文件,其内容如下。

syntax = "proto3";package helloworld;option go_package="github.com/Q1mi/greeter/proto/helloworld";// 定义一个Greeter服务
service Greeter {// 打招呼方法rpc SayHello (HelloRequest) returns (HelloReply) {}
}// 定义请求的message
message HelloRequest {string name = 1;
}// 定义响应的message
message HelloReply {string message = 1;
}
生成代码
 protoc -I=proto \--go_out=proto --go_opt=paths=source_relative \--go-grpc_out=proto --go-grpc_opt=paths=source_relative \helloworld/hello_world.proto

生成pb和gRPC相关代码后,在main函数中注册RPC服务并启动gRPC Server。

// greeter/main.gopackage mainimport ("context""log""net""google.golang.org/grpc"helloworldpb "github.com/Q1mi/greeter/proto/helloworld"
)type server struct {helloworldpb.UnimplementedGreeterServer
}func NewServer() *server {return &server{}
}func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) {return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil
}func main() {// Create a listener on TCP portlis, err := net.Listen("tcp", ":8080")if err != nil {log.Fatalln("Failed to listen:", err)}// 创建一个gRPC server对象s := grpc.NewServer()// 注册Greeter service到serverhelloworldpb.RegisterGreeterServer(s, &server{})// 启动gRPC Serverlog.Println("Serving gRPC on 0.0.0.0:8080")log.Fatal(s.Serve(lis))
}

请添加图片描述

将 gRPC-Gateway 注释添加到现有的proto文件
生成gRPC-Gateway stubs
添加HTTP Server代码
测试gRPC-Gateway
同一个端口提供HTTP API和gRPC API

10、分页

11、gRPC中的名称解析和负载均衡

相关文章:

G0第26章:微服务概述与gRPCprotocol buffers

Go微服务与云原生 1、微服务架构介绍 单体架构&#xff08;电商&#xff09; SOA架构&#xff08;电商&#xff09; 微服务架构&#xff08;电商&#xff09; 优势 挑战 拆分 发展史 第一代:基于RPC的传统服务架构 第二代:Service Mesh(istio) 微服务架构分层 核心组件 Summar…...

三款远程控制软件对比,5大挑选指标:安全、稳定、易用、兼容、功能

陈老老老板&#x1f934; &#x1f9d9;‍♂️本文专栏&#xff1a;生活&#xff08;主要讲一下自己生活相关的内容&#xff09;生活就像海洋,只有意志坚强的人,才能到达彼岸。 &#x1f9d9;‍♂️本文简述&#xff1a;三款远程控制软件对比&#xff0c;5大挑选指标&#xff1…...

Java中static的应用之单例模式

单例模式是一种创建对象的设计模式&#xff0c;它保证一个类只有一个实例&#xff0c;并提供一个全局访问点。由于单例模式只允许存在一个实例&#xff0c;因此它可以节省系统资源并提高程序的性能。在许多情况下&#xff0c;单例模式在应用程序中都是非常有用的&#xff0c;例…...

TypeError: Cannot read properties of undefined (reading ‘container‘)

问题环境&#xff1a; element项目 el-table的错误 项目是由 webpack项目迁移为 vite项目 问题描述&#xff1a; errorLog.js?t1692581753160:17 TypeError: Cannot read properties of undefined (reading container) at unbind (infinite-scroll.js:259:31) …...

Vue--BM记事本

效果如下&#xff1a; 用到了如下的技术&#xff1a; 1.列表渲染&#xff1a;v-for key的设置 2.删除功能&#xff1a;v-on调用参数 fliter过滤 覆盖修改原数组 3.添加功能&#xff1a;v-model绑定&#xff0c;unshift修改原数组添加 html文件如下&#xff1a; <!DOCTYPE …...

openpnp - 板子上最小物料封装尺寸的选择

文章目录 openpnp - 板子上最小物料封装尺寸的选择概述END openpnp - 板子上最小物料封装尺寸的选择 概述 现在设备调试完了, 用散料飞达载入物料试了一下. 0402以上贴的贴别准, 贴片流程也稳, 基本不需要手工干预. 0201可以贴, 但是由于底部相机元件视觉识别成功率不是很高…...

什么是非功能性需求,它们如何影响产品开发?

我们在选购新车时&#xff0c;会预设一些选购的标准&#xff0c;比如GPS导航必须能够保存目的地&#xff0c;或者必须要买黑色的车。我们可能下意识以为这些是功能性需求&#xff0c;但实际上这些特性都是与用户体验相关的非功能性需求。 一、什么是非功能性需求(NFR)? 非功…...

Oracle jdk8 exe->zip

一、背景 目前Oracle网站对应jdk8安装windows仅存在exe安装包&#xff0c;对于某些用户一台机器上对应jdk版本需动态切换&#xff0c;故需使用zip版本jdk&#xff0c;更加方便&#xff0c;本文介绍如何从jdk对应exe提取zip。 二、步骤 下载jdk8对应exe安装包&#xff1b;使用…...

Android 命令行如何运行 JAR 文件

​ 最近有位老哥问了一个问题&#xff0c;说如果将java的jar文件在Android中执行&#xff1f;这个其实很简单的一个问题&#xff0c;直接写个App放里面不就可以了么&#xff1f;但是人家说没有App&#xff0c;直接使用命令行去运行。说明这个需求的时候&#xff0c;把我给整懵了…...

5.4 webrtc的线程

那今天呢&#xff1f;我们来了解一下webrtc中的threed&#xff0c;首先我们看一下threed的类&#xff0c;它里边儿都含了哪些内容&#xff1f;由于threed的类非常大啊&#xff0c;我们将它分成两部分。 那第一部分呢&#xff0c;是我们看threed的类中都包含了哪些数据之后呢&a…...

vscode | linux | c++ intelliense 被弃用解决方案

每日一句&#xff0c;vscode用的爽是爽&#xff0c;主要是可配置太强了。如果也很会研究&#xff0c;可以直接去咸鱼接单了 废话少说&#xff0c;直接整。 用着用着说是c intelliense被弃用&#xff0c;很多辅助功能无法使用&#xff0c;像查看定义、查看引用、函数跳转、智能提…...

HPE服务器常见报错信息以及解决方案

General controller issues 常规控制器问题 Controllers are no longer redundant 控制器不再冗余 HPE Dynamic Smart Array B140i drives are not found when RAID mode is disabled 禁用 RAID 模式时找不到 HPE 动态智能阵列 B140i 驱动器 Data located on drives accessed i…...

尚硅谷宋红康MySQL笔记 3-9

我不会记录的特别详细 大体框架 基本的Select语句运算符排序与分页多表查询单行函数聚合函数子查询 第三章 基本的SELECT语句 SQL分类 这个分类有很多种&#xff0c;大致了解下即可 DDL&#xff08;Data Definition Languages、数据定义语言&#xff09;&#xff0c;定义了…...

Leetcode.2337 移动片段得到字符串

题目链接 Leetcode.2337 移动片段得到字符串 rating : 1693 题目描述 给你两个字符串 start 和 target &#xff0c;长度均为 n n n 。每个字符串 仅 由字符 L、R 和 _ 组成&#xff0c;其中&#xff1a; 字符 L 和 R 表示片段&#xff0c;其中片段 L 只有在其左侧直接存在一…...

【vue】更改角色权限后,实现页面不刷新更改其可展示的导航菜单

登入的角色本身属于领导级别&#xff08;集团权限&#xff09;&#xff0c;没有下级的不同权限&#xff1a; 切换不同身份&#xff08;公司&#xff09;&#xff0c;以获得相应部门的不同导航菜单及权限 这里实现&#xff1a;更改角色权限后&#xff0c;实现页面 不刷新 更改…...

【G-LAB】网络工程师常用排错命令详细版

网络工程师在日常配置中难免出现各种配置错误&#xff0c;比如接口地址配错、掩码位数配错、接口忘记no shutdown。除去这些基础错误&#xff0c;在配置各种路由选择协议时也会因为网络类型、邻居类型、区域和路由器层级等各种问题使邻居无法建立、路由无法传递进而导致网络不通…...

Linux 桌面版关闭GUI桌面环境

持久打开和关闭 通过CtrlAltF1-F6快捷键进入命令行界面 执行以下命令&#xff0c;持久关闭Ubuntu桌面版的GUI环境&#xff1a; sudo systemctl set-default multi-user.target执行以下命令&#xff0c;持久开启Ubuntu桌面版的GUI环境 通过CtrlAltF7快捷键进入GUI界面 sudo s…...

ChatGPT能代替搜索引擎吗?ChatGPT和搜索引擎有什么区别?

ChatGPT和搜索引擎是两种在信息获取和交流中常用的工具&#xff0c;ChatGPT是一种基于人工智能技术的聊天机器人&#xff0c;而搜索引擎是一种在互联网上搜索信息的工具。尽管它们都是依托互联网与信息获取和交流有关&#xff0c;部分功能重合&#xff0c;但在很多方面存在着明…...

PHP海外代购管理系统mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP 海外代购管理系统是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 代码下载 https://download.csdn.net/download/qq_41221322/88229435 论文 https://…...

游戏反外挂方案解析

近年来&#xff0c;游戏市场高速发展&#xff0c;随之而来的还有图谋利益的游戏黑产。在利益吸引下&#xff0c;游戏黑产扩张迅猛&#xff0c;已发展成具有庞大规模的产业链&#xff0c;市面上游戏受其侵扰的案例屡见不鲜。 据《FairGuard游戏安全2022年度报告》数据统计&…...

基于郊狼算法优化的BP神经网络(预测应用) - 附代码

基于郊狼算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于郊狼算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.郊狼优化BP神经网络2.1 BP神经网络参数设置2.2 郊狼算法应用 4.测试结果&#xff1a;5.Matlab代码 摘要…...

【腾讯云 TDSQL-C Serverless 产品测评】全面测评TDSQL-C Mysql Serverless

全面测评TDSQL-C Mysql Serverless 文章目录 全面测评TDSQL-C Mysql Serverless前言什么是TDSQL-C Mysql Serverless初始化 TDSQL-C Mysql Serverless新建数据库建立数据表开启外网访问 兼容性SQL文件 导入导出navicat 直接在线传输 构建测试环境准备Python测试脚本准备 Jmeter…...

Qt应用开发(基础篇)——纯文本编辑窗口 QPlainTextEdit

一、前言 QPlainTextEdit类继承于QAbstractScrollArea&#xff0c;QAbstractScrollArea继承于QFrame&#xff0c;是Qt用来显示和编辑纯文本的窗口。 滚屏区域基类https://blog.csdn.net/u014491932/article/details/132245486?spm1001.2014.3001.5501框架类QFramehttps://blo…...

数据结构-->栈

&#x1f495;休对故人思故国&#xff0c;且将新火试新茶&#xff0c;诗酒趁年华&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;详解链表OJ题 前言&#xff1a; 前面已经学习过顺序表&#xff0c;链表。他们都是线性表&#xff0c;今天要学习的栈也是一种线…...

强训第36天

C D C 193--1100 0001 194--1100 0010 196--1100 0100 198--1100 0110 能包括全部的且最小的为 1100 0xxx xxx为主机号&#xff0c;站三位 B MAC地址是绑定网卡的&#xff0c;全球唯一 D A C D IP网段 17为网络号 所以是 40.15.1aaa aaa(7位主机号).0 因为要划分2个子网 所以…...

PyTorch bug记录

1、RuntimeError: Input type (torch.FloatTensor) and weight type (torch.cuda.FloatTensor) should be the same 这个错误是因为模型的权重是在GPU上&#xff0c;但是输入数据在CPU上。在PyTorch中&#xff0c;Tensor的类型和所在的设备&#xff08;CPU或GPU&#xff09;需…...

js中的正则表达式(一)

目录 1.什么是正则表达式 2.正则表达式在JavaScript中的使用场景: 3.正则表达式的语法&#xff1a; 1.什么是正则表达式 正则表达式(Regular Expression&#xff09;是用于匹配字符串中字符组合的模式。在JavaScript中&#xff0c;正则表达式也是对象通常用来查找、替换那些符…...

免费开源使用的几款红黑网络流量工具,自动化的多功能网络侦查工具、超级关键词URL采集工具、Burpsuite被动扫描流量转发插件

免费开源使用的几款红黑网络流量工具&#xff0c;自动化的多功能网络侦查工具、超级关键词URL采集工具、Burpsuite被动扫描流量转发插件。 #################### 免责声明&#xff1a;工具本身并无好坏&#xff0c;希望大家以遵守《网络安全法》相关法律为前提来使用该工具&am…...

使用Mybatis Plus进行DAO层开发

一、特性 Mybatis应该大家现在都知道&#xff0c;而且在项目中都在使用&#xff0c;因为这块ORM框架让大家能专心业务SQL的编写&#xff0c;数据库的连接&#xff0c;连接池的使用都不用关心&#xff0c;极大的提高了生产效率。 今天要给大家介绍的另外一款ORM框架&#xff0…...

Android中如何不编译源生模块

如果想让自己的app 替换系统的app 比如使用闪电浏览器替换系统的Browser 首先把闪电浏览器放到 vendor/rockchip/common/apps Android.mk LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS)LOCAL_MODULE : Lightning LOCAL_SRC_FILES : $(LOCAL_MODULE).apk LOCAL_MODULE_C…...

安装Vue_dev_tools

Vue控制台出现Download the Vue Devtools extension for a better development experience: 下载Vue_dev_tools,这里给出网盘链接&#xff0c;有Vue2和Vue3的&#xff0c;dev_tools 以Google浏览器为例 点击设置&#xff08;就是那三个点&#xff09;->扩展程序->管理扩…...

【数据结构入门指南】二叉树顺序结构: 堆及实现(全程配图,非常经典)

【数据结构入门指南】二叉树顺序结构: 堆及实现&#xff08;全程配图&#xff0c;非常经典&#xff09; 一、前言&#xff1a;二叉树的顺序结构二、堆的概念及结构三、堆的实现&#xff08;本篇博客以实现小堆为例&#xff09;3.1 准备工作3.2 初始化3.3 堆的插入3.3.1 向上调…...

css实现三角形的几种方法

css实现三角形的方法&#xff1a;1、使用边框实现三角形&#xff0c;利用透明边框和实色边框的组合&#xff0c;可以创建不同方向和大小的三角形&#xff1b;2、使用伪元素实现三角形&#xff0c;通过使用伪元素来创建一个占据父元素一半大小的实心三角形&#xff1b;3、使用tr…...

❤ Vue工作常用的一些动态数据和方法处理

❤ Vue工作常用的一些动态数据和方法处理 &#xff08;1&#xff09;动态拼接相对路径结尾的svg 错误写法一 ❌ 正确写法 &#x1f646; <img :src"require(/assets//amazon/svg/homemenu${index}.svg)" style"height: 20px;display: block;margin: 0 au…...

SQLite的命令用法

学习数据库直达网站 https://www.runoob.com/sqlite/sqlite-tutorial.html&#xff08;菜鸟教程&#xff09; 这里只分享&#xff0c;基础操作&#xff0c;数据库创建打开……等等 用到查菜鸟教程即可 文章目录 学习数据库直达网站创建一个数据库方式1方式2 创建一个表格插入一…...

在jupyter notebook中使用海龟绘图

首先&#xff0c;安装ipyturtle3 ref:ipyturtle3 PyPI pip install ipyturtle3然后&#xff0c;安装ipycanvas ipycanvas是一个需要安装在与JupyterLab实例相同环境的包。此外&#xff0c;您需要安装nodejs&#xff0c;并启用JupyterLab ipycanvas小部件。 所有这些都在ipy…...

密码学学习笔记(十八):Diffie–Hellman (DH) 密钥交换

DH算法是第一个密钥交换算法&#xff0c;也是第一个得到形式化描述的公钥密码算法。 群论 DH密钥交换算法基于数学中的群论&#xff0c;群论也是当今大多数公钥密码的基础。 要使集合及其运算成为一个群&#xff0c;需要满足以下性质&#xff1a; 封闭性&#xff1a;群中两…...

Linux —— 进程间通信(管道)

目录 一&#xff0c;进程间通信 二&#xff0c;管道 匿名管道 命名管道 一&#xff0c;进程间通信 进程间通信&#xff08;IPC&#xff0c;InterProcess Communication&#xff09;&#xff0c;即在不同进程之间进行信息的传播或交换&#xff1b;由于一般进程用户地址空间是…...

python常用

环境配置 conda Conda自动补全 在终端激活conda环境的时候按tab不能自动补全activate和环境名。安装后可用tab进行补全。 安装 conda-bash-completion 插件&#xff1a;GitHub 安装方法&#xff1a; conda install -c conda-forge conda-bash-completion常用命令 #创建虚拟…...

jeecg如何创建报表并配置到菜单中

当使用jeecg创建单表之后,需要进行报表显示,并把报表配置到菜单中,该如何操作呢?下面进行详细讲解。这里以课程表这张表为例进行讲解。 一.表单创建完成,并配置好菜单栏。具体步骤略,如下图: 二.创建积木报表 1.左侧边栏展开低代码开发菜单,进入报表设计器栏目 2.进…...

Servlet+JDBC实战开发书店项目讲解第12讲:会员管理功能

ServletJDBC实战开发书店项目讲解第12讲&#xff1a;会员管理功能 实现思路&#xff1a; 显示会员列表&#xff1a; 创建一个管理页面&#xff0c;用于显示所有会员的信息。在后端&#xff0c;创建一个Servlet来处理显示会员列表的请求。在该Servlet中&#xff0c;通过JDBC从数…...

java面向对象——继承以及super关键字

继承的概念 1. 被继承的类称为父类&#xff08;超类&#xff09;&#xff0c;继承父类的类都称为子类&#xff08;派生类&#xff09; 2. 继承是指一个对象直接使用另一个对象的属性和方法&#xff0c;但是能继承非私有的属性和方法&#xff1b;(1) 构造方法不能被继承。(2) 但…...

[机缘参悟-101] :IT人 - 遵从世界本源的样子,不带个人情感、道德、认知倾向,接纳一切,你就拥有无限的力量

目录 道的本义 如来的本义 观音的本义 无为而治本质是顺势而为 儒家的本质 感悟&#xff1a; 道的本义本质&#xff1a;天地的力量和运行规律 "天地以万物为刍狗"是出自《道德经》第五十章的一句话。在这句话中&#xff0c;"天地"指的是宇宙&#x…...

C++--深度理解智能指针

PS:智能指针简单应用看这里 http://t.csdn.cn/qN7IK 1.智能指针的介绍 在C中&#xff0c;智能指针有三个版本&#xff0c;分别为&#xff1a; auto_ptr unique_ptr shared_ptr 这三个版本的智能指针中&#xff0c;shared_ptr最为完善&#xff0c;auto_ptr基本上没有太大用…...

Spring Boot使用MySQL的默认连接池

笔者在近期秋招面试的时候被问到了这个问题&#xff0c;现在简单梳理一下便于后期重新回顾&#xff0c;并加深记忆。 Spring Boot 默认使用的数据库连接池是 HikariCP(开源库地址)。 HikariCP 是目前性能最好的连接池之一&#xff0c;它具有高度的性能、可靠性和可扩展性&…...

conda使用教程

Conda介绍 conda可以理解为一个工具&#xff0c;也是一个可执行命令&#xff0c;其核心功能是包管理和环境管理。包管理与pip的使用方法类似似&#xff0c;环境管理则是允许用户方便滴安装不同版本的python环境并在不同环境之间快速地切换。 conda的设计理念 conda将几乎所有…...

什么是LLM大语言模型?

什么是LLM大语言模型&#xff1f; 大语言模型&#xff08;英文&#xff1a;Large Language Model&#xff0c;缩写LLM&#xff09;&#xff0c;也称大型语言模型&#xff0c;是一种人工智能模型&#xff0c;旨在理解和生成人类语言。它们在大量的文本数据上进行训练&#xff0…...

jenkins同一jar包部署到多台服务器

文章目录 安装插件配置ssh服务构建完成后执行 没有部署过可以跟这个下面的步骤先部署一遍&#xff0c;我这篇主要讲jenkins同一jar包部署到多台服务器 【Jenkins】部署Springboot项目https://blog.csdn.net/qq_39017153/article/details/131901613 安装插件 Publish Over SSH 这…...

(四)Doceke安装MySQL镜像+Docker启动MySQL容器

Doceke安装MySQL镜像/Docker启动MySQL容器 一、doceke安装MySQL镜像 切换到root用户&#xff0c;su root 。 1、启动Docker 启动&#xff1a;sudo systemctl start docker 停止&#xff1a;systemctl stop docker 重启&#xff1a;systemctl restart docker 查看docker运行…...

Android Studio:Could not initialize class org.codehaus.groovy.vmplugin.v7.Java7

原项目使用jdk8&#xff0c;升级gradle后出现的该问题。 java.lang.NoClassDefFoundError: Could not initialize class org.codehaus.groovy.vmplugin.v7.Java7at org.codehaus.groovy.vmplugin.VMPluginFactory.<clinit>(VMPluginFactory.java:43)at org.codehaus.gro…...