说明:
①. 模拟一个商品service、一个订单service.
②. 客户端可以是gRpc这种测试程序,也可以是一些web框架来连接:
a. 如:gin,都可以直接来连服务端.
b. 也可以是直接用网关来做.
③. 有些系统是不支持GRPC的,也不想改造,应该提供api的方式给它调用.
1. 客户端代码:
1.1 引入其它的proto文件:
(1). 订单相关的proto文件:
grpcserver\pbfiles\Models.proto:
syntax="proto3";
package services;
import "google/protobuf/timestamp.proto";
import "validate.proto";
// 商品模型实体
message OrderModel {
int32 order_id = 1;
string order_no = 2;
float order_price = 3 [(validate.rules).float.gt = 1];
google.protobuf.Timestamp order_time = 4;
repeated OrderDetail order_details = 5; // 嵌套模型下面的子模型
}
// 子订单模型
message OrderDetail{
int32 detail_id=1;
string order_no=2;
int32 prod_id=3;
float prod_price=4;
int32 prod_num=5;
}
注:
①. GOPATH\pkg\mod\github.com\golang\protobuf@v1.3.2\ptypes\timestamp\timestamp.proto
②. 使用第三方库进行字段验证:
a. 可以提交到gin web框架中验证,验证完之后,直接调用grpc方法.
b. web框架可以在struct中写各种规则,但是model.pb.go是自动生成出来.
c. validate在生成部分就写上验证,写起来比较麻烦,但是好处在于可以生成不同语言的验证器.
d. github:https://github.com/envoyproxy/protoc-gen-validate
e. 安装:
go get -u github.com/envoyproxy/protoc-gen-validate
在GOPATH\bin\protoc-gen-validate.exe会多一个这样的exe文件,编译的时候会调用这个插件.
③. 拷贝下图validate.proto到grpc\pbfiles目录中.其实就是为了方便import "validate.proto".
(2). 订单相关proto文件:
syntax="proto3";
package services;
// 引入其它的proto文件
import "Models.proto";
// 请求时,是以orderModel的字段做为json参数来提交,需要把body部分进行映射成orderModel.
message OrderRequest {
OrderModel order_model = 1;
}
message OrderResponse {
int32 status = 1;
string message = 2;
}
service OrderService {
rpc NewOrder(OrderModel) returns (OrderResponse) {};
rpc AddOrder(OrderRequest) returns (OrderResponse) {
option (google.api.http) = {
post: "/v1/addorder"
// 这里的order_model要跟上面的order_model是绑定的,因为类型是OrderModel实体,提交参数的时候,就可以通过OrderModel这个实体,编译的实体获取得到.
body: "order_model"
};
};
}
注:
①. 全部写在一个proto文件,不好维护:
a. 商品的详情、商品的订单信息不能放到一个proto文件.
b. 拆分专门放实体proto文件,如商品订单、商品分类、商品优惠券...
(3). 商品相关proto文件:
grpcserver\pbfiles\Prod.proto:
syntax = "proto3";
package services;
// 使用枚举获取分区商品库存
enum ProdAreas {
A=0;
B=1;
C=2;
}
// 实现的业务:传入一个商品ID,获取一个商品库存.
message ProdRequest {
int32 prod_id = 1;
}
message ProdResponse {
int32 prod_stock = 1;
}
// 实现的业务:获取多少条商品库存.
message QuerySize {
int32 size = 1; // 每页多少条
}
message ProdListResponse {
// 这里同样不能写ProdResponse[]
// ProdResponse有点类似类名,prodres有点类似变量名.
repeated ProdResponse prodres = 1;
}
// 创建一个service
service ProdService {
// 获取一条商品库存的方法
rpc GetProdStock (ProdRequest) returns (ProdResponse) {
rpc GetProdStock (ProdRequest) returns (ProdResponse) {
option (google.api.http) = {
get: "/v1/prod/{prod_id}"
};
};
// 获取多条商品的库存
rpc GetProdStocks (QuerySize) returns (ProdListResponse) {}
}
注:
①. grpc是跨平台的,不能在Contracts方法返回值中写[]ProdResponse.
a. 这是go的语法,不能写在proto文件里面.
②. Repeated语法修饰符:
a. 返回字段可以重复任意多次(包括0次).
b. 可以认为就是一个PHP中的数组或go的切片.
③. ProdListResponse就是一个切片,里面包含了ProdResponse
type ProdListResponse struct {
Prodres []*ProdResponse `protobuf:"bytes,1,rep...`
...
}
④. 商品一般库存有区域之分:
a. 如A区有10个、B区有12个、C区有20个.
b. 加入枚举类型,可以让用户选择到底是A区域、还是B区域.
(4). 增加中间文件生成go文件批处理:
grpcserver\gen.bat:
cd pbfiles && protoc --go_out=plugins=grpc:../services Prod.proto
protoc --go_out=plugins=grpc:../services --validate_out=lang=go:../services Models.proto
protoc --go_out=plugins=grpc:../services Order.proto
protoc --grpc-gateway_out=logtostderr=true:../services Prod.proto
protoc --grpc-gateway_out=logtostderr=true:../services Order.proto
cd ../
注:
①. 生成的验证文件grpc\services\Models.pb.validate.go.
1.2 接口实现类:
(1). 商品接口实现类:
grpcserver\services\ProdService.go:
package services
// 具体的实现类
import (
"context"
)
type ProdService struct {
}
// 获取一个商品的一个库存方法
func(this *ProdService) GetProdStock(ctx context.Context, request *ProdRequest) (*ProdResponse, error) {
var stock int32 = 100
fmt.Println(request.ProdArea) // A
fmt.Println(request.GetProdArea()) // A,打印的值是一样的
if (request.ProdArea == ProdAreas_A) {
stock = 10
}
return &ProdResponse{ProdStock:stock}, nil
}
// 获取多少条商品库存方法
func(s *ProdService) GetProdStocks(ctx context.Context, request *QuerySize) (*ProdListResponse, error) {
stock := []*ProdResponse{
&ProdResponse{ProdStock: 20},
&ProdResponse{ProdStock: 22},
}
return &ProdListResponse{Prodres: stock}, nil
}
注:
①. 没有实现接口的话,server.go中的以下代码会报错:
services.RegisterProdServiceServer(rpcServer, new(services.ProdService))
(2). 订单接口实现类:
grpcserver\services\OrderService.go:
package services
import (
"context"
"fmt"
)
type OrderService struct {
}
func(this *OrderService) NewOrder(cxt context.Context, request *OrderModel) (*OrderResponse, error) {
fmt.Println(request)
// order_id:1 order_no:"DD123456" order_price:12.3 order_time:<seconds:1576819779 >
return &OrderResponse{Status: 200, Message: "success"}, nil
}
func(this *OrderService) AddOrder(cxt context.Context, request *OrderRequest) (*OrderResponse, error) {
fmt.Println(request)
// OrderPrice必须大于1,否则报错:invalid OrderModel.OrderPrice: value must be greater than 1
err := request.OrderModel.Validate()
if err != nil {
return &OrderResponse{Status: 400, Message: err.Error()},nil
}
return &OrderResponse{Status: 200, Message: "success"},nil
}
1.3 服务server.go代码:
(1). 封装公共证书函数:
grpcserver\helper\CertHelper.go:
package helper
import (
"crypto/tls"
"crypto/x509"
"google.golang.org/grpc/credentials"
"io/ioutil"
)
// 获取服务端证书配置
func GetServerCreds() credentials.TransportCredentials {
cert, _ := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
certPool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.VerifyClientCertIfGiven,
ClientCAs: certPool,
})
return creds
}
// 获取客户端证书配置
func GetClientCreds() credentials.TransportCredentials {
cert, _ := tls.LoadX509KeyPair("cert/client.pem", "cert/client.key")
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
certPool.AppendCertsFromPEM(ca)
creds:=credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: "localhost",
RootCAs: certPool,
})
return creds
}
(2). 服务server.go代码:
grpcserver\service.go:
package main
import (
"fmt"
"google.golang.org/grpc"
"grpc.com/helper"
"grpc.com/services"
"net"
)
func main() {
//创建gRPC服务
rpcServer:=grpc.NewServer(grpc.Creds(helper.GetServerCreds()))
// 商品服务注册
services.RegisterProdServiceServer(rpcServer, new(services.ProdService))
// 订单服务注册
services.RegisterOrderServiceServer(rpcServer, new(services.OrderService))
lis, _ := net.Listen("tcp",":8081")
err = rpcServer.Serve(lis)
}
1.4 gateway代码:
package main
import (
"context"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
"grpc.com/helper"
"grpc.com/services"
"log"
"net/http"
)
func main () {
gwmux := runtime.NewServeMux()
opt := []grpc.DialOption{grpc.WithTransportCredentials(helper.GetClientCreds())}
grpcEndpoint := "localhost:8081"
err := services.RegisterProdServiceHandlerFromEndpoint(context.Background(), gwmux, grpcEndpoint, opt)
err1 := services.RegisterOrderServiceHandlerFromEndpoint(context.Background(), gwmux, grpcEndpoint, opt)
if err != nil || err1 != nil {
log.Fatal(err)
}
httpServer := &http.Server{
Addr: ":8080",
Handler:gwmux,
}
httpServer.ListenAndServe()
}
2. 客户端代码:
(1). 复制中间文件生成好的go文件到客户端:
grpcserver\services\Prod.pb.go
grpcserver\services\Order.pb.go
grpcserver\services\Models.pb.go
(2). 客户端调用gRpc代码:
package main
import (
"client.com/services"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
// 这个包来帮助创建类型,不是go内置的类型,是需要第三方包
"github.com/golang/protobuf/ptypes/timestamp"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"log"
)
func main() {
cert, _ := tls.LoadX509KeyPair("cert/client.pem","cert/client.key")
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
certPool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: "localhost",
RootCAs: certPool,
})
client,err := grpc.Dial(":8081", grpc.WithTransportCredentials(creds))
if err != nil{
log.Fatal(err)
}
defer client.Close()
// 创建prod服务的客户端
prodClient := services.NewProdServiceClient(client)
// 接口一:获取一个商品一个库存
res, _ := prodClient.GetProdStock(
context.Background(),
// 如果不写第二个参数,默认就是0,返回的值是10
&services.ProdRequest{ProdId:12, ProdArea:services.ProdAreas_B},
)
fmt.Println(res) // 100
// 接口二:获取多个商品的库存
res, _ := prodClient.GetProdStocks(
context.Background(),
&services.QuerySize{Size: 10},
)
fmt.Println(res.Prodres) // [prod_stock:20 prod_stock:22 ]
fmt.Println(res.Prodres[0].ProdStock) // 20
// 创建order服务的客户端
orderClient := services.NewOrderServiceClient(client)
// 调用gRPC接口
t := timestamp.Timestamp{Seconds: time.Now().Unix()}
res, _ := orderClient.NewOrder(
context.Background(),
&services.OrderModel{OrderId:1, OrderNo: "DD123456", OrderPrice: 12.30, OrderTime: &t,},
)
fmt.Println(res) // status:200 message:"success"
}
3. gateway http api请求:
POST:http://localhost:8080/v1/addorder
{
"order_no":"20190809",
"order_id":"1152",
"order_details":[
{"order_no":"20190809","prod_id":"101","prod_price":"20.0","prod_num":"3"}
]
}