go-zero学习 第三章 微服务

go-zero学习 第三章 微服务

  • 1 相关命令
  • 2 微服务代码实战
    • 2.1 基础代码
    • 2.2 API简单调用RPC服务
    • 2.3 服务注册/发现
    • 2.4 文件上传/下载&分组
    • 2.5 go-zero超时时间
    • 2.6 grpc服务端接收请求体大小限制
    • 2.7 grpc客户端接收响应体大小限制
    • 2.8 API和RPC服务拦截器
    • 2.9 服务间通过metadata代替context传值
    • 2.10 RPC服务如何独立调试

1 相关命令

1.1 API服务模块

  1. goctl使用api文件生成api服务命令:
\go-zero-micro\api> goctl api go -api ./doc/all.api -dir ./code/ucenterapi

1.2 RPC服务模块

  1. goctl使用protoc文件生成rpc服务命令:
goctl rpc protoc ./proto/ucenter.proto --go_out=./code/ucenter --go-grpc_out=./code/ucenter --zrpc_out=./code/ucenter --multiple

注意:--go_out--go-grpc_out--zrpc_out 三者配置的路径需要完全一致,否则会报下列错误。

the output of pb.go and _grpc.pb.go must not be the same with --zrpc_out

2 微服务代码实战

2.1 基础代码

基础代码:已生成基本的API服务、RPC服务。

2.2 API简单调用RPC服务

这里以API服务调用RPC服务的登录校验并生成JWT为例。
本次示例代码

1. JWT生成方法

package utils

import "github.com/golang-jwt/jwt/v4"

// GenerateJwtToken 生成JWT
func GenerateJwtToken(secretKey string, iat, seconds, userId int64) (string, error) {
	claims := make(jwt.MapClaims)
	claims["exp"] = iat + seconds
	claims["iat"] = iat
	claims["userId"] = userId
	token := jwt.New(jwt.SigningMethodHS256)
	token.Claims = claims
	return token.SignedString([]byte(secretKey))
}

2. RPC服务登录生成JWT
RPC服务的配置文件加入JWT的AccessSecret(密钥)AccessExpire(过期时间 / 秒)

JWT:
  AccessSecret: 1a3201qa-8b3d-ed0a-05eb-2e9c9b74f6b7
  AccessExpire: 86400

RPC服务的配置文件映射类加入JWT对应结构体

package config

import "github.com/zeromicro/go-zero/zrpc"

type Config struct {
	zrpc.RpcServerConf

	JWT struct {
		AccessSecret string
		AccessExpire int64
	}
}

RPC的登录逻辑模拟数据库查询后生成JWT

package ucentersqlxlogic

import (
	"context"
	"go-zero-micro/common/utils"
	"time"

	"go-zero-micro/rpc/code/ucenter/internal/svc"
	"go-zero-micro/rpc/code/ucenter/ucenter"

	"github.com/jinzhu/copier"
	"github.com/zeromicro/go-zero/core/logx"
)

type LoginUserLogic struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
	logx.Logger
}

func NewLoginUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginUserLogic {
	return &LoginUserLogic{
		ctx:    ctx,
		svcCtx: svcCtx,
		Logger: logx.WithContext(ctx),
	}
}

// LoginUser 用户登录
func (l *LoginUserLogic) LoginUser(in *ucenter.User) (*ucenter.UserLoginResp, error) {
	// todo: add your logic here and delete this line
	//return &ucenter.UserLoginResp{}, nil
	return l.LoginSuccess(in)
}

func (l *LoginUserLogic) LoginSuccess(in *ucenter.User) (*ucenter.UserLoginResp, error) {
	AccessSecret := l.svcCtx.Config.JWT.AccessSecret
	AccessExpire := l.svcCtx.Config.JWT.AccessExpire
	now := time.Now().Unix()

	jwtToken, err := utils.GenerateJwtToken(AccessSecret, now, AccessExpire, in.Id)
	if err != nil {
		return nil, err
	}
	resp := &ucenter.UserLoginResp{}
	copier.Copy(resp, in)
	resp.AccessToken = jwtToken
	resp.AccessExpire = now + AccessExpire
	resp.RefreshAfter = now + AccessExpire/2
	return resp, nil
}

3. API服务调用RPC服务的登录接口
API服务的svc下的servicecontext.go加入待调用服务接口

这里加入了 ucentergorm.UcenterGormucentersqlx.UcenterSqlx,这里使用ucentersqlx.UcenterSqlx里的登录校验接口。

package svc

import (
	"github.com/zeromicro/go-zero/rest"
	"github.com/zeromicro/go-zero/zrpc"
	"go-zero-micro/api/code/ucenterapi/internal/config"
	"go-zero-micro/api/code/ucenterapi/internal/middleware"
	"go-zero-micro/rpc/code/ucenter/client/ucentergorm"
	"go-zero-micro/rpc/code/ucenter/client/ucentersqlx"
)

type ServiceContext struct {
	Config         config.Config
	Check          rest.Middleware
	UcenterGormRpc ucentergorm.UcenterGorm //gorm方式的接口
	UcenterSqlxRpc ucentersqlx.UcenterSqlx //sqlx方式的接口
}

func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config:         c,
		Check:          middleware.NewCheckMiddleware().Handle,
		UcenterGormRpc: ucentergorm.NewUcenterGorm(zrpc.MustNewClient(c.UCenterRpc)),
		UcenterSqlxRpc: ucentersqlx.NewUcenterSqlx(zrpc.MustNewClient(c.UCenterRpc)),
	}
}

API服务的登录逻辑调用RPC服务的登录校验接口

package login

