目录介绍
接上一节go-zero微服务实战——基本环境搭建。搭建好了微服务的基本环境,开始构建整个微服务体系了,将其他服务也搭建起来。
order的目录结构,如下

- 根目录 
  - api服务
- rpc服务
- 自定义逻辑层logic
- 自定义参数层models
- 自定义工具层util
 
api服务和rpc服务都是基于goctl一键生成的,当然这是小编的目录,各位到也可以自定义目录结构,或者参考其他优秀的目录结构。go-zero官网也提供了官方的目录结构go-zero项目结构
- api服务 
  - config
- handler
- logic
- svc
- types
 

- rpc服务 
  - etc
- intenel
- rpcservice
- rpcserviceclient
 

首先解释一个各个目录是干什么的,两个服务api和rpc是go-zero生成的,其内部目录都是对接服务本身的。logic和models,util是公共的部分。
**公共logic和服务内部的logic是不一样的,公共部分是公用的,例如返回订单列表,完成查询返回结果等,而服务的logic则是进一步对公共logic的私有化封装,主要表现是返回的数据不通用,对于api服务来说,logic最后返回结构体或结构体数组等数据即可,因为zero的api封装httpx对序列化,这些是框架完成的。但是对于rpc服务来说,好需要将这些数据转化为字符串的格式才可以传输,所以服务内部的logic就在于将公共logic数据转化为便于传输的格式。**其他目录就不再介绍了go-zero.dev官网上都有。
服务构建
前一节构建了order服务,本节将构建user和product服务器,项目和order基本一样。唯一的区别是user中存在一个登录即用户名认证过程,该过程需要从rpc客户端传递数据到rpc服务端。
user数据库表
 
公共logic代码
// 验证账户
func Ideatify(account string, pass string) error {
	var user models.User
	b, err := db.Engine.Where("username = ?", account).Get(&user)
	if err != nil {
		fmt.Printf("logic list err%v", err)
		return err
	} else if b && (err != nil) {
		return errors.New("用户不存在")
	} else if user.Password == pass {
		return nil
	} else {
		return errors.New("密码错误")
	}
}
api的handler函数
api服务部分,路由此处省略,挂载到
/login下即可。
func UserIdentify() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req models.User
		err := httpx.ParseJsonBody(r, &req)
		if err != nil {
			httpx.WriteJson(w, 500, fmt.Sprintf("err%v", err))
			return
		}
		err = orderlogic.Ideatify(req.Username, req.Password)
		if err != nil {
			//
			httpx.WriteJson(w, 500, map[string]string{"code": "200", "message": err.Error()})
			return
		}
		httpx.OkJson(w, map[string]string{"code": "200", "message": "登录成功"})
	}
}
rpc的logic重写封装部分
// 继承rpc服务器方法
func Identify(in *rpcservice.Request) (*rpcservice.Response, error) {
	reqstr := in.GetReqJson()
	var req models.User
	_ = json.Unmarshal([]byte(reqstr), &req)
	err := orderlogic.Ideatify(req.Username, req.Password)
	if err != nil {
		fmt.Printf("rpc err:%v", err)
		return &rpcservice.Response{ResJson: err.Error()}, err
	}
	//o 赚json字符串
	return &rpcservice.Response{ResJson: "true"}, nil
}
rpc服务方法重写(方法注册)
//继承
func (s *RpcserviceServer) List(ctx context.Context,in *rpcservice.Request) (*rpcservice.Response, error) {
	return logic.Identify(in)
}
客户端调用
注意修改端口,user端口改为9001。
import (
	"context"
	"fmt"
	"rpcclient/rpcservice"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)
func main() {
	//配置连连接参数(无加密)
	dial, _ := grpc.Dial("localhost:9001", grpc.WithTransportCredentials(insecure.NewCredentials()))
	defer dial.Close()
	//创建客户端连接
	client := rpcservice.NewRpcserviceClient(dial)
	//通过客户端调用方法
	res, err := client.Ping(context.Background(), &rpcservice.Request{ReqJson: "xiaoxu"})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(res)
	//order list
	str := `{
		"id":0,
		"username":"xiaoxu",
		"password":"1234567",
		"status":0
	}`
	r, err := client.List(context.Background(), &rpcservice.Request{ReqJson: str})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(r.ResJson)
}
别忘了
_grpc.pb和pb两个文件。
错误返回
 
 正确返回
 
