垂直扩展和水平扩展:
在垂直扩展模型中,想要增加系统负荷就意味着要在系统现有的部件上下工夫,即通过提高系统部件的能力来实现。不增加系统的成员数,但是我们通过增加系统成员的生产效率来获得期望的负荷量。
在水平扩展模型中,我们不是通过增加单个系统成员的负荷而是简单的通过增加更多的系统成员来实现。系统每个成员的生产力依然没变,通过增加更多的成员来提高系统的能力。
RPC
远程过程调用RPC是一种轻量级的计算机通信协议,它允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外的为这个交互作用编程,如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用
RPC就是像调用本地函数一样去调用远程函数。通过rpc协议传递远程函数的函数名、参数等。达到在本地调用远端函数,得返回值到本地的目标。
golang有RPC的官方库net/rpc,支持encoding/gob进行编解码,支持tcp和http数据传输方式,由于其他语言不支持gob,因此golang的rpc只适用于golang开发的服务器客户端交互(如用nc工具充当服务端会导致接受数据乱码),想避免乱码可以使用jsonrpc包,其方法和rpc一致
golang的rpc必须符合4个条件:
- 结构体字段首字母要大写(因为需要跨域访问,这个结构体是函数接收参数的结构体)
- 函数名首字母大写
- 函数第一个参数是接收参数,第二个参数是返回给客户端的参数,且第二个参数必须是指针类型
- 函数必须有一个返回值error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//服务端,求矩形周长
//声明矩形对象
type Rect struct {
}
//声明从客户接受的参数结构体
type Params struct {
Width,Height int
}
//声明返回给客户端的参数结构体
type Ret struct {
Area int
Perimeter int
}
//定义求矩形面积和周长的方法,第二个参数必须是指针,因为是其他服务调用此服务
//第二个参数也可以定义一个普通类型进行返回
//返回值error如果不为空,那么无论接受参数是否已经有值,服务端都不会返回数据,error和传出参数只会有一个是非空
func (r *Rect)Area(p Params,ret *Ret) error{
ret.Area=p.Width*p.Height
ret.Perimeter=(p.Width+p.Height)*2
return nil
}
func main(){
//1.注册服务
rpc.Register(new(Rect))
//2.把服务绑定到http协议上
rpc.HandleHTTP()
//3.监听服务,等待客户端调用方法
err:=http.ListenAndServe(":8989",nil)
if err != nil {
return
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
//客户端,请求服务端,获取结果
//声明参数结构体
type Params struct {
Width,Height int
}
type Ret struct {
Area int
Perimeter int
}
func main() {
//1. 连接远程的RPC服务
rp, err := rpc.DialHTTP("tcp", "127.0.0.1:8989")
if err != nil {
log.Fatal(err)
}
//2. 调用远程方法
var ret Ret
//第一个参数为方法名,为服务器对应的结构体名.方法名,第二个参数为传过去的参数,第三个参数为接受到的参数,与服务器对应的参数类型要一致
err = rp.Call("Rect.Area", Params{10, 20}, &ret)
if err != nil {
log.Fatal(err)
}
fmt.Printf("面积:%v\n周长:%v\n",ret.Area,ret.Perimeter)
}
|
假如结构体定义的方法参数和返回值不符合rpc的规范,我们是无法在编译期发现的,只有在运行期远程调用时才能发现这个方法的问题。如果希望在编译期就发现其对象是否合法,那就需要在此结构体之上定义一个rpc规范的接口,让这个结构体实现这个接口,这样子如果没有全部实现就无法通过编译
![image.png](/p/%E5%88%9D%E6%AD%A5%E4%BA%86%E8%A7%A3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6/media/image.png)
官方 还提供了net/rpc/jsonrpc库实现rpc方法,jsonrpc采用json进行数据编解码,因此支持跨语言调用,目前jsonrpc库是基于tcp协议实现的,暂不支持http传输方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//服务端
//1.注册服务
rpc.Register(new(Rect))
//2. 监听端口
listen, err := net.Listen("tcp", "127.0.0.1:8989")
if err != nil {
log.Fatal(err)
}
//3. 循环监听服务
for {
conn, err := listen.Accept()
if err != nil {
log.Fatal(err)
continue
}
go func(conn net.Conn) {
//客户端只需要把rpc.DialHTTP变成jsonrpc.Dial即可
fmt.Println("create new a Client")
jsonrpc.ServeConn(conn)
}(conn)
}
|
gRPC
gRPC简介
- grpc由google开发,是一款语言中立,平台中立、开源的远程过程调用系统
- grpc客户端和服务端可以在多种环境中运行和交互,例如用java写一个服务端,可以用go语言客户端调用
gRPC和protobuf介绍
- 微服务架构中,由于每个服务对应的代码库是独立运行的,无法直接调用,彼此间的通信就是个大问题
- gRPC可以实现微服务,将大的项目拆分为多个小且独立的业务模块,即服务,各服务间使用高效的protobuf协议进行gRPC调用,gRPC默认使用protocol buffers,这是google开源的一套成熟的结果数据序列化机制(也可以使用其他数据格式,如json)
- 可以用proto files创建gRPC服务,用message类型来定义方法参数和返回类型
- 从protobuf的角度来看,gRPC只不过是一个针对service接口生成代码的生成器(即在生成命令中添加
plugins=grpc
,不加这段,生成是无法生成service相关的代码的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
go get -u -v github.com/golang/protobuf/proto
go get google.golang.org/grpc(如果无法使用, 用如下命令代替)
git clone https://github.com/grpc/grpc-go.git$GOPATH/src/google.golang.org/grpc
git clone https://github.com/golang/net.git$GOPATH/src/google.golang.org/grpc
git clone https://github.com/golang/net.git$GOPATH/src/golang.org/x/net
git clone https://github.com/golang/text.git$GOPATH/src/golang.org/x/text
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
git clone https://github.com/google/go-genproto.git GOPATH/src/golang.org/x/text
go get −u github.com/golang/protobuf/proto,protoc−gen−go
git clone https://github.com/google/go−genproto.gitGOPATH/src/google.golang.org/genproto
cd $GOPATH/src/
go install google.golang.org/grpc
go get github.com/golang/protobuf/protoc-gen-go
上面安装好后, 会在 GOPATH/bin 下生成 protoc-gen-go.exe
但还需要一个 protoc.exe, windows 平台编译受限, 很难自己手动编译, 直接去网
站下载一个, 地址: https://github.com/protocolbuffers/protobuf/releases/tag/v3.9.0 ,
同样放到 GOPATH/bin 下
|
通过定义好的.proto文件生成Java, Python, C++, Go, Ruby,JavaNano, Objective-C, or C# 代码,需要安装编译器protoc。当使用protocol buffer编译器运行.proto文件时,编译器将生成所选语言的代码,用于使用在.proto文件中定义的消息类型、服务接口约定等。(Go: 生成一个.pb.go文件,每个消息类型对应一个结构体)
protobuf语法
基本规范
- 文件以**.proto作为文件后缀**,除结构体定义外的语句以分号结尾
- 结构定义可以包含:message(结构体)、service(接口)、enum(枚举类)
- rpc方法定义结尾的分号可有可无
- 字段中的数字并不是赋值,它代表了在编译时字段的位置,不能重复
Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式
1
2
3
|
message SongServerRequest {
required string song_name = 1;
}
|
enum 枚举类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式
1
2
3
4
|
enum Foo {
FIRST_VALUE = 0; // 枚举值必须从0开始
SECOND_VALUE = 1;
}
|
Service与rpc方法名统一采用驼峰式命名
字段规则
- 字段格式:限定修饰符 | 数据类型 | 字段名称 | = | 字段编码值 | [字段默认值]
- 限定修饰符包含required\optional\repeated
- Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃
- Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。—因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡
- Repeated:表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值(go是转换成切片)
数据类型
- Protobuf定义了一套基本数据类型。几乎都可以映射到C++\Java等语言的基础数据类型
![](file:///C:/Users/admin/AppData/Local/Temp/msohtmlclip1/01/clip_image002.jpg)
![image.png](/p/%E5%88%9D%E6%AD%A5%E4%BA%86%E8%A7%A3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6/media/image-2.png)
- 表示打包的字节并不是固定。而是根据数据的大小或者长度
- 关于 fixed32 和int32的区别。fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高
- 字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的
- protobuf建议字段的命名采用以下划线分割的驼峰式。例如first_name 而不是firstName
字段编码值
- 有了该值,通信双方才能互相识别对方的字段,相同的编码值,其限定修饰符和数据类型必须相同,编码值的取值范围为 1~2^32(4294967296)
- 其中1~15的编码时间和空间效率都是最高的,编码值越大,其编码的时间和空间效率就越低,所以建议把经常要传递的值把其字段编码设置为1-15之间的值
- 1900~2000编码值为Google protobuf 系统内部保留值,建议不要在自己的项目中使用
当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端
service定义
- 如果想要将消息类型用在RPC系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器会根据所选择的不同语言生成服务接口代码。例如,想要定义一个RPC服务并具有一个方法(Search),该方法接收SearchRequest并返回一个SearchResponse(两个都是proto中定义的message),此时可以在.proto文件中进行如下定义:
1
2
3
4
|
service SearchService
{
rpc Search (SearchRequest) returns (SearchResponse);
}
|
- 生成的接口代码作为客户端与服务端的约定,服务端必须实现定义的所有接口方法,客户端直接调用同名方法向服务端发起请求,比较麻烦的是,即便业务上不需要参数也必须指定一个请求消息,一般会定义一个空message
message定义
- 一个message类型定义描述了一个请求或响应的消息格式,可以包含多种类型字段。例如定义一个搜索请求的消息格式,每个请求包含查询字符串、页码、每页数目,字段名用小写,转为go文件后自动变为大写,message就相当于结构体
1
2
3
4
5
6
|
syntax ="proto3";
message SearchRequest {
string query = 1; // 查询字符串
int32 page_number = 2; // 页码
int32 result_per_page = 3; // 每页条数
}
|
- SearchRequest 定义了三个字段,每个字段声明以分号结尾,.proto文件支持双斜线 // 添加单行注释
- 首行声明使用的protobuf版本为proto3(必须声明)
- 一个.proto文件中可以定义多个消息类型,一般用于同时定义多个相关的消息,例如在同一个.proto文件中同时定义搜索请求和响应消息
- message支持嵌套使用,作为另一message中的字段类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
//内部声明的message类型名称只可在内部直接使用
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
|
map类型的定义
1
2
3
|
//map<key_type, value_type> map_field = N;
message Project {...}
map<string, Project> projects = 1;
|
- 键、值类型可以是内置的类型,也可以是自定义message类型
- 字段不支持repeated属性
示例:
user.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
syntax = "proto3";
package proto;
// 编译到当前目录下的proto目录下,包名也会叫proto
option go_package="./proto";
message UserRequest{
string name = 1;
}
message UserResponse{
int32 id = 1;
string name = 2;
int32 age = 3;
repeated string hobby = 4;
}
service UserInfoService{
rpc GetUserInfo(UserRequest) returns (UserResponse){}
}
|
生成go文件: protoc -I . --go_out=plugins=grpc:. ./user.proto
server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
pb "main/gRPC/proto"
"net"
)
//定义一个结构体用于实现protobuf生成出来的接口的方法,这样子就可以将这个结构体通过传参的方式进行注册
type UserInfoService struct {}
//实现接口中的函数
func (u *UserInfoService)GetUserInfo(ctx context.Context, req *pb.UserRequest) (resp *pb.UserResponse,err error){
name:=req.Name
if name=="zs"{
resp=&pb.UserResponse{
Id: 1,
Name: name,
Age: 22,
Hobby: []string{"run","game"},
}
}
return
}
var UserInfo = UserInfoService{}
func main(){
addr:="127.0.0.1:8999"
// 1.监听端口
listen, err := net.Listen("tcp", addr)
if err != nil {
fmt.Println("监听失败,",err)
return
}
defer listen.Close()
// 2.实例化gRPC
s := grpc.NewServer()
// 3.在gRPC上注册微服务
pb.RegisterUserInfoServiceServer(s,&UserInfo)
// 4.启动服务端
s.Serve(listen)
}
|
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
pb "main/gRPC/proto"
)
func main(){
// 1.连接,必须设置传输安全WithInsecure(),作用是关闭传输安全。不加则会报错
conn, err := grpc.Dial("127.0.0.1:8999",grpc.WithInsecure())
if err != nil {
fmt.Println("连接异常,",err)
return
}
defer conn.Close()
// 2.实例化客户端
client := pb.NewUserInfoServiceClient(conn)
// 3.组装一个请求参数
req:=new(pb.UserRequest)
req.Name="zs"
// 4.调用接口
resp, err := client.GetUserInfo(context.Background(), req)
if err != nil {
fmt.Println("响应异常,",err)
}
fmt.Printf("响应结果%v\n",resp)
}
|
Go Micro
Go Micro是一个插件化的基础框架,基于此可以构建微服务,Micro的设计哲学是可插拔的插件化架构。
在架构之外,它默认实现了mdns作为服务发现,通过http进行通信,通过protobuf和json进行编解码
go-micro框架的一个缺点是从1.0到2.0,在到3.0版本都不兼容,每个版本都有很大的改动。
go-micro的3.0版本升级为一个云原生开发平台,原来的go-micro项目改名字了。
云原生开发平台官网地址:https://m3o.com/
可能是由于go-micro框架应用的人比较多,后来作者又把项目改回了go-micro,现在依然在维护。
所以现在关于go-micro项目是有些混乱的,我们可以这样认为:
其实这两个项目的发起人都是asim,不过micro版本的现在是商业化了,变成了一个云原生开发平台,我们使用micro生成脚手架,项目开发框架是asim的go-micro。
go-micro依然是一个微服务框架,现在说go-micro v3准确的讲是指https://github.com/asim/go-micro 这个项目,micro v3是指https://github.com/micro/micro
所以用micro生成的脚手架后要将micro/v3相关的改成 go-micro.dev/v4
现在网络上很多博客和文章都是过时的,没人讲清楚两者的区别,给初学者带来很多困扰。
主要功能
- 服务发现:自动服务注册和名称解析。服务发现是微服务开发的核心。当服务A需要与服务B通话时,它需要该服务的位置。默认发现机制是多播DNS(mdns),一种零配置系统。可以选择使用SWIM协议为p2p网络设置八卦,或者为弹性云原生设置设置consul
- 每个server启动时都会将自己的ip、port和服务名 注册给 服务发现,由服务发现来管理多个服务的信息
- client通过借助服务发现来访问service。当client需要某个服务时,会先请求服务发现,服务发现找到一个可用的服务,返回其对应服务的信息,然后client再借助服务发现返回的服务信息直接访问service
- 负载均衡:基于服务发现构建的客户端负载均衡。一旦我们获得了服务的任意数量实例的地址,我们现在需要一种方法来决定要路由到哪个节点。我们使用随机散列负载均衡来提供跨服务的均匀分布,并在出现问题时重试不同的节点
- 消息编码:基于内容类型的动态消息编码。客户端和服务器将使用编解码器和内容类型为您无缝编码和解码Go类型。可以编码任何种类的消息并从不同的客户端发送。客户端和服务器默认处理此问题。这包括默认的protobuf和json
- 请求/响应:基于RPC的请求/响应,支持双向流。我们提供了同步通信的抽象。对服务的请求将自动解决,负载平衡,拨号和流式传输。启用tls时,默认传输为http / 1.1或http2
- Async Messaging(异步处理):PubSub是异步通信和事件驱动架构的一流公民。事件通知是微服务开发的核心模式。启用tls时,默认消息传递是点对点http/ 1.1或http2
- 可插拔接口:Go Micro为每个分布式系统抽象使用Go接口,因此,这些接口是可插拔的,并允许Go Micro与运行时无关,可以插入任何基础技术(插件地址:https://github.com/micro/go-plugins)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
package main
import (
"context"
"fmt"
"github.com/micro/go-micro"
"log"
pb "mico/proto"
)
type Hello struct {}
func (h *Hello)Info(ctx context.Context,req *pb.UserRequest,resp *pb.UserResponse) error{
resp.Msg="hello"+req.Name
return nil
}
func main() {
// 1.得到服务端实例,Name()用于设置微服务的名,用来访问
//cmd命令: micro call hello Hello.Info {"username":"zs"}
service := micro.NewService(micro.Name("hello"))
// 2.初始化
service.Init()
// 3.服务注册
err := pb.RegisterUserInfoServiceHandler(service.Server(), new(Hello))
if err != nil {
fmt.Println(err)
}
// 4.启动服务
err = service.Run()
if err != nil {
log.Fatal(err)
}
}
syntax = "proto3";
package proto;
message UserRequest{
string name = 1;
}
message UserResponse{
string msg = 1;
}
service UserInfoService{
rpc Info(UserRequest) returns (UserResponse){}
}
protoc -I . --micro_out=. --go_out=. ./user.proto
|
服务发现-consul
![image.png](/p/%E5%88%9D%E6%AD%A5%E4%BA%86%E8%A7%A3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6/media/image-5.png)
服务发现也可以看作是一个服务,是给 “具体业务服务”提供服务的(从这个角度来看,服务发现就属于具体业务的server端,具体业务服务是client端)。通常一台正常运行的服务崩溃的几率在2%,如果有三台则崩溃几率是2% * %2 *%2。
![image.png](/p/%E5%88%9D%E6%AD%A5%E4%BA%86%E8%A7%A3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6/media/image-7.png)
consul 常用命令
consul agent -dev
以一个开发者模式去运行一个consul代理,走的都是consul中一系列默认配置
![image.png](/p/%E5%88%9D%E6%AD%A5%E4%BA%86%E8%A7%A3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6/media/image-7.png)
![image.png](/p/%E5%88%9D%E6%AD%A5%E4%BA%86%E8%A7%A3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6/media/image-5.png)
![image.png](/p/%E5%88%9D%E6%AD%A5%E4%BA%86%E8%A7%A3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6/media/image-2.png)
![image.png](/p/%E5%88%9D%E6%AD%A5%E4%BA%86%E8%A7%A3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6/media/image-1.png)
服务端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
// "github.com/hashicorp/consul/api"
//把grpc注册到consul上
// 1.初始化consul配置
consulConfig := api.DefaultConfig()
// 2.创建consul对象,此时server对于consul是客户端,因此是NewClient
consulClient, err := api.NewClient(consulConfig)
if err != nil {
fmt.Println("err:", err)
return
}
// 3. 通知consul即将注册的服务的配置信息
reg := api.AgentServiceRegistration{
ID: "1",
Tags: []string{"server tag"},
Name: "server",
// 此处的ip port和check的ip port要与grpc监听的ip port一致
// 这里的ip端口是监听的目标端口,并不是consul自己的端口
Port: 8999,
Address: "127.0.0.1",
Check: &api.AgentServiceCheck{
TCP: "127.0.0.1:8999",
Timeout: "1s",
Interval: "5s",
},
}
// 4. 注册grpc服务到consul上
err = consulClient.Agent().ServiceRegister(®)
if err != nil {
fmt.Println("err:", err)
return
}
fmt.Println("注册成功")
//############以下为grpc远程调用####################
addr := "127.0.0.1:8999"
// 1.监听端口
listen, err := net.Listen("tcp", addr)
if err != nil {
fmt.Println("监听失败,", err)
return
}
defer listen.Close()
// 2.实例化gRPC
s := grpc.NewServer()
// 3.在gRPC上注册微服务
teacher.RegisterSayServer(s, &SayStruct{})
// 4.启动服务端
s.Serve(listen)
|
consulClient.Agent().ServiceDeregister("serviceID
") 注销对应id的服务
客户端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
// "github.com/hashicorp/consul/api"
// 1.初始化consul配置
consulConfig := api.DefaultConfig()
// 2.创建consul对象,此时server对于consul是客户端,因此是NewClient
consulClient, err := api.NewClient(consulConfig)
if err != nil {
fmt.Println("err:", err)
return
}
// 参数: service:对应服务名; tag:别名,如有多个就任选一个;passingOnly; 是否通过健康检查,通常要找通过的,即true; QueryOptions:查询参数,通常传nil
// 返回值:ServiceEntry:存储服务的切片,因为一个服务可以分成主从的同名多个服务, QueryMeta:查询参数的返回值,传nil返nil
service, _, err := consulClient.Health().Service("server", "server tag", true, nil)
if err != nil || len(service) == 0 {
return
}
addr := fmt.Sprintf("%v:%v", service[0].Service.Address, service[0].Service.Port)
//############以下为grpc远程调用####################
// 1.连接,必须设置传输安全WithInsecure(),否则会报错
conn, err := grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
fmt.Println("连接异常,", err)
return
}
defer conn.Close()
// 2.实例化客户端
client := teacher.NewSayClient(conn)
// 3.组装一个请求参数
req := new(teacher.Teacher)
// 4.调用接口
resp, err := client.SayHellp(context.Background(), req)
if err != nil {
fmt.Println("响应异常,", err)
}
|
new [创建micro脚手架]
1
2
3
4
|
#指定服务的命名空间
--namespace "go.micro" Namespace for the service e.g com.example
#服务类型,可以是微服务srv,或者web项目,或者api等
--type "srv" Type of service e.g api, fnc, srv, web
|
![image.png](/p/%E5%88%9D%E6%AD%A5%E4%BA%86%E8%A7%A3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6/media/image-4.png)
![image.png](/p/%E5%88%9D%E6%AD%A5%E4%BA%86%E8%A7%A3%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%A1%86%E6%9E%B6/media/image-6.png)
服务端Test:
micro默认的服务发现插件是mdns,这个插件是支持组播的,它必须和服务在同一局域网内,这导致如果服务分布在不同局域网就无法使用。因此要将插件改为consul micro.Registry(consulReg)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
// 初始化服务发现--consul: "github.com/asim/go-micro/plugins/registry/consul/v4"
// registry: "go-micro.dev/v4/registry"
consulReg := consul.NewRegistry(
// 这个可以指定对应的consul地址,不加的话默认是连接本机的8500端口的consul
func(o *registry.Options) {
o.Addrs = []string{
// 配置consul服务地址
"127.0.0.1:8500",
}
})
// Create service 初始化服务器对象
srv := micro.NewService(
micro.Address("127.0.0.1:8999"), // 指定ip和端口,如果不指定的话,micro会随机分配一个端口
micro.Name("test-micro"), // 服务器名
micro.Version("latest"), // 版本,latest为最终版本,最新
micro.Registry(consulReg), // 注册到consul上
// 设置命令行参数 --run_client,BoolFlag默认值是false。与init配套。如果不需要命令行可以不加
micro.Flags(&cli.BoolFlag{
Name: "run_client",
Usage: "Launch the client",
}),
)
// init作用于对newService中的flags进行加工处理,如果没有flag可以不调用
// 建议不调用,因为init如果加了和newService中相同的参数,如micro.Name("newName"),它会覆盖掉之前的。导致name变成newName
// 后续代码运行期想重新初始化才有必要调用此函数
// srv.Init(micro.Action(func(c *cli.Context) error {
// if c.Bool("run_client") {
// fmt.Println("run_client cmd")
// os.Exit(0)
// }
// return nil
// }))
// Register handler 注册服务
pb.RegisterTestMicroHandler(srv.Server(), new(handler.TestMicro))
// Run service 运行服务
if err := srv.Run(); err != nil {
return
}
|
通过 micro new 服务包名
在当前文件夹下生成一个micro-service的脚手架包,然后通过makefile中的 protoc --proto_path=. --micro_out=. --go_out=:. proto/test-micro.proto
生成对应的proto代码,然后修改对应导入的包名,将micro包改成对应框架的 go-micro.dev/v4
包,最后go mod tidy 服务端就初始化完成可以运行了
客户端Test
客户端使用gin框架来对服务端进行远程调用。因为micro的客户端过于臃肿且实现复杂,采用gin会更简单方便
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
func main() {
route := gin.Default()
route.GET("/", handlerFuncsTest)
route.Run(":8080")
}
func handlerFuncsTest(c *gin.Context) {
// 初始化服务发现consul,可以加参数指定consul的地址,不加默认为本机的8500端口
consulReg := consul.NewRegistry(
func(o *registry.Options) {
o.Addrs = []string{
// 配置consul服务地址
"127.0.0.1:8500",
}
},
)
// 初始化micro对象,指定consul为服务发现
service := micro.NewService(
micro.Registry(consulReg),
)
// 初始化客户端,client.DefaultClient是go-micro.dev/v4/client包的
// 如果使用默认的mdns,则是client.DefaultClient
// microClient := pb.NewTestMicroService("test-micro", client.DefaultClient)
microClient := pb.NewTestMicroService("test-micro", service.Client())
// 可以将服务端的pb复制过来,也可以通过mod直接调用服务端的pb,在服务端中,handler后缀的是给服务端使用的注册,service是给客户端使用的调用
resp, _ := microClient.Call(context.TODO(), &pb.Request{Name: "zs"})
c.JSON(http.StatusOK, gin.H{
"resp": resp,
})
}
|