import (
	"context"
	"github.com/jinzhu/copier"
	"go-zero-micro/common/errorx"
	"go-zero-micro/rpc/code/ucenter/ucenter"

	"go-zero-micro/api/code/ucenterapi/internal/svc"
	"go-zero-micro/api/code/ucenterapi/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

type LoginByPasswordLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewLoginByPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginByPasswordLogic {
	return &LoginByPasswordLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *LoginByPasswordLogic) LoginByPassword(req *types.UserLoginPasswordModel) (resp *types.UserLoginResp, err error) {
	// todo: add your logic here and delete this line
	param := &ucenter.User{}
	copier.Copy(param, req)
	loginRes, err := l.svcCtx.UcenterSqlxRpc.LoginUser(l.ctx, param)
	if err != nil {
		return nil, errorx.NewDefaultError(errorx.UserLoginPasswordErrorCode)
	}
	res := &types.UserLoginResp{}
	copier.Copy(res, loginRes)
	return res, nil
}
  1. 登录测试
    请求地址:http://localhost:8888/login/loginByPassword
    请求方式:POST
    请求格式:JSON
    请求数据:{"account":"hello","password":"123456"}
    返回结果:
{
    "code": 200,
    "msg": "success",
    "success": true,
    "data": {
        "id": 0,
        "account": "hello",
        "username": "",
        "gender": 0,
        "avatar": "",
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODgzMDk2NDIsImlhdCI6MTY4ODIyMzI0MiwidXNlcklkIjowfQ.XYRNMXH0ViWai9N3THTe2X_38st_kFLb76TDHM8ko-k",
        "accessExpire": 1688309642,
        "refreshAfter": 1688266442
    }
}
  1. 未登录测试
    请求地址:http://localhost:8888/ucenter/getUserById
    请求方式:GET
    请求格式:JSON
    请求数据:?id=1
    返回结果:401 Unauthorized

  2. 已登录测试
    请求地址:http://localhost:8888/ucenter/getUserById
    请求方式:GET
    请求格式:URL
    请求数据:?id=1
    请求头:需要在Authorization中选择 Bearer Token,填入在第 4步获取的token
    返回结果:

{
    "code": 200,
    "msg": "success",
    "success": true,
    "data": null
}

2.3 服务注册/发现

参考1:服务连接
参考2:服务发现/直连模式

go-zero的服务发现支持以下几种形式:

  1. K8S服务发现
  2. ETCD【默认】
  3. IP直连
  4. Consul
  5. Nacos
  6. PolarisMash

在 go-zero 中,支持 ETCD【默认】服务注册和直连模式。

这里仅以ETCD、IP直连。

1 ETCD:
go-zero默认是通过ETCD注册发现,在2.2 API简单调用RPC服务基础上,通过查看API服务的登录调用RPC服务登录校验的过程,可以发现底层还是通过GRPC调用,go-zero只是进一步进行了封装。

ETCD方式的流程和代码与2.2 API简单调用RPC服务API服务调用RPC服务的登录接口内容一致,这里不再重复。

有以下两点需要注意:

  1. go-zero使用k8s时,服务发现不能使用直连方式,可能会导致负载不均衡。
  2. API服务的yamlListenOn: 0.0.0.0:8080,和RPC服务的yamlHost: 0.0.0.0,设置成0.0.0.0会自动获取内网IP,如果设置成固定内网ip的话就是直接写成指定的IP了。

2 IP直连:(不推荐,RPC服务的具体地址不能动态修改)

有两种方式

方式一(不推荐):使用grpc,因为改动位置过多,改动较大。
本次示例代码

参考2:服务发现/直连模式

直连分为两种模式,一种是直连单个服务,一种是直连服务集群。

  1. RPC服务
    使用直连模式时RPC服务的yaml配置中仅需要去除 ETCD 配置即可,go-zero 自动识别,最简配置参考:

只需要去除在ETCD注册服务的配置即可

Name: ucenter.rpc
ListenOn: 0.0.0.0:8080
#Etcd:
#  Hosts:
#  - 127.0.0.1:2379
#  Key: ucenter.rpc

JWT:
  AccessSecret: 1a3201qa-8b3d-ed0a-05eb-2e9c9b74f6b7
  AccessExpire: 86400

修改完后重启RPC服务即可。

  1. API服务
package svc

import (
	"github.com/zeromicro/go-zero/rest"
	"github.com/zeromicro/go-zero/zrpc"
	"go-zero-micro/api/code/ucenterapi/internal/config"
	"go-zero-micro/api/code/ucenterapi/internal/middleware"
	"go-zero-micro/rpc/code/ucenter/ucenter"
)

type ServiceContext struct {
	Config config.Config
	Check  rest.Middleware
	//UcenterGormRpc ucentergorm.UcenterGorm //gorm方式的接口
	//UcenterSqlxRpc ucentersqlx.UcenterSqlx //sqlx方式的接口
	UcenterSqlxDC ucenter.UcenterSqlxClient //sqlx方式的GRPC接口
}

func NewServiceContext(c config.Config) *ServiceContext {
	conn := zrpc.MustNewClient(zrpc.RpcClientConf{
		Target: "dns:///127.0.0.1:8080",
	})
	return &ServiceContext{
		Config: c,
		Check:  middleware.NewCheckMiddleware().Handle,
		//UcenterGormRpc: ucentergorm.NewUcenterGorm(zrpc.MustNewClient(c.UCenterRpc)),
		//UcenterSqlxRpc: ucentersqlx.NewUcenterSqlx(zrpc.MustNewClient(c.UCenterRpc)),
		UcenterSqlxDC: ucenter.NewUcenterSqlxClient(conn.Conn()),
	}
}

登录接口:

package login

import (
	"context"
	"github.com/jinzhu/copier"
	"go-zero-micro/common/errorx"
	"go-zero-micro/rpc/code/ucenter/ucenter"

	"go-zero-micro/api/code/ucenterapi/internal/svc"
	"go-zero-micro/api/code/ucenterapi/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

type LoginByPasswordLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewLoginByPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginByPasswordLogic {
	return &LoginByPasswordLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *LoginByPasswordLogic) LoginByPassword(req *types.UserLoginPasswordModel) (resp *types.UserLoginResp, err error) {
	// todo: add your logic here and delete this line
	param := &ucenter.User{}
	copier.Copy(param, req)
	//loginRes, err := l.svcCtx.UcenterSqlxRpc.LoginUser(l.ctx, param)
	loginRes, err := l.svcCtx.UcenterSqlxDC.LoginUser(l.ctx, param)
	if err != nil {
		return nil, errorx.NewDefaultError(errorx.UserLoginPasswordErrorCode)
	}
	res := &types.UserLoginResp{}
	copier.Copy(res, loginRes)
	return res, nil
}

方式2(推荐):
本次示例代码

参考:二十二、api与rpc直连方式服务发现,只需要更改RPC和API的yaml文件即可。

  1. RPC服务
    使用直连模式时RPC服务的yaml配置中仅需要去除 ETCD 配置即可,go-zero 自动识别,最简配置参考:

只需要去除在ETCD注册服务的配置即可

Name: ucenter.rpc
ListenOn: 0.0.0.0:8080
#Etcd:
#  Hosts:
#  - 127.0.0.1:2379
#  Key: ucenter.rpc

JWT:
  AccessSecret: 1a3201qa-8b3d-ed0a-05eb-2e9c9b74f6b7
  AccessExpire: 86400

修改完后重启RPC服务即可。

  1. API服务
    使用直连模式时API服务的yaml配置中仅需要将 ETCD 配置去除,同时在Endpoints中添加服务端ip地址即可,可添加多个,go-zero 自动识别:
Name: ucenter-api
Host: 0.0.0.0
Port: 8888

Auth:
  AccessSecret: 1a3201qa-8b3d-ed0a-05eb-2e9c9b74f6b7
  AccessExpire: 86400

#web请求到此api服务的超时时间
Timeout: 10000
# 将请求体最大允许字节数从1MB改为1000MB
MaxBytes: 1048576000

#文件
UploadFile:
  MaxFileNum: 1000
  MaxFileSize: 1048576000  # 1000MB
  SavePath: projects/go-zero-micro/uploadFiles/

# UCenter 服务
UCenterRpc:
#  Etcd:
#    Hosts:
#      - 127.0.0.1:2379
#    Key: ucenter.rpc
  Endpoints:
    - 127.0.0.1:8080
  #api请求rpc服务的超时时间
  Timeout: 10000

#日志配置
Log:
  Mode: file
  Path: log/go-zero-micro
  Level: error
  Compress: true
  KeepDays: 180

2.4 文件上传/下载&分组

本次示例代码

这里的文件上传包括api服务rpc服务之间文件传输,具体涉及到的文件以及变动,可查看本次示例代码。

先从rpc服务开始写代码,然后api服务。

1 PRC服务代码

  1. yaml配置文件:设置文件的存储位置及文件大小的限制。
Name: ucenter.rpc
ListenOn: 0.0.0.0:8080
Etcd:
  Hosts:
  - 127.0.0.1:2379
  Key: ucenter.rpc

JWT:
  AccessSecret: 1a3201qa-8b3d-ed0a-05eb-2e9c9b74f6b7
  AccessExpire: 86400

#文件
UploadFile:
  MaxFileNum: 1000
  MaxFileSize: 1048576000  # 1000MB
  SavePath: projects/go-zero-micro/uploadFiles/

#日志配置
Log:
  Mode: file
  Path: log/go-zero-micro/ucenterpc
  Level: error
  Compress: true
  KeepDays: 180
  1. 配置映射类:
package config

import "github.com/zeromicro/go-zero/zrpc"

type Config struct {
	zrpc.RpcServerConf

	JWT struct {
		AccessSecret string
		AccessExpire int64
	}
	UploadFile UploadFile
}

type UploadFile struct {
	MaxFileNum  int64
	MaxFileSize int64
	SavePath    string
}
  1. ucenter.proto(这里只显示了部分代码,全部的请到github查看):文件下载是使用流的方式。
message FileInfo {
  //文件-id
  int64 FileId = 1;
  //文件-名称
  string FileName = 2;
  //文件类型 1:图片 2:音频 3:视频 4:文件
  string FileType = 3;
  //文件大小
  int64 FileSize = 4;
  //文件流
  bytes FileData = 5;
  //文件地址
  string FileUrl = 6;
}

// 文件集合
message FileList {
  //父级-id
  int64 PidId = 1;
  //父级-id
  int64 Type = 2;
  //文件信息
  repeated FileInfo file = 3;
  // 创建者-id
  int64 CreatedBy = 4;
  // 创建时间
  string CreatedAt = 5;
  // 更新者-id
  int64 UpdatedBy = 6;
  // 更新时间
  string UpdatedAt = 7;
  // 删除者-id
  int64 DeletedBy = 8;
  // 删除时间
  string DeletedAt = 9;
}

service fileStorage {
  //文件上传
  rpc FileUpload(FileList) returns(BaseResp);
  //文件下载
  rpc FileDownload(FileInfo) returns(stream FileInfo);
}
  1. 根据ucenter.proto生成go-zerorpc服务代码。
goctl rpc protoc ./proto/ucenter.proto --go_out=./code/ucenter --go-grpc_out=./code/ucenter --zrpc_out=./code/ucenter --multiple
  1. 文件上传功能代码位置:internal/logic/filestorage/fileuploadlogic.go
    核心代码逻辑:
// 文件上传
func (l *FileUploadLogic) FileUpload(in *ucenter.FileList) (*ucenter.BaseResp, error) {
	// todo: add your logic here and delete this line
	SavePath := l.svcCtx.Config.UploadFile.SavePath //上传文件的存储路径
	utils.CreateDir(SavePath)
	for _, fileInfo := range in.File {
		//获取文件名称带后缀
		fileNameWithSuffix := path.Base(fileInfo.FileName)
		//获取文件的后缀(文件类型)
		fileType := path.Ext(fileNameWithSuffix)
		//生成UUID防止文件被覆盖
		uuidName := strings.Replace(uuid.NewV4().String(), "-", "", -1)
		saveName := uuidName + fileType

		saveFullPath := SavePath + saveName
		err := ioutil.WriteFile(saveFullPath, fileInfo.FileData, 0644)
		if err != nil {
			// handle error
		}
	}
	return &ucenter.BaseResp{
		Data: "upload success",
	}, nil
}
  1. 文件下载功能代码位置:internal/logic/filestorage/filedownloadlogic.go
    核心代码逻辑:
// 文件下载
func (l *FileDownloadLogic) FileDownload(in *ucenter.FileInfo, stream ucenter.FileStorage_FileDownloadServer) error {
	// todo: add your logic here and delete this line
	SavePath := l.svcCtx.Config.UploadFile.SavePath //上传文件的存储路径

	filePath := SavePath + in.FileUrl
	_, err := os.Stat(filePath)
	if err != nil || os.IsNotExist(err) {
		return errors.New("文件不存在")
	}
	bytes, err := os.ReadFile(filePath)
	if err != nil {
		return errors.New("读取文件失败")
	}

	response := &ucenter.FileInfo{}
	copier.Copy(response, in)
	response.FileName = "go-zero.png"
	response.FileData = bytes
	if err := stream.Send(response); err != nil {
		return err
	}
	return nil
}

2 API服务代码

  1. yaml配置文件:
  • 修改MaxBytes参数是为了调整api服务处理请求体大小限制,如果MaxBytes不调大,上传文件会失败,同时时api服务会返回 413Request Entity Too Large
  • 但是如果文件过大,则有可能导致内存溢出,因此在yaml中修改MaxBytes参数时,MaxBytes又不能设置的过大。
Name: ucenter-api
Host: 0.0.0.0
Port: 8888

Auth:
  AccessSecret: 1a3201qa-8b3d-ed0a-05eb-2e9c9b74f6b7
  AccessExpire: 86400

#web请求到此api服务的超时时间
Timeout: 10000
# 将请求体最大允许字节数从1MB改为1000MB
MaxBytes: 1048576000

#文件
UploadFile:
  MaxFileNum: 1000
  MaxFileSize: 1048576000  # 1000MB
  SavePath: projects/go-zero-micro/uploadFiles/

# UCenter 服务
UCenterRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: ucenter.rpc
  #api请求rpc服务的超时时间
  Timeout: 10000

#日志配置
Log:
  Mode: file
  Path: log/go-zero-micro/ucenterapi
  Level: error
  Compress: true
  KeepDays: 180
  1. file.api:注意:这里的文件相关接口上没有加鉴权。
syntax = "v1"

info(
	title : "go-zero-micro"
	desc: "userapi"
	author: "ximuqi"
	email: "xxx"
	version: "0.0.1"
)

type (
	/* 1 上传文件 */
	FileUploadReq {
		Id       int64   `form:"id"`                // 父级-id
		Type     int64   `form:"type,optional"`     // 类型 1:类型1;2:类型2
		FileList []*byte `form:"fileList,optional"` // 文件列表
	}

	/* 2 下载/预览文件 */
	FileShowReq {
		Id      int64  `form:"id"`               // 文件-id
		FileUrl string `form:"fileUrl,optional"` // 文件地址
	}
)
@server(
	group: fileStorage
	prefix: /fileStorage
)
service ucenter-api {
	@doc(
		summary: "1 上传文件"
	)
	@handler fileUpload
	post /fileUpload (FileUploadReq) returns (BaseModel)

	@doc(
		summary: "2 文件下载"
	)
	@handler fileDownload
	get /fileDownload (FileShowReq)

	@doc(
		summary: "3 文件预览"
	)
	@handler filePreview
	get /filePreview (FileShowReq)
}
  1. 根据file.api生成go-zero的api服务代码。
goctl api go -api ./doc/all.api -dir ./code/ucenterapi
  1. internal/svc/servicecontext.go加入rpc服务的文件存储相关服务接口
package svc

import (
	"github.com/zeromicro/go-zero/rest"
	"github.com/zeromicro/go-zero/zrpc"
	"go-zero-micro/api/code/ucenterapi/internal/config"
	"go-zero-micro/api/code/ucenterapi/internal/middleware"
	"go-zero-micro/rpc/code/ucenter/client/filestorage"
	"go-zero-micro/rpc/code/ucenter/client/ucentergorm"
	"go-zero-micro/rpc/code/ucenter/client/ucentersqlx"
)

type ServiceContext struct {
	Config         config.Config
	Check          rest.Middleware
	UcenterGormRpc ucentergorm.UcenterGorm //gorm方式的接口
	UcenterSqlxRpc ucentersqlx.UcenterSqlx //sqlx方式的接口
	FileStorageRpc filestorage.FileStorage //文件存储相关接口
}

func NewServiceContext(c config.Config) *ServiceContext {
	uCenterRpcClient := zrpc.MustNewClient(c.UCenterRpc)

	return &ServiceContext{
		Config:         c,
		Check:          middleware.NewCheckMiddleware().Handle,
		UcenterGormRpc: ucentergorm.NewUcenterGorm(uCenterRpcClient),
		UcenterSqlxRpc: ucentersqlx.NewUcenterSqlx(uCenterRpcClient),
		FileStorageRpc: filestorage.NewFileStorage(uCenterRpcClient),
	}
}
  1. 文件上传功能代码位置:internal/logic/fileStorage/fileuploadlogic.go
package fileStorage

import (
	"bytes"
	"context"
	"fmt"
	"github.com/jinzhu/copier"
	uuid "github.com/satori/go.uuid"
	"go-zero-micro/common/utils"
	"go-zero-micro/rpc/code/ucenter/ucenter"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path"
	"strings"

	"go-zero-micro/api/code/ucenterapi/internal/svc"
	"go-zero-micro/api/code/ucenterapi/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

type FileUploadLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewFileUploadLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FileUploadLogic {
	return &FileUploadLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *FileUploadLogic) FileUpload(request *http.Request, req *types.FileUploadReq) (resp *types.BaseModel, err error) {
	// todo: add your logic here and delete this line
	//return LocalFileToByte(l, request, requestBody)
	return FileToByte(l, request, req)
}

// LocalFileToByte 方式1:ioutil.ReadFile()转换成byte需要知道文件路径,因此会生成临时文件,不适合处理文件上传的场景
func LocalFileToByte(l *FileUploadLogic, request *http.Request, requestBody *types.FileUploadReq) (resp *types.BaseModel, err error) {
	SavePath := l.svcCtx.Config.UploadFile.SavePath //上传文件的存储路径
	utils.CreateDir(SavePath)
	files := request.MultipartForm.File["fileList"]
	res := &types.BaseModel{
		Data: "上传成功",
	}
	param := &ucenter.FileList{}
	copier.Copy(param, requestBody)
	rpcFileList := make([]*ucenter.FileInfo, 0)
	typeId := fmt.Sprintf("%d", requestBody.Type)
	// 遍历所有文件
	for _, fileHeader := range files {
		//获取文件大小
		fileSize := fileHeader.Size
		//获取文件名称带后缀
		fileNameWithSuffix := path.Base(fileHeader.Filename)
		//获取文件的后缀(文件类型)
		fileType := path.Ext(fileNameWithSuffix)
		//生成UUID防止文件被覆盖
		uuidName := typeId + "_" + strings.Replace(uuid.NewV4().String(), "-", "", -1)

		saveName := uuidName + fileType
		saveFullPath := SavePath + saveName
		logx.Infof("upload file: %+v, file size: %d", fileNameWithSuffix, fileSize)
		file, err := fileHeader.Open()
		tempFile, err := os.Create(saveFullPath)
		if err != nil {
			return nil, err
		}
		io.Copy(tempFile, file)
		//关闭文件
		file.Close()
		tempFile.Close()

		//方式1:ioutil.ReadFile()转换成byte需要知道文件路径,不适合处理文件上传的场景
		content, err := ioutil.ReadFile(saveFullPath)
		fileInfo := &ucenter.FileInfo{
			FileId:   requestBody.Id,
			FileName: fileNameWithSuffix,
			FileType: fileType,
			FileSize: fileSize,
			FileData: content,
		}
		err = os.Remove(saveFullPath)
		if err != nil {
			logx.Infof("%s:删除失败", fileNameWithSuffix)
		}
		rpcFileList = append(rpcFileList, fileInfo)
	}
	param.File = rpcFileList
	uploadRes, err := l.svcCtx.FileStorageRpc.FileUpload(l.ctx, param)
	if err != nil {
		return nil, err
	}
	res.Data = uploadRes.Data
	return res, nil
}

// FileToByte 方式2:转换成byte适合上传文件的场景
func FileToByte(l *FileUploadLogic, request *http.Request, requestBody *types.FileUploadReq) (resp *types.BaseModel, err error) {
	files := request.MultipartForm.File["fileList"]
	res := &types.BaseModel{
		Data: "上传成功",
	}
	param := &ucenter.FileList{}
	copier.Copy(param, requestBody)
	rpcFileList := make([]*ucenter.FileInfo, 0)
	// 遍历所有文件
	for _, fileHeader := range files {
		//获取文件大小
		fileSize := fileHeader.Size
		//获取文件名称带后缀
		fileNameWithSuffix := path.Base(fileHeader.Filename)
		//获取文件的后缀(文件类型)
		fileType := path.Ext(fileNameWithSuffix)
		logx.Infof("upload file: %+v, file size: %d", fileNameWithSuffix, fileSize)
		file, err := fileHeader.Open()
		if err != nil {
			return nil, err
		}
		//方式2:转换成byte适合上传文件的场景
		fil := make([][]byte, 0)
		var b int64 = 0
		// 通过for循环写入
		for {
			buffer := make([]byte, 1024)
			n, err := file.ReadAt(buffer, b)
			b = b + int64(n)
			fil = append(fil, buffer)
			if err != nil {
				fmt.Println(err.Error())
				break
			}
		}
		// 生成最后的文件字节流
		content := bytes.Join(fil, []byte(""))
		fileInfo := &ucenter.FileInfo{
			FileId:   requestBody.Id,
			FileName: fileNameWithSuffix,
			FileType: fileType,
			FileSize: fileSize,
			FileData: content,
		}
		rpcFileList = append(rpcFileList, fileInfo)
	}
	param.File = rpcFileList
	uploadRes, err := l.svcCtx.FileStorageRpc.FileUpload(l.ctx, param)
	if err != nil {
		return nil, err
	}
	res.Data = uploadRes.Data
	return res, nil
}
  1. 文件下载功能代码位置:internal/logic/fileStorage/filedownloadlogic.go,下载是写入到响应流中的,所以需要加入 writer http.ResponseWriter
package fileStorage

import (
	"context"
	"errors"
	"github.com/jinzhu/copier"
	"go-zero-micro/api/code/ucenterapi/internal/svc"
	"go-zero-micro/api/code/ucenterapi/internal/types"
	"go-zero-micro/rpc/code/ucenter/ucenter"
	"net/http"

	"github.com/zeromicro/go-zero/core/logx"
)

type FileDownloadLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
	writer http.ResponseWriter
}

func NewFileDownloadLogic(ctx context.Context, svcCtx *svc.ServiceContext, writer http.ResponseWriter) *FileDownloadLogic {
	return &FileDownloadLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
		writer: writer,
	}
}

func (l *FileDownloadLogic) FileDownload(req *types.FileShowReq) error {
	// todo: add your logic here and delete this line
	param := &ucenter.FileInfo{}
	copier.Copy(param, req)
	downloadRes, err := l.svcCtx.FileStorageRpc.FileDownload(l.ctx, param)
	if err != nil {
		return errors.New("文件服务异常")
	}
	fileInfo, err := downloadRes.Recv()
	if err != nil {
		return errors.New("文件下载失败")
	}
	fileName := fileInfo.FileName
	byteArr := fileInfo.FileData

	//如果是下载,则需要在Header中设置这两个参数
	//l.writer.Header().Add("Content-Type", "application/octet-stream")
	//l.writer.Header().Add("Content-Disposition", "attachment; filename= "+fileName)

	l.writer.Header().Add("Content-Type", "application/octet-stream")
	l.writer.Header().Add("Content-Disposition", "attachment; filename= "+fileName)
	l.writer.Write(byteArr)
	return nil
}
  1. 文件预览功能代码位置:internal/logic/fileStorage/filepreviewlogic.go(预览只针对浏览器支持的类型有效),预览和下载一样是写入到响应流中的,所以也需要加入 writer http.ResponseWriter
package fileStorage

import (
	"context"
	"errors"
	"github.com/jinzhu/copier"
	"go-zero-micro/rpc/code/ucenter/ucenter"
	"net/http"

	"go-zero-micro/api/code/ucenterapi/internal/svc"
	"go-zero-micro/api/code/ucenterapi/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

type FilePreviewLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
	writer http.ResponseWriter
}

func NewFilePreviewLogic(ctx context.Context, svcCtx *svc.ServiceContext, writer http.ResponseWriter) *FilePreviewLogic {
	return &FilePreviewLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
		writer: writer,
	}
}