传入数据通过&rpcservice.Request{ReqJson: "xiaoxu"}的Request结构体完成的。在pb文件下,这个客户端和服务端共有的。

以此方法逐个完成其他方法的封装和注册,另外完成product服务的构建。三个服务端口不同注意修改,api为8000系,rpc为9000系列。
product的api服务
 
 rpc客户端代码完全一样改一下端接口9002即可

rpc远程调用
到上一小结三个服务就构建完成了。服务之间是应该可以互相调用的,就像客户端调用服务端一样。在其他服务调用其本身就是客户端,被调用的服务就相当于服务端。
在api和rpc服务的目录下都有一个主程序,都启动即可。如下图所示,注意三个服务一种药开6个终端分别启动。

在三个独立的api服务和rpc服务中,各自都只能操作相应的数据库,但是涉及多表查询是就需要rpc远程调用了。
在goctl目录下,存在XXXclent目录该目录提供了构造client实例的代码。
 
// Code generated by goctl. DO NOT EDIT.
// Source: rpcservice.proto
package rpcserviceclient
import (
	"context"
	"demo/rpcservice/rpcservice"
	"github.com/zeromicro/go-zero/zrpc"
	"google.golang.org/grpc"
)
type (
	Request  = rpcservice.Request
	Response = rpcservice.Response
	Rpcservice interface {
		Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
	}
	defaultRpcservice struct {
		cli zrpc.Client
	}
)
func NewRpcservice(cli zrpc.Client) Rpcservice {
	return &defaultRpcservice{
		cli: cli,
	}
}
func (m *defaultRpcservice) Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {
	client := rpcservice.NewRpcserviceClient(m.cli.Conn())
	return client.Ping(ctx, in, opts...)
}
对比上一章节自定义的客户端,如下:
package main
import (
	"context"
	"fmt"
	"rpcclient/rpcservice"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)
func main() {
	//配置连连接参数(无加密)
	dial, _ := grpc.Dial("localhost:9002", grpc.WithTransportCredentials(insecure.NewCredentials()))
	defer dial.Close()
	//创建客户端连接
	client := rpcservice.NewRpcserviceClient(dial)
	//通过客户端调用方法
	res, err := client.Ping(context.Background(), &rpcservice.Request{ReqJson: "xiaoxu"})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(res)
	// //order list
	// str := `{
	// 	"id":0,
	// 	"username":"xiaoxu",
	// 	"password":"123456",
	// 	"status":0
	// }`
	r, err := client.List(context.Background(), &rpcservice.Request{})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(r.ResJson)
}
rpcclient/rpcservice包是存放_grpc.pb和pb文件的目录
创建rpc服务端的方法是来源于_grpc.pb的NewRpcserviceClient
func NewRpcserviceClient(cc grpc.ClientConnInterface) RpcserviceClient {
	return &rpcserviceClient{cc}
}
对比可以看出,都是使用该方法构建的客户端实例,唯一的不同在于,自定义的客户端时通过grpc.Dial返回客户端对象,但是官方提供的代码通过返回zrpc.Client(内置连接对象)。但是官方提供的并未配置端口的直接入口。
从参数入手,由于都是调用的NewRpcserviceClient方法,那么参数都是*grpc.ClientConn类型。
func (m *defaultRpcservice) Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {
	client := rpcservice.NewRpcserviceClient(m.cli.Conn())
	return client.Ping(ctx, in, opts...)
}
回到源码,看到Conn()方法指向如下图所示的结构体。

 导航到该结构体的定义处,其是*grpc.ClientConn的一个实现类。

 该实现类继承了Conn方法同时也扩展了另一个眼熟的方法dial如下,那么到这就知道该如何使用了吧。

直接调用dial方法配置端口,配置*grpc.ClientConn对象。注意这个方法和自定义的不一样,
 自定义是直接调用grpc.Dial来自于grpc库,直接返回连接对象实例。而前者只是连接对象的一个配置端口和参数的方法。
