一: 基本介绍
服务间通讯通常有,HTTP、RPC、消息队列、事件驱动几种方式。其中 HTTP 是最常见的方式,当前推荐使用 Restful API。除了不同的协议之外,服务间的通信还可以分类为同步通信和异步通信,一般来说,HTTP 的是典型的同步方案,而消息队列和事件驱动是典型的异步方案。同步方案服务间的耦合度相对较高,而异步通信服务间的耦合度相对较低。
同步通信 : A 服务必须得到 B 服务的结果才能进行下去 , 所以此时耦合度较高
异步通信 : A 服务和 B 服务进行通信时 , 通信完就不在等待了 , 做后续操作 , B 拿到内容做后续处理 , 有结果通知给 A
协议分类
HTTP,超文本传输协议
旧式风格,GET、POST 完成全部请求,URI 上标识对资源的操作
Restful API,HTTP API 的一种风格
RPC,远程过程调用,通常使用 gRPC ( 基于H2 )
消息队列,Message Queue : A 服务产生一个消息 , 放到队列当中 , B 拿走消息进行处理
事件驱动
同步异步
同步
HTTP,很多客户端也支持异步HTTP通讯了
RPC
异步
消息队列
事件驱动
HTTP,很多客户端也支持异步HTTP通讯了
目前的微服务架构采用服务对外是 HTTP ( 使用RestFulAPI ) , 对内是基于gRPC 的 RPC 通讯。使用消息队列完成异步消息通讯。
二: RestFulAPI
1. 详细介绍
Restful API,一种通用、流行的 API 设计风格,至少基于 HTTP/1.1,因为 1.1 中增加了若干请求方式,包含 PUT、DELETE等。其中:
REST 是 Representational State Transfer, 表述性状态转移的缩写,如果一个架构符合 REST 原则,就称它为 RESTful 架构
RESTful 架构可以充分的利用 HTTP 协议的各种功能,是 HTTP 协议的最佳实践
RESTful API 是一种软件架构风格、设计风格,可以让软件更加清晰,更简洁,更有层次,可维护性更好
如果要对 Article 进行操作 , 以下是各个请求代表的意思:
对某个资源操作采用动作加资源标识的形式。动作,使用 HTTP 的请求方式标识,资源标识用特定的URI标识,通常为复数形式
批量数据,使用 Query String 中的过滤器 filter 或 关键字 keyword 进行过滤
特定资源,在 URI 上使用资源 ID 进行标识
Restful API 还规范率标准的响应状态码 Response Status Code 来表示请求结果。
做限流操作时,如果客户端请求被限,则会响应:429 Too Many Requests
表示客户端请求过多。
除了状态要规范,响应主体通常也是 JSON 的格式进行规范。
REST 是 Representational State Transfer, 表述性状态转移的缩写,如果一个架构符合 REST 原则,就称它为 RESTful 架构。该原则具有如下特点:
表述性(Representational)是指客户端请求一个资源,服务器拿到的这个资源,就是表述
资源是REST系统的核心概念,所有的设计都是以资源为中心的
资源的地址在Web中就是URL统一资源定位符
对资源的操作不会改变标识符
2. RestFulAPI Go 编码
使用路由包实现 RestFulAPI 的编写
go get -u github.com/gorilla/mux
func Router() {
// 定义路由
router := mux.NewRouter()
// Restful API
router.HandleFunc("/articles", articlesList).Methods("GET")
router.HandleFunc("/articles/{id}", articlesRetrieve).Methods("GET")
router.HandleFunc("/articles", articlesCreate).Methods("POST")
router.HandleFunc("/articles/{id}", articlesDelete).Methods("DELETE")
router.HandleFunc("/articles/{id}", articlesUpdate).Methods("PUT")
router.HandleFunc("/articles/{id}", articlesUpdatePartial).Methods("PATCH")
log.Fatal(http.ListenAndServe(":8088", router))
}
func articlesList(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "Article Service: List articles")
}
func articlesRetrieve(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "Article Service: Retrieve articles")
}
func articlesCreate(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "Article Service: Create articles")
}
func articlesDelete(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "Article Service: Delete articles")
}
func articlesUpdate(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "Article Service: Update articles")
}
func articlesUpdatePartial(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintf(writer, "Article Service: Update Partial articles")
}
三: http 1 和 2 特性
HTTP/1.1 的典型特点:
Host 标头,通过 Host 标头可以区分虚拟主机
支持持久连接,persistent connection,默认开启
Connection: keep-alive
,即 TCP 连接默认不关闭,可以被多个请求复用范围请求,在请求头引入了
range
头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接,支持断点续传缓存处理,引入了更多的缓存控制策略:
Cache-Control
、Etag/If-None-Match
等
2015年5月HTTP/2标准正式发表,就是 RFC 7540。H2 标准带来了如下的特征:
二进制分帧,frame,HTTP/1.1的头信息是文本(ASCII编码),数据体可以是文本,也可以是二进制;HTTP/2 头信息和数据体都是二进制,统称为“帧”:头信息帧和数据帧。帧是 HTTP/2 数据通信的最小单位。
数据流,stream,HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求或响应。HTTP/2 将每个请求或响应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流 ID,用来区分它属于哪个数据流。
多路复用,双工通信,通过单一的 HTTP/2 连接发起多重的请求-响应消息,即在一个连接里,客户端可以同时发送和接收多个请求和响应
HTTP/2 不再依赖多 TCP 连接实现多流并行
同域名下所有通信都在单个连接上完成,同个域名只需要占用一个 TCP 连接,消除了因多个 TCP 连接而带来的延时和内存消耗
单个连接可以承载任意数量的双向数据流,单个连接上可以并行交错的请求和响应,之间互不干扰
数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。
首部压缩,HTTP/2对消息头采用 HPACK 算法进行压缩传输,能够节省消息头占用的网络的流量。压缩是使用了首部表策略
服务端推送,server push,HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送
当我们的服务支持 H2 后,意味着我们可以高效的在服务间进行基于 HTTP 的数据传递了。Go 中最常用的 RPC 实现 gRPC 底层也是基于 HTTP/2 的。
四: RPC 协议
RPC , Remote Procedure Call,远程过程调用。与 HTTP 一致,也是应用层协议。该协议的目标是实现:调用远程过程(方法、函数)就如调用本地方法一致
ServerA 通过 RPC 可以直接调用 ServerB 中的func FuncOnB()
RPC 是 C/S 模式,调用方为 Client,远程方为 Server
RPC 把整体的调用过程,数据打包、网络请求等,封装完毕,在 C、S 两端的 Stub 中。Stub(代码存根)
Stub : 方法存根 , 相当于把 ServerB 里面可以被远程调用的方法做了一个列表放到了 ServerA 上
整体调用过程
ServiceA 将调回需求告知 Client Sub
Client Sub 将调用目标(Call ID)、参数数据(params)等调用信息进行打包(序列化),并将打包好的调用信息通过网络传输给 Server Sub
Server Sub 将根据调用信息,调用相应过程。期间涉及到数据的拆包(反序列化)等操作。
远程过程 FuncOnB 运行,并得到结果,将结果告知 Server Sub
Server Sub 将结果打包,并传输回给 Client Sub
Client Sub 将结果拆包,把最终函数调用的结果告知 ServiceA
另外 , RPC 协议没有对网络层进行规范 , 所以具体的 RPC 实现可以基于 TCP , 也可以基于 HTTP , UDP
RPC 也没有对数据传输格式做规范 , JSON , Text , Protobuf 等都可以
五: gRPC 通信
1. 基本介绍
RPC 是协议 , gRPC 是实现 RPC 的产品
官网 : gRPC
在 gRPC 中,客户端应用程序可以直接调用不同机器上的服务器应用程序的方法,就像它是本地对象一样,使您更容易创建分布式应用程序和服务。与许多 RPC 系统一样,gRPC 基于定义服务的思想,指定可以远程调用的方法及其参数和返回类型。在服务端,服务端实现这个接口并运行一个 gRPC 服务器来处理客户端调用。在客户端,客户端有一个存根(在某些语言中仅称为客户端),它提供与服务器相同的方法。
gRPC 基于 HTTP/2 通信,采用 Protocol Buffers 作数据序列化
2. gRPC 环境
使用 gRPC 环境
Go
Protocol Buffer 编译器,
protoc
,推荐版本3Go Plugin,用于 Protocol Buffer 编译器
安装 protoc
win : 安装解压 , 然后把 bin 目录添加到环境变量
linux : 解压到 /usr/local/bin/ ,
https://github.com/protocolbuffers/protobuf/releases
# 测试是否安装成功
protoc --version
Go Plugin:
// 安装两个包,分别用来生成go代码和go的gRPC代码
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
// 会安装到GoPath目录中
# 测试安装是否成功
protoc-gen-go --version
> protoc-gen-go v1.31.0
protoc-gen-go-grpc --version
> protoc-gen-go-grpc 1.3.0
3. Protocol Bufffer 的基本使用
默认情况下,gRPC 使用 Protocol Buffers,这是 Google 用于序列化结构化数据的成熟开源机制(尽管它可以与 JSON 等其他数据格式一起使用 )
官方文档 : Protocol Buffers Documentation (protobuf.dev)
步骤 :
使用 protocol buffers 语法定义消息,消息是用于传递的数据
使用 protocol buffers 语法定义服务,服务是 RPC 方法的集合,来使用消息
使用 Protocol Buffer编 译工具
protoc
来编译,生成对应语言的代码,例如 Go 的代码
定义消息和服务 , 文件格式为 proto
// 用于定义版本
syntax = "proto3";
// 定义生成的go代码所在的包
option go_package = "./com";
// 定义用于在服务间传递消息
// 响应的产品消息结构
message ProductResponse{
// 消息的字段
int32 id = 1;
string name = 2;
bool is_sole = 3;
}
// 请求产品信息时参数消息
message ProductRequest {
int64 id =1;
}
// 定义Product服务
// 说明服务应该具备哪些操作,类似接口
service Product {
// 远程过程,服务器端的过程
// 接收的参数wield ProductRequest类型的数据,返回值是ProductResponse类型的数据
rpc ProductInfo (ProductRequest) returns (ProductResponse) {}
// 其他的服务操作
}
命令行生成 go 代码
go_out 表示生成的 go 代码要放在哪个目录下面
上面 proto 文件中的 go_package 表示生成的代码放在哪个包下面 , 会自动生成 。最终位置就是 protobuf/com
protoc --go_out=./protobuf --go-grpc_out=./protobuf .\protobuf\product.proto
*.pb.go
包含消息类型的定义和操作的相关代码*_grpc.pb.go
包含客户端和服务端的相关代码
编译形成两个一般是不会去改的 , 想要修改的话 , 可以定义一个新的结构体 , 然后重写方法
六: gRPC 服务端和客户端编码
有两个服务 , 订单服务和产品服务
订单服务提供一个 HTTP 接口 , 用户可以通过这个 HTTP 接口查询订单
订单服务要访问内部的产品服务 , 获取对应的产品信息 , 订单服务和产品服务之间通过 gRPC 的方式进行通信
订单服务对外提供 HTTP 服务 , 对内作为 gRPC 的客户端去访问产品服务
产品服务作为 gRPC 的服务端为订单服务提供数据 ( 订单 )
1. 服务端
package main
import (
"Go_WorkSpace/gRPC/protobuf/com"
"context"
"flag"
"fmt"
"google.golang.org/grpc"
"log"
"net"
)
var (
port = flag.Int("port", 9999, "gRPC Server Port")
)
func main() {
flag.Parse()
// 之前通过proto生成的代码是用来进行数据传递的
// 业务逻辑是需要自己写的
// 设置TCP的监听器
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatal(err)
}
// 构建一个 (实例化) gRPC服务器,
gRPCServer := grpc.NewServer()
// 将product的服务注册到gRPC服务中
// 通过Register这个方法把UnimplementedProductServer注册到gRPC服务器中
com.RegisterProductServer(gRPCServer, &ProductServer{})
//启动监听
log.Println("gRPC Server 监听端口:", listener.Addr())
// gRPC 要在lister定义的端口上提供服务
if err := gRPCServer.Serve(listener); err != nil {
log.Fatalln(err)
}
}
// ProductServer 因为那边的方法没有实现
// 下面代码表示嵌入结构体ProductServer,重写结构体
type ProductServer struct {
com.UnimplementedProductServer
}
// ProductInfo 重写那边未定义的方法
func (ProductServer) ProductInfo(ctx context.Context, pr *com.ProductRequest) (*com.ProductResponse, error) {
// 假设查询到了以下数据
response := com.ProductResponse{
Id: 1,
Name: "Sakura",
IsSole: true,
}
return &response, nil
}
2. 客户端
package main
import (
"Go_WorkSpace/gRPCServer/protobuf/com"
"context"
"encoding/json"
"flag"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"net/http"
"time"
)
var (
gRPCServer = flag.String("gRPCServer", "localhost:9999", "The gRPC Server addr")
// http 命令行参数
addr = flag.String("addr", "127.0.0.1", "The Address for listen. Default is 127.0.0.1")
port = flag.Int("port", 8080, "The Port for listen. Default is 8080.")
)
func main() {
flag.Parse()
//// 连接 grpc 服务器
//conn, err := grpc.Dial(*gRPCServer, grpc.WithTransportCredentials(insecure.NewCredentials()))
//if err != nil {
// log.Fatalf("did not connect: %v", err)
//}
//defer conn.Close()
//// 实例化 grpc 客户端
//c := com.NewProductClient(conn)
// 定义业务逻辑服务,假设为产品服务
service := http.NewServeMux()
service.HandleFunc("/orders", func(writer http.ResponseWriter, request *http.Request) {
// 在这里进行远程调用
// 1.连接到gRPC服务器,因为gRPC
conn, err := grpc.Dial(*gRPCServer, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalln(err)
}
defer conn.Close()
// 2.实例化gRPC客户端
client := com.NewProductClient(conn)
// 3.远程调用 RPC
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
info, err := client.ProductInfo(ctx, &com.ProductRequest{
Id: 13,
})
if err != nil {
log.Fatalln(err)
}
fmt.Println(info)
// 如果没有err,ino就已经是我们需要的数据了
resp := struct {
ID int `json:"id"`
Quantity int `json:"quantity"`
Products []*com.ProductResponse `json:"products"`
}{
9527, 1,
[]*com.ProductResponse{
info,
},
}
respJson, err := json.Marshal(resp)
if err != nil {
log.Fatalln(err)
}
writer.Header().Set("Content-Type", "application/json")
_, err = fmt.Fprintf(writer, "%s", string(respJson))
if err != nil {
log.Fatalln(err)
}
})
// 启动Server监听
address := fmt.Sprintf("%s:%d", *addr, *port)
fmt.Printf("Order service is listening on %s.\n", address)
log.Fatalln(http.ListenAndServe(address, service))
}
package main
import (
"Go_WorkSpace/gRPC/protobuf/com"
"context"
"encoding/json"
"flag"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"net/http"
"time"
)
var (
gRPCServer = flag.String("gRPCServer", "localhost:9999", "The gRPC Server addr")
// http 命令行参数
addr = flag.String("addr", "127.0.0.1", "The Address for listen. Default is 127.0.0.1")
port = flag.Int("port", 8080, "The Port for listen. Default is 8080.")
)
func main() {
flag.Parse()
// 连接 grpc 服务器
conn, err := grpc.Dial(*gRPCServer, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 实例化 grpc 客户端
c := com.NewProductClient(conn)
// 定义业务逻辑服务,假设为产品服务
service := http.NewServeMux()
service.HandleFunc("/orders", func(writer http.ResponseWriter, request *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.ProductInfo(ctx, &com.ProductRequest{
Id: 42,
})
if err != nil {
log.Fatalln(err)
}
resp := struct {
ID int `json:"id"`
Quantity int `json:"quantity"`
Products []*com.ProductResponse `json:"products"`
}{
9527, 1,
[]*com.ProductResponse{
r,
},
}
respJson, err := json.Marshal(resp)
if err != nil {
log.Fatalln(err)
}
writer.Header().Set("Content-Type", "application/json")
_, err = fmt.Fprintf(writer, "%s", string(respJson))
if err != nil {
log.Fatalln(err)
}
//// 在这里进行远程调用
//// 1.连接到gRPC服务器,因为gRPC
//conn, err := grpc.Dial(*gRPCServer, grpc.WithTransportCredentials(insecure.NewCredentials()))
//if err != nil {
// log.Fatalln(err)
//}
//defer conn.Close()
//
//// 2.实例化gRPC客户端
//client := com.NewProductClient(conn)
//// 3.远程调用 RPC
//ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
//defer cancel()
//info, err := client.ProductInfo(ctx, &com.ProductRequest{
// Id: 13,
//})
//if err != nil {
// log.Fatalln(err)
//}
//fmt.Println(info)
//// 如果没有err,ino就已经是我们需要的数据了
//resp := struct {
// ID int `json:"id"`
// Quantity int `json:"quantity"`
// Products []*com.ProductResponse `json:"products"`
//}{
// 9527, 1,
// []*com.ProductResponse{
// info,
// },
//}
//respJson, err := json.Marshal(resp)
//if err != nil {
// log.Fatalln(err)
//}
//writer.Header().Set("Content-Type", "application/json")
//_, err = fmt.Fprintf(writer, "%s", string(respJson))
//if err != nil {
// log.Fatalln(err)
//}
})
// 启动Server监听
address := fmt.Sprintf("%s:%d", *addr, *port)
fmt.Printf("Order service is listening on %s.\n", address)
log.Fatalln(http.ListenAndServe(address, service))
}
七: gRPC 的核心概念
1. 四种服务定义
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
一元 RPC,其中客户端向服务器发送单个请求并获得单个响应,就像正常的函数调用一样。
rpc SayHello(HelloRequest) returns (HelloResponse);
服务器流式 RPC,其中客户端向服务器发送请求并获取流以读回一系列消息。客户端从返回的流中读取,直到没有更多消息为止。可以理解响应是连续的详请求端传输的过程。 gRPC 保证单个 RPC 调用中的消息顺序。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
客户端流式 RPC,其中客户端写入一系列消息并将它们发送到服务器,再次使用提供的流。可以理解为请求数据不断向服务端进行发送,一旦客户端完成了消息的写入,它就会等待服务器读取它们并返回它的响应。 gRPC 再次保证了单个 RPC 调用中的消息顺序。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
双向流式 RPC,双方使用读写流发送一系列消息。这两个流独立运行,因此客户端和服务器可以按照他们喜欢的任何顺序读取和写入 , 既有从客户端 -> 服务端的流 , 也有服务端 -> 客户端的流
例如,服务器可以在写入响应之前等待接收所有客户端消息,或者它可以交替读取消息然后写入消息,或其他一些读取和写入的组合。保留每个流中消息的顺序。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
2. 使用 API
服务端 , 实现服务和方法的声明 , 并且运行了一个 gRPC 监听器来处理监听客户端的请求调用,gRPC 基础架构解码传入请求、执行服务方法并编码服务响应。
客户端,客户端有一个存根的本地对象,客户端在存根里面找到服务器有哪些方法,然后调用存根里面的方法,完成一次远程调用请求
3. 同步和异步
同步:当发出一个请求之后,要阻塞等待响应到来之后在做下一步操作
异步:发出请求后,做其他事情,如果接收到响应,再做响应之后的事情
七: gRPC 的生命周期
gRPC 的生命周期指的是 gRPC 客户端调用 gRPC 服务端的过程 , 不同的服务类型 , 生命周期略有不同
一元 RPC
一旦客户端调用了一个存根方法 , 服务器就会被通知 RPC 已经被调用 , 其中包含调用的客户端元数据 , 方法名称和截止日期 ( 有效期 )
然后 , 服务器可以立即返回自己的初始元数据 ( 在做正式的业务逻辑之前可以返回初始元数据 ) , 或者等待客户端的请求消息
一旦服务器收到客户端的请求消息 , 就会执行工作来创建和填充响应。然后将响应连同状态详细信息(状态代码和可选状态消息)和可选尾随元数据一起返回(如果成功)给客户端。
服务器流式 RPC
服务器流式 RPC 和一元 RPC 的区别在于 , 服务器返回消息流以响应客户端的请求 , ( 客户单没办法一次性接收 , 需要连续着接收 )
客户端口流式 RPC
客户端流式 RPC 类似于一元 RPC,不同之处在于客户端向服务器发送消息流而不是单个消息 , 而是一段一段发送
双向流式 RPC
客户端和服务器端流处理是特定于应用程序的。由于这两个流是独立的,客户端和服务器可以以任意顺序读写消息。例如,服务器可以等到它收到客户端的所有消息后再写入它的消息,或者服务器和客户端可以玩 “ping-pong”——服务器收到请求,然后发回响应,然后客户端发送基于响应的另一个请求,依此类推。
截止日期/超时
gRPC 允许客户端指定在 RPC 因 DEADLINE_EXCEEDED 错误而终止之前,他们愿意等待 RPC 完成多长时间。在服务器端,服务器可以查询特定的 RPC 是否已超时,或者还剩多少时间来完成 RPC。
指定期限或超时是特定于语言的:一些语言 API 根据超时(持续时间)工作,而一些语言 API 根据期限(固定时间点)工作,可能有也可能没有默认期限。
评论区