func (l *FilePreviewLogic) FilePreview(req *types.FileShowReq) error {
	// todo: add your logic here and delete this line
	param := &ucenter.FileInfo{}
	copier.Copy(param, req)
	downloadRes, err := l.svcCtx.FileStorageRpc.FileDownload(l.ctx, param)
	if err != nil {
		return errors.New("文件服务异常")
	}
	fileInfo, err := downloadRes.Recv()
	if err != nil {
		return errors.New("打开文件失败")
	}
	//fileName := fileInfo.FileName
	byteArr := fileInfo.FileData

	//如果是下载,则需要在Header中设置这两个参数
	//l.writer.Header().Add("Content-Type", "application/octet-stream")
	//l.writer.Header().Add("Content-Disposition", "attachment; filename= "+fileName)
	l.writer.Write(byteArr)
	return nil
}

3 功能测试

  1. 文件上传功能

请求地址:http://localhost:8888/fileStorage/fileUpload
请求方式:POST
请求格式:FORM
请求数据:id=1,type=1,fileList(文件类型)
返回结果:

{
    "code": 200,
    "msg": "success",
    "success": true,
    "data": {
        "id": 0,
        "name": "",
        "data": "upload success"
    }
}
  1. 文件下载功能

请求地址:http://127.0.0.1:8888/fileStorage/fileDownload?id=1&fileUrl=d3d22404926349fd8cb1924b12cadae8.png
请求方式:GET
请求格式:URL
请求数据:id=1&fileUrl=d3d22404926349fd8cb1924b12cadae8.png
返回结果:请求成功后会立即下载。

  1. 文件预览功能