// NewClient returns a Client.
func NewClient(target string, middlewares ClientMiddlewaresConf, opts ...ClientOption) (Client, error) {
	cli := client{
		middlewares: middlewares,
	}
	svcCfg := fmt.Sprintf(`{"loadBalancingPolicy":"%s"}`, p2c.Name)
	balancerOpt := WithDialOption(grpc.WithDefaultServiceConfig(svcCfg))
	opts = append([]ClientOption{balancerOpt}, opts...)
	if err := cli.dial(target, opts...); err != nil {
		return nil, err
	}
	return &cli, nil
}
上述源码来自zrpc提供了创建api构建zrpc.Client实例,作为官方提供的NewRpcservice方法的参数,于是请求的地址和端口就能配置了。如下:
得到的
r就是_grpc.pb的RpcserviceClient对象,就可以实现方法调用了。
func GetRpcClientData() (string, error) {
	c, err := zrpc.NewClient(zrpc.RpcClientConf{
		Etcd: discov.EtcdConf{
			Hosts: []string{"127.0.0.1:9000"},
		},
	})
	if err != nil {
		return "", errors.New("rpc connect failed")
	}
	r := rpcserviceclient.NewRpcservice(c)
	r2, err := r.Ping(context.Background(), &rpcservice.Request{ReqJson: "xiaoxu"})
	if err != nil {
		return "", errors.New("rpc method anlyse failed")
	}
	return r2.ResJson, nil
}
上述方法使用了微服务的服务注册,下一章节讲,因此需要将服务再注册到服务中心中。到此函数已经注册两次了,第一次是继承服务器函数(服务器注册函数),第二次是客户端使用服务注册时将函数注册到服务中心。
上述代码构建使用
discov.EtcdConf就是服务发现etcd的配置,上述代码是无法直接调用的,应为本地没有服务中心。
无服务中心服务注册的调用
func GetRpcClientPing() (string, error) {
	c, err := zrpc.NewClient(zrpc.RpcClientConf{
		Target: "127.0.0.1:9000",
	})
	if err != nil {
		return "", err
	}
	r := rpcserviceclient.NewRpcservice(c)
	r2, err := r.Ping(context.Background(), &rpcservice.Request{ReqJson: "xiaoxu"})
	if err != nil {
		return "", errors.New("rpc method anlyse failed")
	}
	return r2.ResJson, nil
}
使用
Tartget属性就跳过服务中心。
import (
	"fmt"
	"testing"
)
func TestGetData(t *testing.T) {
	str, err := GetRpcClientPing()
	if err != nil {
		panic(err)
	}
	t.Log(str)
	fmt.Println(str)
}

rpcclient中注册自定义函数:
func TestGetList(t *testing.T) {
	str, err := GetRpcClientList()
	if err != nil {
		panic(err)
	}
	t.Log(str)
	fmt.Println(str)
}


测试通过,返回数据。该数据是字符串,还需要经过反序列化得到结构体数据数据。
部分参考自:https://juejin.cn/post/7041907188972912676。
gRPC Client 的开发
服务发现
在rpc远程调用时,连接的套接字是直接写在代码中的,如下图所示:
func GetRpcClientPing() (string, error) {
	c, err := zrpc.NewClient(zrpc.RpcClientConf{
		Target: "127.0.0.1:9000",
	})
	if err != nil {
		return "", err
	}
	r := rpcserviceclient.NewRpcservice(c)
	r2, err := r.Ping(context.Background(), &rpcservice.Request{ReqJson: "xiaoxu"})
	if err != nil {
		return "", errors.New("rpc method anlyse failed")
	}
	return r2.ResJson, nil
}
这样的弊端在于当分布部署或者服务器更换时需要修改源代码的套接字,这样时非常不方便的。服务发现的是微服务治理的一种手段,功能在于使用服务注册后只需记录服务的名称,有注册中心自动查找该名称的服务,这样就脱离ip的强绑定了。
zero默认的服务中心是Etcd。服务etcd是一个注册与发现服务器,当然功能不止如此,首先在电脑上下载服务器。
-  apt install etcd下载etcd
  
-  etcd启动服务

默认启动端口为2379。

启动会报错,那么如何将服务以名称的形式注册到etcd中呢?
官方教程
搭建etcd服务器
etcd官网
用 go-grpc 使用etcd发现
etcd服务注册与发现的原理和实现
- 服务注册
go-zero集成了etcd,在core/discov包下提供了注册与发现的方法。
 
章节到此结束,具体使用方法请看下一章节
go-zero微服务实战——etcd服务注册与发现



















