目录介绍
接上一节零微服务实战——基础环境搭建。搭建好了微服务的基本环境,开始构建整个微服务体系了,将其他服务也搭建起来。
order的目录结构如下
- 根目录
- API服务
- 远程过程调用服务
- 自定义逻辑层逻辑
- 自定义参数层模型
- 自定义工具层util
api服务和rpc服务都是基于goctl一键生成的,当然这是小编的目录,各位到也可以自定义目录结构,或者参考其他优秀的目录结构。go-zero官网也提供了官方的目录结构归零项目结构
- api服务
- 配置
- 处理程序
- 逻辑
- 服务中心
- 类型
- rpc服务
- ETC
- 因特内尔
- RPC服务
- rpc服务客户端
首先,解释一下每个目录的用途。这两个服务api和rpc都是go-zero生成的,它们的内部目录都是连接到服务本身的。逻辑、模型和实用程序是公共部分。
**公共逻辑与服务内部的逻辑不同。公共部分是公共的,比如返回订单列表、完成查询并返回结果等,而业务逻辑则是对公共逻辑的进一步私有化封装。主要表现是返回的数据不通用。对于api服务来说,逻辑最终可以返回结构体或者结构体数组等数据,因为zero的api封装了httpx序列化,是由框架完成的。但对于rpc服务来说,最好将这些数据转换成字符串格式再进行传输,所以服务内部的逻辑就是将公共逻辑数据转换成方便传输的格式。 **其他目录不再介绍,都可以在go-zero.dev官网上找到。
服务构建
订单服务是在上一节中构建的。本节将构建用户和产品服务器。项目与订单基本一致。唯一的区别是,用户中有一个登录或用户名认证过程,需要将数据从rpc客户端传递到rpc服务器。
用户数据库表
公共逻辑代码
// 验证账户
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处理函数
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的逻辑重写了封装部分
// 继承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)
}
客户来电
注意修改端口。用户端口修改为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文件下,这个客户端和服务端共有的。
这样就一一完成了其他方法的封装和注册,完成了产品服务的构建。请注意,这三个服务端口是不同的。 api是8000系列,rpc是9000系列。
product的api服务
rpc客户端代码完全一样改一下端接口9002即可
rpc远程调用
上一节总结的三项服务就完成了。服务应该能够相互调用,就像客户端调用服务器一样。当调用其他服务时,客户端本身就是客户端,被调用的服务就相当于服务端。
api和rpc服务目录下有主程序,启动即可。如下图,注意三个服务,一个药品,六个终端分别启动。
三个独立的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。
服务发现
远程调用RPC时,直接在代码中写入连接的socket,如下图:
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
}
这样做的缺点是分布式部署或者更换服务器时需要修改源码socket,非常不方便。服务发现是微服务治理的一种手段。其作用是,注册一个服务后,只需要记录该服务的名称,注册中心就会自动找到该名称的服务,从而摆脱了IP的强绑定。
zero默认的服务中心是Etcd
。服务etcd是一个注册与发现服务器,当然功能不止如此,首先在电脑上下载服务器。
-
apt install etcd
下载etcd
-
etcd
启动服务
默认启动端口为2379。
启动时会报错,那么如何将服务以name的形式注册到etcd中呢?
- 服务注册
go-zero集成了etcd,在core/discov
包下提供了注册与发现的方法。
章节到此结束,具体使用方法请看下一章节
go-zero微服务实战——etcd服务注册与发现