请求地址:http://127.0.0.1:8888/fileStorage/filePreview?id=1&fileUrl=d3d22404926349fd8cb1924b12cadae8.png
请求方式:GET
请求格式:URL
请求数据:id=1&fileUrl=d3d22404926349fd8cb1924b12cadae8.png
返回结果:请求成功后会立即显示在浏览器页面上。

4 注意事项

  1. api服务调用rpc服务报错并返回错误信息:unknown service ucenter.fileStorage
    原因:在启动类中rpc里新加的文件存储相关服务没有注册到rpc服务中,如果启动类的所有代码一直是使用goctl的命令自动生成的,则可能不会出现这个问题。
    解决办法:手动在启动类里加上注册文件存储相关服务的代码。
package main

import (
	"flag"
	"fmt"
	filestorageServer "go-zero-micro/rpc/code/ucenter/internal/server/filestorage"

	"go-zero-micro/rpc/code/ucenter/internal/config"
	ucentergormServer "go-zero-micro/rpc/code/ucenter/internal/server/ucentergorm"
	ucentersqlxServer "go-zero-micro/rpc/code/ucenter/internal/server/ucentersqlx"
	"go-zero-micro/rpc/code/ucenter/internal/svc"
	"go-zero-micro/rpc/code/ucenter/ucenter"

	"github.com/zeromicro/go-zero/core/conf"
	"github.com/zeromicro/go-zero/core/service"
	"github.com/zeromicro/go-zero/zrpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

var configFile = flag.String("f", "conf/dev/rpc/ucenter.yaml", "the config file")

func main() {
	flag.Parse()

	var c config.Config
	conf.MustLoad(*configFile, &c)
	ctx := svc.NewServiceContext(c)

	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
		ucenter.RegisterUcenterSqlxServer(grpcServer, ucentersqlxServer.NewUcenterSqlxServer(ctx))
		ucenter.RegisterUcenterGormServer(grpcServer, ucentergormServer.NewUcenterGormServer(ctx))

		//新增的分组接口必须要在这里注册,根据proto生成时可能未新增,否则会报 unknown service ucenter.fileStorage
		ucenter.RegisterFileStorageServer(grpcServer, filestorageServer.NewFileStorageServer(ctx))

		if c.Mode == service.DevMode || c.Mode == service.TestMode {
			reflection.Register(grpcServer)
		}
	})
	defer s.Stop()

	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
	s.Start()
}
  1. 文件上传请求超时
    如果文件过大,在上传文件时可能会出现请求等待一段时间后没有返回响应信息。
    原因
    (1)超时时间设置过短;
    (2)api往rpc发送信息时,数据过大;

解决办法
2.5 go-zero超时时间2.6节 grpc请求体大小限制中讲解。

2.5 go-zero超时时间

本次示例代码

在处理请求时有可能出现时间过长的情况,go-zero设置了默认的超时时间(单位是毫秒)避免等待过长。

在go-zero微服务超时时间的设置有三处:

  1. rpc服务处理grpc请求时(grpc服务端);
  2. api服务调用rpc服务时(grpc客户端);
  3. api服务处理http请求时;

其中1是在rpc服务中配置,2、3是在api服务中配置。如果是单体服务,则只需要配置3. api服务处理http请求时即可。

以登录超时演示实际效果:先配置rpc服务中超时,再配置api服务中的超时。

1 rpc服务处理grpc请求时(grpc服务端):

这里是在rpc服务的配置文件yaml中设置,注意Timeout的层级位置。

Name: ucenter.rpc
ListenOn: 0.0.0.0:8080
Etcd:
  Hosts:
  - 127.0.0.1:2379
  Key: ucenter.rpc

#rpc处理超时时间 10秒
Timeout: 10000

#开启grpc调试模式
Mode: dev

JWT:
  AccessSecret: 1a3201qa-8b3d-ed0a-05eb-2e9c9b74f6b7
  AccessExpire: 86400

#文件
UploadFile:
  MaxFileNum: 1000
  MaxFileSize: 1048576000  # 1000MB
  SavePath: projects/go-zero-micro/uploadFiles/

#日志配置
Log:
  Mode: file
  Path: log/go-zero-micro/ucenterpc
  Level: error
  Compress: true
  KeepDays: 180

zrpc.RpcClientConf 中可以查看到go-zero设置的默认超时时间是2000毫秒

	// A RpcServerConf is a rpc server config.
	RpcServerConf struct {
		service.ServiceConf
		ListenOn      string
		Etcd          discov.EtcdConf    `json:",optional,inherit"`
		Auth          bool               `json:",optional"`
		Redis         redis.RedisKeyConf `json:",optional"`
		StrictControl bool               `json:",optional"`
		// setting 0 means no timeout
		Timeout      int64 `json:",default=2000"`
		CpuThreshold int64 `json:",default=900,range=[0:1000]"`
		// grpc health check switch
		Health      bool `json:",default=true"`
		Middlewares ServerMiddlewaresConf
	}

rpc服务模拟登录逻辑处理超时,在internal/logic/ucentersqlx/loginuserlogic.go中设置睡眠10秒钟。

package ucentersqlxlogic

import (
	"context"
	"go-zero-micro/common/utils"
	"time"

	"go-zero-micro/rpc/code/ucenter/internal/svc"
	"go-zero-micro/rpc/code/ucenter/ucenter"

	"github.com/jinzhu/copier"
	"github.com/zeromicro/go-zero/core/logx"
)

type LoginUserLogic struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
	logx.Logger
}

func NewLoginUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginUserLogic {
	return &LoginUserLogic{
		ctx:    ctx,
		svcCtx: svcCtx,
		Logger: logx.WithContext(ctx),
	}
}

// LoginUser 用户登录
func (l *LoginUserLogic) LoginUser(in *ucenter.User) (*ucenter.UserLoginResp, error) {
	// todo: add your logic here and delete this line
	//return &ucenter.UserLoginResp{}, nil
	
	//模拟耗时 20秒钟
	sleepTime := 20 * time.Second
	time.Sleep(sleepTime)
	return l.LoginSuccess(in)
}

func (l *LoginUserLogic) LoginSuccess(in *ucenter.User) (*ucenter.UserLoginResp, error) {
	AccessSecret := l.svcCtx.Config.JWT.AccessSecret
	AccessExpire := l.svcCtx.Config.JWT.AccessExpire
	now := time.Now().Unix()

	jwtToken, err := utils.GenerateJwtToken(AccessSecret, now, AccessExpire, in.Id)
	if err != nil {
		return nil, err
	}
	resp := &ucenter.UserLoginResp{}
	copier.Copy(resp, in)
	resp.AccessToken = jwtToken
	resp.AccessExpire = now + AccessExpire
	resp.RefreshAfter = now + AccessExpire/2
	return resp, nil
}

rpc登录超时测试
因已在配置文件中开启了grpc调试模式,故可以直接使用postman进行grpc请求测试。

请求地址:http://127.0.0.1:8080
请求方式:GRPC
请求格式:JSON
请求数据:{“account”:“hello”,“password”:“123456”}
测试结果:Status code: 4 DEADLINE_EXCEEDED Time: 9982 ms

结论:配置rpc服务处理grpc请求超时为10秒钟,模拟登录处理耗时为20秒钟,超过rpc服务配置的超时时间,故rpc在10秒钟后返回结果,所以rpc配置的超时时间已生效。


2 api服务调用rpc服务时(grpc客户端):

是在api服务的配置文件yaml中设置,它是设置在UCenterRpc下,这里设置10秒钟,注意Timeout的层级位置。

Name: ucenter-api
Host: 0.0.0.0
Port: 8888

Auth:
  AccessSecret: 1a3201qa-8b3d-ed0a-05eb-2e9c9b74f6b7
  AccessExpire: 86400

#web请求到此api服务的超时时间
Timeout: 10000
# 将请求体最大允许字节数从1MB改为1000MB
MaxBytes: 1048576000

#文件
UploadFile:
  MaxFileNum: 1000
  MaxFileSize: 1048576000  # 1000MB
  SavePath: projects/go-zero-micro/uploadFiles/

# UCenter 服务
UCenterRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: ucenter.rpc
  #api请求rpc服务的超时时间
  Timeout: 5000

#日志配置
Log:
  Mode: file
  Path: log/go-zero-micro/ucenterapi
  Level: error
  Compress: true
  KeepDays: 180

zrpc.RpcClientConf 中可以查看到go-zero设置的默认超时时间是2000毫秒

	// A RpcClientConf is a rpc client config.
	RpcClientConf struct {
		Etcd          discov.EtcdConf `json:",optional,inherit"`
		Endpoints     []string        `json:",optional"`
		Target        string          `json:",optional"`
		App           string          `json:",optional"`
		Token         string          `json:",optional"`
		NonBlock      bool            `json:",optional"`
		Timeout       int64           `json:",default=2000"`
		KeepaliveTime time.Duration   `json:",optional"`
		Middlewares   ClientMiddlewaresConf
	}

3 api服务处理http请求时:2 api服务调用rpc服务时(grpc客户端)一样是在api服务的配置文件yaml中设置,注意Timeout的层级位置。

rest.RestConf 中可以查看到go-zero设置的默认超时时间是3000毫秒

	RestConf struct {
		service.ServiceConf
		Host     string `json:",default=0.0.0.0"`
		Port     int
		CertFile string `json:",optional"`
		KeyFile  string `json:",optional"`
		Verbose  bool   `json:",optional"`
		MaxConns int    `json:",default=10000"`
		MaxBytes int64  `json:",default=1048576"`
		// milliseconds
		Timeout      int64         `json:",default=3000"`
		CpuThreshold int64         `json:",default=900,range=[0:1000]"`
		Signature    SignatureConf `json:",optional"`
		// There are default values for all the items in Middlewares.
		Middlewares MiddlewaresConf
		// TraceIgnorePaths is paths blacklist for trace middleware.
		TraceIgnorePaths []string `json:",optional"`
	}

记录api调用rpc服务的超时时间,在 internal/logic/login/loginbypasswordlogic.go

package login

import (
	"context"
	"fmt"
	"github.com/jinzhu/copier"
	"go-zero-micro/common/errorx"
	"go-zero-micro/rpc/code/ucenter/ucenter"
	"time"

	"go-zero-micro/api/code/ucenterapi/internal/svc"
	"go-zero-micro/api/code/ucenterapi/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

type LoginByPasswordLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewLoginByPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginByPasswordLogic {
	return &LoginByPasswordLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *LoginByPasswordLogic) LoginByPassword(req *types.UserLoginPasswordModel) (resp *types.UserLoginResp, err error) {
	// todo: add your logic here and delete this line
	param := &ucenter.User{}
	copier.Copy(param, req)
	
	fmt.Printf("call rpc start\n")
	startTime := time.Now()
	loginRes, err := l.svcCtx.UcenterSqlxRpc.LoginUser(l.ctx, param)

	cost := time.Since(startTime) / time.Second
	fmt.Printf("call rpc end:%d秒\n", cost)
	if err != nil {
		return nil, errorx.NewDefaultError(errorx.UserLoginPasswordErrorCode)
	}
	res := &types.UserLoginResp{}
	copier.Copy(res, loginRes)
	return res, nil
}

4 模拟登录测试

第一次测试
请求地址:http://localhost:8888/login/loginByPassword
请求方式:POST
请求格式:JSON
请求数据:{“account”:“hello”,“password”:“123456”}
测试结果:
(1)api调用rpc耗时5秒钟;
(2)api服务处理http请求耗时5秒钟;

结论:因api服务调用rpc服务时超时时间为5秒钟,rpc在处理登录逻辑时,模拟登录处理耗时为20秒钟,超过api调用rpc配置的超时时间,故api在5秒钟后返回结果,所以 2 api服务调用rpc服务时(grpc客户端)配置的超时时间已生效


第二次测试:(这次把api服务处理http请求时的超时时间由10秒减为2秒)

请求地址:http://localhost:8888/login/loginByPassword
请求方式:POST
请求格式:JSON
请求数据:{“account”:“hello”,“password”:“123456”}
测试结果:
(1)api服务处理http请求耗时2秒钟;

结论:因api服务处理http请求时的超时时间为2秒钟,所以当api处理登录逻辑超过2秒钟后。直接返回登陆超时,所以 3 api服务处理http请求时配置的超时时间已生效

综上:在api服务调用rpc服务时,超时时间以最小者为优先生效。


2.6 grpc服务端接收请求体大小限制

本次示例代码

API服务往RPC服务传递数据实际就是grpc的客户端grpc的服务端发送数据。

grpc的服务端在接收数据时默认是有数据大小限制的,本次在2.5超时时间基础上进一步学习了解。

1 调整配置:

  1. 调整超时时间:将rpc和api的yaml中将超时时间统一调整为600000毫秒(即10分钟)
  2. rpc的yaml中关闭grpc调试模式
  3. rpc的登录处理逻辑屏蔽模拟耗时操作
  4. api的yaml中将MaxBytes从1MB改为10000MB

2 大文件上传测试

请求地址:http://localhost:8888/fileStorage/fileUpload
请求方式:POST
请求格式:FORM
请求数据:id=1,type=1,fileList(文件类型,文件大小:167,840,304 字节,约160 MB
测试结果:在 internal/logic/fileStorage/fileuploadlogic.go调用rpc服务的地方查看返回的错误信息,或者将rpc返回的错误信息返回给前端。
go-zero学习 第三章 微服务_第1张图片
可以看到api往rpc传输文件失败的原因是文件的大小167840818字节(约160 MB)超过了rpc端(grpc服务端)消息体接收限制4194304字节(约4MB)的大小。

3 调整grpc服务端接收请求体大小限制
在启动类里加上:

	MaxFileSize := int(c.UploadFile.MaxFileSize)
	//调整RPC服务端收到的消息体大小限制
	s.AddOptions(grpc.MaxRecvMsgSize(MaxFileSize))

完整代码:

package main

import (
	"flag"
	"fmt"
	filestorageServer "go-zero-micro/rpc/code/ucenter/internal/server/filestorage"

	"go-zero-micro/rpc/code/ucenter/internal/config"
	ucentergormServer "go-zero-micro/rpc/code/ucenter/internal/server/ucentergorm"
	ucentersqlxServer "go-zero-micro/rpc/code/ucenter/internal/server/ucentersqlx"
	"go-zero-micro/rpc/code/ucenter/internal/svc"
	"go-zero-micro/rpc/code/ucenter/ucenter"

	"github.com/zeromicro/go-zero/core/conf"
	"github.com/zeromicro/go-zero/core/service"
	"github.com/zeromicro/go-zero/zrpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

var configFile = flag.String("f", "conf/dev/rpc/ucenter.yaml", "the config file")

func main() {
	flag.Parse()

	var c config.Config
	conf.MustLoad(*configFile, &c)
	ctx := svc.NewServiceContext(c)

	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
		ucenter.RegisterUcenterSqlxServer(grpcServer, ucentersqlxServer.NewUcenterSqlxServer(ctx))
		ucenter.RegisterUcenterGormServer(grpcServer, ucentergormServer.NewUcenterGormServer(ctx))

		//新增的分组接口必须要在这里注册,根据proto生成时可能未新增,否则会报 unknown service ucenter.fileStorage
		ucenter.RegisterFileStorageServer(grpcServer, filestorageServer.NewFileStorageServer(ctx))

		if c.Mode == service.DevMode || c.Mode == service.TestMode {
			reflection.Register(grpcServer)
		}
	})

	MaxFileSize := int(c.UploadFile.MaxFileSize)
	//调整RPC服务端收到的消息体大小限制
	s.AddOptions(grpc.MaxRecvMsgSize(MaxFileSize))

	defer s.Stop()

	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
	s.Start()
}

2.7 grpc客户端接收响应体大小限制

本次示例代码

2.6 grpc服务端接收请求体大小限制类似,grpc的客户端在接收grpc服务端返回的数据时默认也是有数据大小限制的,限制4194304字节(约4MB),本次在2.6 grpc服务端接收请求体大小限制基础上进一步学习了解。

1. 文件下载功能

请求地址:http://127.0.0.1:8888/fileStorage/fileDownload?id=1&fileUrl=ee2a0940a3b64c81b2e8b125881523df.exe
请求方式:GET
请求格式:URL
请求数据:id=1&fileUrl=ee2a0940a3b64c81b2e8b125881523df.exe
返回结果:
go-zero学习 第三章 微服务_第2张图片

可以看到api接收rpc传输文件失败的原因是文件的大小167840824字节(约160 MB)超过了rpc端(grpc服务端)消息体接收限制4194304字节(约4MB)的大小。

2 调整grpc客户端接收响应体大小限制

internal/svc/servicecontext.go加上:

	MaxFileSize := int(c.UploadFile.MaxFileSize)
	//调整RPC客户端收到的消息体大小限制
	dialOption := grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(MaxFileSize))
	opt := zrpc.WithDialOption(dialOption)

完整代码:

package svc

import (
	"github.com/zeromicro/go-zero/rest"
	"github.com/zeromicro/go-zero/zrpc"
	"go-zero-micro/api/code/ucenterapi/internal/config"
	"go-zero-micro/api/code/ucenterapi/internal/middleware"
	"go-zero-micro/rpc/code/ucenter/client/filestorage"
	"go-zero-micro/rpc/code/ucenter/client/ucentergorm"
	"go-zero-micro/rpc/code/ucenter/client/ucentersqlx"
	"google.golang.org/grpc"
)

type ServiceContext struct {
	Config         config.Config
	Check          rest.Middleware
	UcenterGormRpc ucentergorm.UcenterGorm //gorm方式的接口
	UcenterSqlxRpc ucentersqlx.UcenterSqlx //sqlx方式的接口
	FileStorageRpc filestorage.FileStorage //文件存储相关接口
}

func NewServiceContext(c config.Config) *ServiceContext {
	MaxFileSize := int(c.UploadFile.MaxFileSize)
	//调整RPC客户端收到的消息体大小限制
	dialOption := grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(MaxFileSize))
	opt := zrpc.WithDialOption(dialOption)

	uCenterRpcClient := zrpc.MustNewClient(c.UCenterRpc, opt)

	return &ServiceContext{
		Config:         c,
		Check:          middleware.NewCheckMiddleware().Handle,
		UcenterGormRpc: ucentergorm.NewUcenterGorm(uCenterRpcClient),
		UcenterSqlxRpc: ucentersqlx.NewUcenterSqlx(uCenterRpcClient),
		FileStorageRpc: filestorage.NewFileStorage(uCenterRpcClient),
	}
}

2.8 API和RPC服务拦截器

本次示例代码

API、RPC服务拦截器又称RPC的客户端、服务端拦截器。

根据功能不同可以声明创建不同的拦截器,然后加入到拦截器链上,多个拦截器的拦截顺序是先进后出,即先触发的拦截器,最后才处理结束。

注意:这里指的拦截器指的是微服务之间调用时生效,即API服务是在发往RPC数据时拦截,RPC服务是在接收API服务时拦截。

1 API服务拦截器
API服务拦截器主要是在 internal/svc/servicecontext.go中声明和使用,查看zrpc.WithUnaryClientInterceptor()源码可以发现,底层还是调用的grpc的拦截器。

初次使用:

package svc

import (
	"context"
	"fmt"
	"github.com/zeromicro/go-zero/rest"
	"github.com/zeromicro/go-zero/zrpc"
	"go-zero-micro/api/code/ucenterapi/internal/config"
	"go-zero-micro/api/code/ucenterapi/internal/middleware"
	"go-zero-micro/rpc/code/ucenter/client/filestorage"
	"go-zero-micro/rpc/code/ucenter/client/ucentergorm"
	"go-zero-micro/rpc/code/ucenter/client/ucentersqlx"
	"google.golang.org/grpc"
)

type ServiceContext struct {
	Config         config.Config
	Check          rest.Middleware
	UcenterGormRpc ucentergorm.UcenterGorm //gorm方式的接口
	UcenterSqlxRpc ucentersqlx.UcenterSqlx //sqlx方式的接口
	FileStorageRpc filestorage.FileStorage //文件存储相关接口
}

func NewServiceContext(c config.Config) *ServiceContext {
	MaxFileSize := int(c.UploadFile.MaxFileSize)
	//调整RPC客户端收到的消息体大小限制
	dialOption := grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(MaxFileSize))
	opt := zrpc.WithDialOption(dialOption)

	//声明拦截器
	interceptor1 := zrpc.WithUnaryClientInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		fmt.Printf("interceptor1 ====> Start \n")
		fmt.Printf("req =====================> %+v \n", req)

		err := invoker(ctx, method, req, reply, cc, opts...)
		fmt.Printf("interceptor1 ====> End \n")
		if err != nil {
			return err
		}
		return nil
	})

	//声明拦截器
	interceptor2 := zrpc.WithUnaryClientInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		fmt.Printf("interceptor2 ====> Start \n")
		fmt.Printf("req =====================> %+v \n", req)

		err := invoker(ctx, method, req, reply, cc, opts...)
		fmt.Printf("interceptor2 ====> End \n")
		if err != nil {
			return err
		}
		return nil
	})

	uCenterRpcClient := zrpc.MustNewClient(c.UCenterRpc, opt, interceptor1, interceptor2)

	return &ServiceContext{
		Config:         c,
		Check:          middleware.NewCheckMiddleware().Handle,
		UcenterGormRpc: ucentergorm.NewUcenterGorm(uCenterRpcClient),
		UcenterSqlxRpc: ucentersqlx.NewUcenterSqlx(uCenterRpcClient),
		FileStorageRpc: filestorage.NewFileStorage(uCenterRpcClient),
	}
}

2 RPC服务拦截器

RPC服务的拦截器是在启动类里使用s.AddUnaryInterceptors(),里面的参数是具体的拦截方法,需要注意的是可以重复声明多个不同功能的拦截器,底层还是调用的grpc的拦截器。

初次使用:

package main

import (
	"context"
	"flag"
	"fmt"
	filestorageServer "go-zero-micro/rpc/code/ucenter/internal/server/filestorage"

	"go-zero-micro/rpc/code/ucenter/internal/config"
	ucentergormServer "go-zero-micro/rpc/code/ucenter/internal/server/ucentergorm"
	ucentersqlxServer "go-zero-micro/rpc/code/ucenter/internal/server/ucentersqlx"
	"go-zero-micro/rpc/code/ucenter/internal/svc"
	"go-zero-micro/rpc/code/ucenter/ucenter"

	"github.com/zeromicro/go-zero/core/conf"
	"github.com/zeromicro/go-zero/core/service"
	"github.com/zeromicro/go-zero/zrpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

var configFile = flag.String("f", "conf/dev/rpc/ucenter.yaml", "the config file")

func main() {
	flag.Parse()

	var c config.Config
	conf.MustLoad(*configFile, &c)
	ctx := svc.NewServiceContext(c)

	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
		ucenter.RegisterUcenterSqlxServer(grpcServer, ucentersqlxServer.NewUcenterSqlxServer(ctx))
		ucenter.RegisterUcenterGormServer(grpcServer, ucentergormServer.NewUcenterGormServer(ctx))

		//新增的分组接口必须要在这里注册,根据proto生成时可能未新增,否则会报 unknown service ucenter.fileStorage
		ucenter.RegisterFileStorageServer(grpcServer, filestorageServer.NewFileStorageServer(ctx))

		if c.Mode == service.DevMode || c.Mode == service.TestMode {
			reflection.Register(grpcServer)
		}
	})

	MaxFileSize := int(c.UploadFile.MaxFileSize)
	//调整RPC服务端收到的消息体大小限制
	s.AddOptions(grpc.MaxRecvMsgSize(MaxFileSize))

	//拦截器
	s.AddUnaryInterceptors(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		fmt.Printf("RpcServerInterceptor1 ====> Start \n")
		fmt.Printf("req =====================> %+v \n", req)
		fmt.Printf("info =====================> %+v \n", info)
		resp, err = handler(ctx, req)
		fmt.Printf("resp =====================> %+v \n", resp)
		fmt.Printf("RpcServerInterceptor1 ====> End \n")
		return resp, err
	})

	//拦截器
	s.AddUnaryInterceptors(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		fmt.Printf("RpcServerInterceptor2 ====> Start \n")
		fmt.Printf("req =====================> %+v \n", req)
		fmt.Printf("info =====================> %+v \n", info)
		resp, err = handler(ctx, req)
		fmt.Printf("resp =====================> %+v \n", resp)
		fmt.Printf("RpcServerInterceptor2 ====> End \n")
		return resp, err
	})

	defer s.Stop()

	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
	s.Start()
}

2.9 服务间通过metadata代替context传值

本次示例代码,在拦截器的基础上实现本功能。

  1. context是服务内使用的,如果想跨服务传递一些通用数据,要么在参数里加,要么通过metadata传递。
  2. 不同服务(不只是api服务往rpc服务、rpc服务之间也是)之间通过metadata代替context传值。

注意:metadata不支持中文!可以使用base64转码解决

API服务是在common/interceptor/apinterceptor.go,核心代码:

// RpcClientInterceptor2 rpc的客户端拦截器
func RpcClientInterceptor2(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	fmt.Printf("RpcClientInterceptor2 ====> Start \n")
	fmt.Printf("req =====================> %+v \n", req)

	mapData := map[string]string{}
	mapData["userId"] = utils.Base64Encode("111")
	mapData["userName"] = utils.Base64Encode("哈哈哈")
	md := metadata.New(mapData)
	ctx = metadata.NewOutgoingContext(ctx, md)

	err := invoker(ctx, method, req, reply, cc, opts...)
	fmt.Printf("RpcClientInterceptor2 ====> End \n")
	if err != nil {
		return err
	}
	return nil
}

RPC服务是在common/interceptor/rpcinterceptor.go,核心代码:

// RpcServerInterceptor2 rpc的服务端拦截器
func RpcServerInterceptor2(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
	fmt.Printf("RpcServerInterceptor2 ====> Start \n")
	fmt.Printf("req =====================> %+v \n", req)
	fmt.Printf("info =====================> %+v \n", info)

	if md, ok := metadata.FromIncomingContext(ctx); ok {
		tmp := md.Get("userId")
		if len(tmp) > 0 {
			userId, _ := utils.Base64Decode(tmp[0])
			uid, _ := strconv.ParseInt(userId, 10, 64)
			fmt.Printf("userId:%d\n", uid)
		}
		uname := md.Get("userName")
		if len(tmp) > 0 {
			userName, _ := utils.Base64Decode(uname[0])
			fmt.Printf("userName:%s\n", userName)
		}
	}

	resp, err = handler(ctx, req)
	fmt.Printf("resp =====================> %+v \n", resp)
	fmt.Printf("RpcServerInterceptor2 ====> End \n")
	return resp, err
}

2.10 RPC服务如何独立调试

参考:开启 gRPC 调试开关

有时候不想通过api服务调用rpc服务,只想单独调用rpc服务。可以在rpc的yaml配置文件里加入以下配置即可,然后可以通过postman发起grpc请求。

#开启grpc调试模式
Mode: dev

注意:建议在开发环境和测试环境开启,预生产环境和正式环境建议关闭,因此我们在静态配置文件中将环境模式配置为 dev 或者 test 时才会开启(默认为 dev 环境)。

你可能感兴趣的:(go-zero,golang)