gRPC的简单应用
gRPC是由开发的一个高性能、通用的开源RPC框架,主要面向移动应用开发且基于HTTP/2协议标准而设计,同时支持大多数流行的编程语言。
官网:https://grpc.io/
安装protoc 工具
https://protobuf.dev/
安装Go插件
旧版本直接安装protoc-gen-go即可
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
新版本须同时安装protoc-gen-go,protoc-gen-go-grpc
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
以下的过程使用旧版本演示,新版本在新版Go插件章节演示
grpc sample
1.创建pb/product.proto文件
product.proto
syntax = "proto3";
option go_package="../service";
package service;
message ProductRequest {
int32 prod_id = 1;
}
message ProductResponse {
int32 prod_stock = 1;
}
// 定义接口
service ProductService {
rpc GetProductStock(ProductRequest) returns(ProductResponse);
}
2.cd 到pb目录执行
protoc --go_out=plugins=grpc:./ product.proto
3.生成了 /service/product.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.21.12
// source: product.proto
package service
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ProductRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ProdId int32 `protobuf:"varint,1,opt,name=prod_id,json=prodId,proto3" json:"prod_id,omitempty"`
}
func (x *ProductRequest) Reset() {
*x = ProductRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_product_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ProductRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProductRequest) ProtoMessage() {}
func (x *ProductRequest) ProtoReflect() protoreflect.Message {
mi := &file_product_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProductRequest.ProtoReflect.Descriptor instead.
func (*ProductRequest) Descriptor() ([]byte, []int) {
return file_product_proto_rawDescGZIP(), []int{0}
}
func (x *ProductRequest) GetProdId() int32 {
if x != nil {
return x.ProdId
}
return 0
}
type ProductResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ProdStock int32 `protobuf:"varint,1,opt,name=prod_stock,json=prodStock,proto3" json:"prod_stock,omitempty"`
}
func (x *ProductResponse) Reset() {
*x = ProductResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_product_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ProductResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ProductResponse) ProtoMessage() {}
func (x *ProductResponse) ProtoReflect() protoreflect.Message {
mi := &file_product_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ProductResponse.ProtoReflect.Descriptor instead.
func (*ProductResponse) Descriptor() ([]byte, []int) {
return file_product_proto_rawDescGZIP(), []int{1}
}
func (x *ProductResponse) GetProdStock() int32 {
if x != nil {
return x.ProdStock
}
return 0
}
var File_product_proto protoreflect.FileDescriptor
var file_product_proto_rawDesc = []byte{
0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x29, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x64,
0x75, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x72,
0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x70, 0x72, 0x6f,
0x64, 0x49, 0x64, 0x22, 0x30, 0x0a, 0x0f, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x5f, 0x73,
0x74, 0x6f, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x64,
0x53, 0x74, 0x6f, 0x63, 0x6b, 0x32, 0x56, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x50, 0x72,
0x6f, 0x64, 0x75, 0x63, 0x74, 0x53, 0x74, 0x6f, 0x63, 0x6b, 0x12, 0x17, 0x2e, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x50, 0x72,
0x6f, 0x64, 0x75, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0c, 0x5a,
0x0a, 0x2e, 0x2e, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (
file_product_proto_rawDescOnce sync.Once
file_product_proto_rawDescData = file_product_proto_rawDesc
)
func file_product_proto_rawDescGZIP() []byte {
file_product_proto_rawDescOnce.Do(func() {
file_product_proto_rawDescData = protoimpl.X.CompressGZIP(file_product_proto_rawDescData)
})
return file_product_proto_rawDescData
}
var file_product_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_product_proto_goTypes = []interface{}{
(*ProductRequest)(nil), // 0: service.ProductRequest
(*ProductResponse)(nil), // 1: service.ProductResponse
}
var file_product_proto_depIdxs = []int32{
0, // 0: service.ProductService.GetProductStock:input_type -> service.ProductRequest
1, // 1: service.ProductService.GetProductStock:output_type -> service.ProductResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_product_proto_init() }
func file_product_proto_init() {
if File_product_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_product_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ProductRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_product_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ProductResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_product_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_product_proto_goTypes,
DependencyIndexes: file_product_proto_depIdxs,
MessageInfos: file_product_proto_msgTypes,
}.Build()
File_product_proto = out.File
file_product_proto_rawDesc = nil
file_product_proto_goTypes = nil
file_product_proto_depIdxs = nil
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6
// ProductServiceClient is the client API for ProductService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type ProductServiceClient interface {
GetProductStock(ctx context.Context, in *ProductRequest, opts ...grpc.CallOption) (*ProductResponse, error)
}
type productServiceClient struct {
cc grpc.ClientConnInterface
}
func NewProductServiceClient(cc grpc.ClientConnInterface) ProductServiceClient {
return &productServiceClient{cc}
}
func (c *productServiceClient) GetProductStock(ctx context.Context, in *ProductRequest, opts ...grpc.CallOption) (*ProductResponse, error) {
out := new(ProductResponse)
err := c.cc.Invoke(ctx, "/service.ProductService/GetProductStock", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ProductServiceServer is the server API for ProductService service.
type ProductServiceServer interface {
GetProductStock(context.Context, *ProductRequest) (*ProductResponse, error)
}
// UnimplementedProductServiceServer can be embedded to have forward compatible implementations.
type UnimplementedProductServiceServer struct {
}
func (*UnimplementedProductServiceServer) GetProductStock(context.Context, *ProductRequest) (*ProductResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetProductStock not implemented")
}
func RegisterProductServiceServer(s *grpc.Server, srv ProductServiceServer) {
s.RegisterService(&_ProductService_serviceDesc, srv)
}
func _ProductService_GetProductStock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ProductRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ProductServiceServer).GetProductStock(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/service.ProductService/GetProductStock",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ProductServiceServer).GetProductStock(ctx, req.(*ProductRequest))
}
return interceptor(ctx, in, info, handler)
}
var _ProductService_serviceDesc = grpc.ServiceDesc{
ServiceName: "service.ProductService",
HandlerType: (*ProductServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetProductStock",
Handler: _ProductService_GetProductStock_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "product.proto",
}
4.创建/service/product.go实现Productserveice
package service
import "context"
var ProductService = &productService{}
type productService struct {
}
// GetProductStock 接口实现
func (p *productService) GetProductStock(context context.Context, request *ProductRequest) (*ProductResponse, error) {
stock := p.GetStockById(request.ProdId)
return &ProductResponse{ProdStock: stock}, nil
}
func (p *productService) GetStockById(id int32) int32 {
return 100
}
5.创建服务端代码
main.go
package main
import (
"fmt"
"google.golang.org/grpc"
"log"
"net"
"test_grpc/service"
)
func main() {
// 创建rpc实例
rpcServer := grpc.NewServer()
// 服务注册
service.RegisterProductServiceServer(rpcServer, service.ProductService)
// 启动监听
listener, err := net.Listen("tcp", ":8000")
if err != nil {
log.Fatal("启动监听失败", err)
}
// 启动服务
err = rpcServer.Serve(listener)
if err != nil {
log.Fatal("启动服务失败", err)
}
}
6.将/service目录复制到client目录下
client引入service包中的方法,进行使用
7.创建客户端代码 /client/main.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"test_grpc/client/service"
)
func main() {
// 创建连接
conn, err := grpc.Dial(":8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal("服务端连接失败: ", err)
}
// 退出时关闭连接
defer conn.Close()
// 创建客户端实例
productServiceClient := service.NewProductServiceClient(conn)
// 方法请求
res, err := productServiceClient.GetProductStock(context.Background(), &service.ProductRequest{ProdId: 520})
if err != nil {
log.Fatal("调用gRPC方法失败: ", err)
}
fmt.Println("调用gRPC方法成功, ProdStock = ", res.ProdStock)
}
8.分别运行服务端与客户端
客户端返回:
调用gRPC方法成功, ProdStock = 100
9.目录结构
加密通信与认证方式
HTTP是明文传输的,即客户端与服务端之间通信的信息是可见的,这就存在被窃听、冒充或篡改的风险。HTTPS在HTTP和TCP之间加入了TLS协议。
[HTTP] [HTTP]
[SSL/TLS]
[TCP] [TCP]
[IP] [IP]
[MAC] [MAC]
TLS协议主要解决了以下三个网络安全问题:
1.信息加密: HTTP 交互信息是被加密的,第三方就无法被窃取;
2.校验机制:校验信息传输过程中是否有被第三方篡改过,如果被篡改过,则会有警告提示;
3.身份证书:双方认证,双方都可以配置证书,防止身份被冒充;
客户端与服务端通过gRPC进行方法调用,也需要加入证书来保证调用的安全。
0.安装openssl工具
安装openssl:http://slproweb.com/products/Win32OpenSSL.html
并将openssl加入环境变量
创建cert目录,以下所有操作均在该目录中进行
1.生成私钥文件
openssl genrsa -des3 -out ca.key 2048
2.创建证书请求
openssl req -new -key ca.key -out ca.csr
3.生成ca.crt
openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt
4.修改openssl.cnf
在openssl安装目录中查找openssl.cnf,复制到当前目录下
# 打开copy_extensions
copy_extensions = copy
# 打开req_extensions
req_extensions = v3_req
# 找到[ v3_req ], 添加
subjectAltName = @alt_names
# 添加标签
[ alt_names ]
DNS.1 = *.test.com
5.生成证书私钥
openssl genpkey -algorithm RSA -out server.key
6.通过私钥生成证书请求文件
openssl req -new -nodes -key server.key -out server.csr -days 3650 -config ./openssl.cnf -extensions v3_req
7.生成SAN证书
openssl x509 -req -days 365 -in server.csr -out server.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。
说明:
key:服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接收到数据的解密;
csr:证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名;
crt:由颁发证书机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息;
pem:是基于Base64编码的证书格式,扩展名包括PEM、CRT和CER;
单向认证
流程:
1.发送客户端SSL版本等信息到服务端
2.服务端给客户端返回SSL版本,随机数等信息,以及服务器公钥
3.客户端校验服务端证书是否合法,合法继续,否则警告
4.客户端发送自己可支持的对称加密方案给服务端,供其选择
5.服务端选择加密程度高的加密方式
6.服务端将选择好的加密方式以明文方式发送给客户端
7.客户端收到加密方式后,产生随机码,作为对称加密密钥,使用服务端公钥进行加密后,发送给服务端
8.服务端使用私钥对加密信息进行解密,获得对称加密的密钥
9.双端通信,对称加密确保了通信安全
1.修改服务端代码
package main
import (
"test_grpc/service"
"google.golang.org/grpc"
"net"
"log"
"fmt"
"google.golang.org/grpc/credentials"
)
func main() {
fmt.Println("开始启动服务")
// 添加证书
creds, err := credentials.NewServerTLSFromFile("cert/server.pem", "cert/server.key")
if err != nil {
log.Fatal("证书获取失败", err)
}
// 创建rpc实例(添加认证)
rpcServer := grpc.NewServer(grpc.Creds(creds))
// 服务注册
service.RegisterProductServiceServer(rpcServer, service.ProductService)
// 启动监听
listener, err := net.Listen("tcp", ":8888")
if err != nil {
log.Fatal("启动监听失败", err)
}
// 启动服务
err = rpcServer.Serve(listener)
if err != nil {
log.Fatal("启动服务失败", err)
}
fmt.Println("启动服务成功")
}
2.修改客户端代码
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"log"
"test_grpc/client/service"
)
func main() {
// 添加公钥
creds, err := credentials.NewClientTLSFromFile("../cert/server.pem", "*.test.com")
if err != nil {
log.Fatal("证书错误: ", err)
}
// 创建连接
conn, err := grpc.Dial(":8888", grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatal("服务端连接失败: ", err)
}
fmt.Println("证书认证通过")
// 退出时关闭连接
defer conn.Close()
// 创建客户端实例
productServiceClient := service.NewProductServiceClient(conn)
// 方法请求
resq, err := productServiceClient.GetProductStock(context.Background(), &service.ProductRequest{ProdId: 233})
if err != nil {
log.Fatal("调用gRPC方法失败: ", err)
}
fmt.Println("调用gRPC方法成功, ProdStock = ", resq.ProdStock)
}
3.使用命令行启动服务端与客户端
客户端执行结果为:
证书认证通过
调用gRPC方法成功, ProdStock = 100
注意:credentials.NewClientTLSFromFile("../cert/server.pem", "*.test.com")
,*.test.com应与证书中的域名相对应。
4.目录结构
双向认证
流程:
1.发送客户端SSL版本等信息到服务端
2.服务端给客户端返回SSL版本,随机数等信息,以及服务器公钥
3.客户端校验服务端证书是否合法,合法继续,否则警告
4.客户端校验通过后,将自己的证书及公钥发送至服务端
5.服务端对客户端证书进行校验,校验结束后获得客户端公钥
6.客户端发送自己可支持的对称加密方案给服务端,供其选择
7.服务端选择加密程度高的加密方式
8.服务端将选择好的加密方式使用客户端公钥进行加密后发送给客户端
9.客户端收到加密方式后,产生随机码,作为对称加密密钥,使用服务端公钥进行加密后,发送给服务端
10.服务端使用私钥对加密信息进行解密,获得对称加密的密钥
11.双端通信,对称加密确保了通信安全
1.生成客户端公钥和私钥
1.1生成私钥
openssl genpkey -algorithm RSA -out client.key
1.2生成证书
openssl req -new -nodes -key client.key -out client.csr -days 3650 -config ./openssl.cnf -extensions v3_req
1.3生成SAN证书
openssl x509 -req -days 365 -in client.csr -out client.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
2.修改服务端代码
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"log"
"net"
"test_grpc/service"
)
func main() {
fmt.Println("开始启动服务")
// 添加证书
// creds, err0 := credentials.NewServerTLSFromFile("cert/server.pem", "cert/server.key")
// if err0 != nil {
// log.Fatal("证书生成失败", err0)
// }
// 证书认证-双向认证
cert, err0 := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
if err0 != nil {
log.Fatal("证书读取失败", err0)
}
fmt.Println("证书读取成功")
// 创建一个新的、空的CertPool
certPool := x509.NewCertPool()
ca, err1 := ioutil.ReadFile("cert/ca.crt")
if err1 != nil {
log.Fatal("ca证书读取失败", err1)
}
fmt.Println("ca证书读取成功")
// 尝试解析所传入的PEM编码的证书,如果解析成功会将其加到CertPool中,便于后面使用
certPool.AppendCertsFromPEM(ca)
// 构建基于TLS的TransportCredentials选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链, 允许包含一个或多个
Certificates: []tls.Certificate{cert},
// 要求必须校验客户端的证书
ClientAuth: tls.RequireAndVerifyClientCert,
// 设置根证书的集合, 校验方式使用ClientAuth中设定的模式
ClientCAs: certPool,
})
fmt.Println("设置TLS的TransportCredentials选项成功")
// 创建rpc实例
rpcServer := grpc.NewServer(grpc.Creds(creds))
// 服务注册
service.RegisterProductServiceServer(rpcServer, service.ProductService)
// 启动监听
listener, err := net.Listen("tcp", ":8888")
if err != nil {
log.Fatal("启动监听失败", err)
}
// 启动服务
err = rpcServer.Serve(listener)
if err != nil {
log.Fatal("启动服务失败", err)
}
}
3.修改客户端代码
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"log"
"test_grpc/client/service"
)
func main() {
// 添加公钥
// creds, err0 := credentials.NewClientTLSFromFile("../cert/server.pem", "*.test.com")
// if err0 != nil {
// log.Fatal("证书错误: ", err0)
// }
// 证书认证-双向认证
// 从证书相关文件中读取解析信息, 得到证书公钥、密钥对
cert, err0 := tls.LoadX509KeyPair("../cert/client.pem", "../cert/client.key")
if err0 != nil {
log.Fatal("证书读取失败", err0)
}
fmt.Println("证书读取成功")
// 创建一个新的、空的CertPool
certPool := x509.NewCertPool()
ca, err1 := ioutil.ReadFile("../cert/ca.crt")
if err1 != nil {
log.Fatal("ca证书读取失败", err1)
}
fmt.Println("ca证书读取成功")
// 尝试解析所传入的PEM编码的证书,如果解析成功会将其加到CertPool中,便于后面使用
certPool.AppendCertsFromPEM(ca)
// 构建基于TLS的TransportCredentials选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链, 允许包含一个或多个
Certificates: []tls.Certificate{cert},
ServerName: "*.test.com",
RootCAs: certPool,
})
fmt.Println("设置TLS的TransportCredentials选项成功")
// 创建连接
conn, err := grpc.Dial(":8888", grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatal("服务端连接失败: ", err)
}
fmt.Println("证书认证通过")
// 退出时关闭连接
defer conn.Close()
// 创建客户端实例
productServiceClient := service.NewProductServiceClient(conn)
// 方法请求
resq, err := productServiceClient.GetProductStock(context.Background(), &service.ProductRequest{ProdId: 233})
if err != nil {
log.Fatal("调用gRPC方法失败: ", err)
}
fmt.Println("调用gRPC方法成功, ProdStock = ", resq.ProdStock)
}
4.使用命令行启动服务端与客户端
客户端代码运行结果为:
证书读取成功
ca证书读取成功
设置TLS的TransportCredentials选项成功
证书认证通过
调用gRPC方法成功, ProdStock = 100
5.目录结构
Token认证
服务端添加用户名密码的校验
或使用jwt或oauth2
1.修改服务端代码
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"io/ioutil"
"log"
"net"
"test_grpc/service"
)
func main() {
fmt.Println("开始启动服务")
// 添加证书
// creds, err0 := credentials.NewServerTLSFromFile("cert/server.pem", "cert/server.key")
// if err0 != nil {
// log.Fatal("证书生成失败", err0)
// }
// 证书认证-双向认证
cert, err0 := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
if err0 != nil {
log.Fatal("证书读取失败", err0)
}
fmt.Println("证书读取成功")
// 创建一个新的、空的CertPool
certPool := x509.NewCertPool()
ca, err1 := ioutil.ReadFile("cert/ca.crt")
if err1 != nil {
log.Fatal("ca证书读取失败", err1)
}
fmt.Println("ca证书读取成功")
// 尝试解析所传入的PEM编码的证书,如果解析成功会将其加到CertPool中,便于后面使用
certPool.AppendCertsFromPEM(ca)
// 构建基于TLS的TransportCredentials选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链, 允许包含一个或多个
Certificates: []tls.Certificate{cert},
// 要求必须校验客户端的证书
ClientAuth: tls.RequireAndVerifyClientCert,
// 设置根证书的集合, 校验方式使用ClientAuth中设定的模式
ClientCAs: certPool,
})
fmt.Println("设置TLS的TransportCredentials选项成功")
// token认证 -- 合法的用户名和密码
var authInterceptor grpc.UnaryServerInterceptor
authInterceptor = func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
// 拦截请求, 验证token
err = Auth(ctx)
if err != nil {
return
}
// 继续处理请求
return handler(ctx, req)
}
// 创建rpc实例
rpcServer := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(authInterceptor))
// 服务注册
service.RegisterProductServiceServer(rpcServer, service.ProductService)
// 启动监听
listener, err := net.Listen("tcp", ":8888")
if err != nil {
log.Fatal("启动监听失败", err)
}
// 启动服务
err = rpcServer.Serve(listener)
if err != nil {
log.Fatal("启动服务失败", err)
}
}
func Auth(ctx context.Context) error {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return fmt.Errorf("missing credentials")
}
var user string
var passwd string
if val, ok := md["user"]; ok {
user = val[0]
}
if val, ok := md["passwd"]; ok {
passwd = val[0]
}
if user != "admin" || passwd != "admin@123" {
return status.Errorf(codes.Unauthenticated, "token认证失败")
}
fmt.Println("token认证成功")
return nil
}
2.在客户端创建/auth目录 auth.go 文件,实现grpc.PerRPCCredentials接口
package auth
import "context"
type Authentication struct {
User string
Passwd string
}
func (a *Authentication) GetRequestMetadata(context.Context, ...string) (
map[string]string, error,
) {
return map[string]string{"user": a.User, "passwd": a.Passwd}, nil
}
func (a *Authentication) RequireTransportSecurity() bool {
return false
}
3.修改客户端代码
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"log"
"test_grpc/client/auth"
"test_grpc/client/service"
)
func main() {
// 添加公钥
// creds, err0 := credentials.NewClientTLSFromFile("../cert/server.pem", "*.test.com")
// if err0 != nil {
// log.Fatal("证书错误: ", err0)
// }
// 证书认证-双向认证
// 从证书相关文件中读取解析信息, 得到证书公钥、密钥对
cert, err0 := tls.LoadX509KeyPair("../cert/client.pem", "../cert/client.key")
if err0 != nil {
log.Fatal("证书读取失败", err0)
}
fmt.Println("证书读取成功")
// 创建一个新的、空的CertPool
certPool := x509.NewCertPool()
ca, err1 := ioutil.ReadFile("../cert/ca.crt")
if err1 != nil {
log.Fatal("ca证书读取失败", err1)
}
fmt.Println("ca证书读取成功")
// 尝试解析所传入的PEM编码的证书,如果解析成功会将其加到CertPool中,便于后面使用
certPool.AppendCertsFromPEM(ca)
// 构建基于TLS的TransportCredentials选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链, 允许包含一个或多个
Certificates: []tls.Certificate{cert},
ServerName: "*.test.com",
RootCAs: certPool,
})
fmt.Println("设置TLS的TransportCredentials选项成功")
token := &auth.Authentication{
User: "admin",
Passwd: "admin@123",
}
// 创建连接
conn, err := grpc.Dial(":8888", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(token))
if err != nil {
log.Fatal("服务端连接失败: ", err)
}
fmt.Println("证书认证通过")
// 退出时关闭连接
defer conn.Close()
// 创建客户端实例
productServiceClient := service.NewProductServiceClient(conn)
// 方法请求
resq, err := productServiceClient.GetProductStock(context.Background(), &service.ProductRequest{ProdId: 233})
if err != nil {
log.Fatal("调用gRPC方法失败: ", err)
}
fmt.Println("调用gRPC方法成功, ProdStock = ", resq.ProdStock)
}
4.使用命令行启动服务端与客户端
客户端执行结果:
证书读取成功
ca证书读取成功
设置TLS的TransportCredentials选项成功
证书认证通过
调用gRPC方法成功, ProdStock = 100
5.目录结构
新版Go插件
1.官方提醒新版使用以下方式生成
直接在模块根目录执行即可
protoc --go_out=./service --go-grpc_out=./service pb\product.proto
2.生成后目录结构发生改变
3.直接运行代码会报错
.\main.go:81:50: cannot use service.ProductService (variable of type *service.productService) as service.ProductServiceServer value in argument to service.RegisterProductServiceServer: *service.productService does not implement service.ProductServiceServer (missing method mustEmbedUnimplementedProductServiceServer)
4.解决办法
在product.go文件中
使 productService
实现mustEmbedUnimplementedProductServiceServer
方法
package service
import (
"context"
)
var ProductService = &productService{}
type productService struct {
}
// GetProductStock 接口实现
func (p *productService) GetProductStock(context context.Context, request *ProductRequest) (*ProductResponse, error) {
stock := p.GetStockById(request.ProdId)
return &ProductResponse{ProdStock: stock}, nil
}
func (p *productService) GetStockById(id int32) int32 {
return 100
}
func (*productService) mustEmbedUnimplementedProductServiceServer() {}
UnimplementedProductServiceServer must be embedded to have forward compatible implementations.
//type UnimplementedProductServiceServer struct {
//}
//
//func (UnimplementedProductServiceServer) GetProductStock(context.Context, *ProductRequest) (*ProductResponse, error) {
// return nil, status.Errorf(codes.Unimplemented, "method GetProductStock not implemented")
//}
//func (UnimplementedProductServiceServer) mustEmbedUnimplementedProductServiceServer() {}
5.同样将生成的目录复制到客户端
6.使用命令行启动服务端与客户端
客户端执行结果:
证书读取成功
ca证书读取成功
设置TLS的TransportCredentials选项成功
证书认证通过
调用gRPC方法成功, ProdStock = 100
7.最终目录结构
修改mod文件,规范化模块名为:module test.com/test_grpc
go.mod文件
module test.com/test_grpc
go 1.20
require (
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
)
require (
github.com/golang/protobuf v1.5.2 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
)
自动更新引用即可
更新proto文件
user.proto
syntax = "proto3";
option go_package="../service";
package service;
message User {
string name = 1;
int32 age = 2;
optional string password = 3; // optional代表指针
optional string address = 4;
}
product.proto
syntax = "proto3";
// 从执行protoc这个命令的当前目录开始引入,如果在user.proto文件同级目录,则`import "user.proto";`即可
import "pb/user.proto";
option go_package="../service";
package service; // 将要生成的go文件包名
message ProductRequest {
int32 prod_id = 1; // 1代表顺序
}
message ProductResponse {
int32 prod_stock = 1; // 1代表顺序
User user = 2; // 导入其他pb文件
}
service ProductService {
rpc GetProductStock(ProductRequest) returns (ProductResponse);
}
重新生成Go代码:protoc --go_out=./service --go-grpc_out=./service pb\*.proto
*.proto
代表该目录下的所有proto文件
google.protobuf.Any与anypb.New()的使用
message Content { // 定义新结构
string msg = 1;
}
message ProductResponse {
int32 prod_stock = 1; // 1代表顺序
User user = 2;
google.protobuf.Any data = 3; // Any 类型
}
// GetProductStock 接口实现
func (p *productService) GetProductStock(context context.Context, request *ProductRequest) (*ProductResponse, error) {
stock := p.GetStockById(request.ProdId)
addr := "beijing"
user := &User{Address: &addr}
a, _ := anypb.New(&Content{Msg: "hello"}) // 转换成Any类型
return &ProductResponse{ProdStock: stock, User: user,Data: a}, nil
}
Stream
使用stream关键字修饰来表示流程传输。当该关键字修饰参数时,表示为客户端流式的gRPC接口;
普通RPC
rpc SimplePing(PingRequest) returns (PingReply);
客户端流式RPC
rpc SimplePing(stream PingRequest) returns (PingReply);
1.修改product.proto文件,重新生成Go文件,复制到客户端
syntax = "proto3";
// 从执行protoc这个命令的当前目录开始引入
import "pbfile/user.proto";
import "google/protobuf/any.proto"; //引入Any类型
option go_package="../service";
package service; // 将要生成的go文件包名
message ProductRequest {
int32 prod_id = 1; // 1代表顺序
}
message Content {
string msg = 1;
}
message ProductResponse {
int32 prod_stock = 1; // 1代表顺序
User user = 2;
google.protobuf.Any data = 3;
}
// 导入其他pb文件
service ProductService {
rpc GetProductStock(ProductRequest) returns (ProductResponse);
// 增加客户端流定义方法
rpc UpdateProductStockClientStream(stream ProductRequest) returns(ProductResponse);
}
2.修改服务端product.go文件
package service
import (
"context"
"fmt"
"google.golang.org/protobuf/types/known/anypb"
"io"
)
var ProductService = &productService{}
type productService struct {
}
// GetProductStock 接口实现
func (p *productService) GetProductStock(context context.Context, request *ProductRequest) (*ProductResponse, error) {
stock := p.GetStockById(request.ProdId)
addr := "beijing"
user := &User{Address: &addr}
a, _ := anypb.New(&Content{Msg: "hello"}) // 转换成Any类型
return &ProductResponse{ProdStock: stock, User: user, Data: a}, nil
}
func (p *productService) GetStockById(id int32) int32 {
return 100
}
func (*productService) mustEmbedUnimplementedProductServiceServer() {}
func (*productService) UpdateProductStockClientStream(stream ProductService_UpdateProductStockClientStreamServer) error {
count := 0
for {
recv, err := stream.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
fmt.Println("服务端接收到的流", recv.ProdId)
count++
if count > 10 {
rsq := &ProductResponse{ProdStock: recv.ProdId}
err := stream.SendAndClose(rsq) // 发送响应
if err != nil {
return err
}
return nil
}
}
}
UnimplementedProductServiceServer must be embedded to have forward compatible implementations.
//type UnimplementedProductServiceServer struct {
//}
//
//func (UnimplementedProductServiceServer) GetProductStock(context.Context, *ProductRequest) (*ProductResponse, error) {
// return nil, status.Errorf(codes.Unimplemented, "method GetProductStock not implemented")
//}
//func (UnimplementedProductServiceServer) mustEmbedUnimplementedProductServiceServer() {}
3.修改客户端文件
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"log"
"test.com/test_grpc/client/auth"
"test.com/test_grpc/client/service"
"time"
)
func main() {
// 添加公钥
// creds, err0 := credentials.NewClientTLSFromFile("../cert/server.pem", "*.test.com")
// if err0 != nil {
// log.Fatal("证书错误: ", err0)
// }
// 证书认证-双向认证
// 从证书相关文件中读取解析信息, 得到证书公钥、密钥对
cert, err0 := tls.LoadX509KeyPair("../cert/client.pem", "../cert/client.key")
if err0 != nil {
log.Fatal("证书读取失败", err0)
}
fmt.Println("证书读取成功")
// 创建一个新的、空的CertPool
certPool := x509.NewCertPool()
ca, err1 := ioutil.ReadFile("../cert/ca.crt")
if err1 != nil {
log.Fatal("ca证书读取失败", err1)
}
fmt.Println("ca证书读取成功")
// 尝试解析所传入的PEM编码的证书,如果解析成功会将其加到CertPool中,便于后面使用
certPool.AppendCertsFromPEM(ca)
// 构建基于TLS的TransportCredentials选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链, 允许包含一个或多个
Certificates: []tls.Certificate{cert},
ServerName: "*.test.com",
RootCAs: certPool,
})
fmt.Println("设置TLS的TransportCredentials选项成功")
token := &auth.Authentication{
User: "admin",
Passwd: "admin@123",
}
// 创建连接
conn, err := grpc.Dial(":8888", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(token))
if err != nil {
log.Fatal("服务端连接失败: ", err)
}
fmt.Println("证书认证通过")
// 退出时关闭连接
defer conn.Close()
// 创建客户端实例
productServiceClient := service.NewProductServiceClient(conn)
// 方法请求
// resq, err := productServiceClient.GetProductStock(context.Background(), &service.ProductRequest{ProdId: 233})
// if err != nil {
// log.Fatal("调用gRPC方法失败: ", err)
// }
// fmt.Println("调用gRPC方法成功, ProdStock = ", resq.ProdStock)
// 获取流
stream, err := productServiceClient.UpdateProductStockClientStream(context.Background())
if err != nil {
log.Fatal("获取流失败", err)
}
// 定义切片,设置请求
rsq := make(chan struct{}, 1)
go prodRequest(stream, rsq)
// 等待数据接收
select {
case <-rsq:
// 接收数据
recv, err := stream.CloseAndRecv()
if err != nil {
log.Fatal(err)
}
stock := recv.ProdStock
fmt.Println("客户端收到响应: ", stock)
}
}
// 请求接口
func prodRequest(stream service.ProductService_UpdateProductStockClientStreamClient, rsq chan struct{}) {
count := 0
for {
request := &service.ProductRequest{
ProdId: 100,
}
// 发送数据
err := stream.SendMsg(request)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second)
count++
if count > 10 {
rsq <- struct{}{}
break
}
}
}
4.执行结果
客户端:
证书读取成功
ca证书读取成功
设置TLS的TransportCredentials选项成功
证书认证通过
客户端收到响应: 100
服务端流式RPC
rpc SimplePing(PingRequest) returns (stream PingReply);
1.修改product.proto文件,重新生成Go文件,复制到客户端
syntax = "proto3";
// 从执行protoc这个命令的当前目录开始引入
import "pbfile/user.proto";
import "google/protobuf/any.proto"; //引入Any类型
option go_package="../service";
package service; // 将要生成的go文件包名
message ProductRequest {
int32 prod_id = 1; // 1代表顺序
}
message Content {
string msg = 1;
}
message ProductResponse {
int32 prod_stock = 1; // 1代表顺序
User user = 2;
google.protobuf.Any data = 3;
}
// 导入其他pb文件
service ProductService {
rpc GetProductStock(ProductRequest) returns (ProductResponse);
// 增加客户端流定义方法
rpc UpdateProductStockClientStream(stream ProductRequest) returns(ProductResponse);
// 服务端流定义方法
rpc GetProductStockServerStream(ProductRequest) returns(stream ProductResponse);
}
2.修改服务端product.go文件
package service
import (
"context"
"fmt"
"google.golang.org/protobuf/types/known/anypb"
"io"
"time"
)
var ProductService = &productService{}
type productService struct {
}
// GetProductStock 接口实现
func (p *productService) GetProductStock(context context.Context, request *ProductRequest) (*ProductResponse, error) {
stock := p.GetStockById(request.ProdId)
addr := "beijing"
user := &User{Address: &addr}
a, _ := anypb.New(&Content{Msg: "hello"}) // 转换成Any类型
return &ProductResponse{ProdStock: stock, User: user, Data: a}, nil
}
func (p *productService) GetStockById(id int32) int32 {
return 100
}
func (*productService) mustEmbedUnimplementedProductServiceServer() {}
func (*productService) UpdateProductStockClientStream(stream ProductService_UpdateProductStockClientStreamServer) error {
count := 0
for {
recv, err := stream.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
fmt.Println("服务端接收到的流", recv.ProdId)
count++
if count > 10 {
rsq := &ProductResponse{ProdStock: recv.ProdId}
err := stream.SendAndClose(rsq) // 发送响应
if err != nil {
return err
}
return nil
}
}
}
// GetProductStockServerStream 新增服务端接口实现
func (p *productService) GetProductStockServerStream(request *ProductRequest, stream ProductService_GetProductStockServerStreamServer) error {
count := 0
for {
rsp := &ProductResponse{ProdStock: request.ProdId}
err := stream.SendMsg(rsp)
if err != nil {
return err
}
time.Sleep(time.Second)
count++
if count > 10 {
return nil
}
}
}
UnimplementedProductServiceServer must be embedded to have forward compatible implementations.
//type UnimplementedProductServiceServer struct {
//}
//
//func (UnimplementedProductServiceServer) GetProductStock(context.Context, *ProductRequest) (*ProductResponse, error) {
// return nil, status.Errorf(codes.Unimplemented, "method GetProductStock not implemented")
//}
//func (UnimplementedProductServiceServer) mustEmbedUnimplementedProductServiceServer() {}
3.修改客户端文件
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io"
"io/ioutil"
"log"
"test.com/test_grpc/client/auth"
"test.com/test_grpc/client/service"
"time"
)
func main() {
// 添加公钥
// creds, err0 := credentials.NewClientTLSFromFile("../cert/server.pem", "*.test.com")
// if err0 != nil {
// log.Fatal("证书错误: ", err0)
// }
// 证书认证-双向认证
// 从证书相关文件中读取解析信息, 得到证书公钥、密钥对
cert, err0 := tls.LoadX509KeyPair("../cert/client.pem", "../cert/client.key")
if err0 != nil {
log.Fatal("证书读取失败", err0)
}
fmt.Println("证书读取成功")
// 创建一个新的、空的CertPool
certPool := x509.NewCertPool()
ca, err1 := ioutil.ReadFile("../cert/ca.crt")
if err1 != nil {
log.Fatal("ca证书读取失败", err1)
}
fmt.Println("ca证书读取成功")
// 尝试解析所传入的PEM编码的证书,如果解析成功会将其加到CertPool中,便于后面使用
certPool.AppendCertsFromPEM(ca)
// 构建基于TLS的TransportCredentials选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链, 允许包含一个或多个
Certificates: []tls.Certificate{cert},
ServerName: "*.test.com",
RootCAs: certPool,
})
fmt.Println("设置TLS的TransportCredentials选项成功")
token := &auth.Authentication{
User: "admin",
Passwd: "admin@123",
}
// 创建连接
conn, err := grpc.Dial(":8888", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(token))
if err != nil {
log.Fatal("服务端连接失败: ", err)
}
fmt.Println("证书认证通过")
// 退出时关闭连接
defer conn.Close()
// 创建客户端实例
productServiceClient := service.NewProductServiceClient(conn)
// 方法请求
// resq, err := productServiceClient.GetProductStock(context.Background(), &service.ProductRequest{ProdId: 233})
// if err != nil {
// log.Fatal("调用gRPC方法失败: ", err)
// }
// fmt.Println("调用gRPC方法成功, ProdStock = ", resq.ProdStock)
// 获取流
// stream, err := productServiceClient.UpdateProductStockClientStream(context.Background())
// if err != nil {
// log.Fatal("获取流失败", err)
// }
// rsq := make(chan struct{}, 1)
// go prodRequest(stream, rsq)
// select {
// case <-rsq:
// recv, err := stream.CloseAndRecv()
// if err != nil {
// log.Fatal(err)
// }
// stock := recv.ProdStock
// fmt.Println("客户端收到响应: ", stock)
// }
request := &service.ProductRequest{
ProdId: 100,
}
// 调用服务端接口获取流
stream, err := productServiceClient.GetProductStockServerStream(context.Background(), request)
if err != nil {
log.Fatal("获取流失败")
}
// 循环获取流
for {
recv, err := stream.Recv()
if err != nil {
// 流数据接收完成标志
if err == io.EOF {
fmt.Println("客户端接收数据完成")
stream.CloseSend()
break
}
log.Fatal(err)
}
fmt.Println("客户端收到的流", recv.ProdStock)
}
}
func prodRequest(stream service.ProductService_UpdateProductStockClientStreamClient, rsq chan struct{}) {
count := 0
for {
request := &service.ProductRequest{
ProdId: 100,
}
err := stream.Send(request)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second)
count++
if count > 10 {
rsq <- struct{}{}
break
}
}
}
4.执行结果
客户端:
证书读取成功
ca证书读取成功
设置TLS的TransportCredentials选项成功
证书认证通过
客户端收到的流 100
客户端收到的流 100
客户端收到的流 100
客户端收到的流 100
客户端收到的流 100
客户端收到的流 100
客户端收到的流 100
客户端收到的流 100
客户端收到的流 100
客户端收到的流 100
客户端收到的流 100
客户端接收数据完成
双向流式RPC
// 双向流式RPC
rpc SimplePing(stream PingRequest) returns (stream PingReply);
1.修改product.proto文件,重新生成Go文件,复制到客户端
syntax = "proto3";
// 从执行protoc这个命令的当前目录开始引入
import "pbfile/user.proto";
import "google/protobuf/any.proto"; //引入Any类型
option go_package="../service";
package service; // 将要生成的go文件包名
message ProductRequest {
int32 prod_id = 1; // 1代表顺序
}
message Content {
string msg = 1;
}
message ProductResponse {
int32 prod_stock = 1; // 1代表顺序
User user = 2;
google.protobuf.Any data = 3;
}
// 导入其他pb文件
service ProductService {
rpc GetProductStock(ProductRequest) returns (ProductResponse);
// 增加客户端流定义方法
rpc UpdateProductStockClientStream(stream ProductRequest) returns(ProductResponse);
// 服务端流定义方法
rpc GetProductStockServerStream(ProductRequest) returns(stream ProductResponse);
// 双向流定义方法
rpc SayHelloStream(stream ProductRequest) returns(stream ProductResponse);
}
2.修改服务端product.go文件
package service
import (
"context"
"fmt"
"google.golang.org/protobuf/types/known/anypb"
"io"
"time"
)
var ProductService = &productService{}
type productService struct {
}
// GetProductStock 接口实现
func (p *productService) GetProductStock(context context.Context, request *ProductRequest) (*ProductResponse, error) {
stock := p.GetStockById(request.ProdId)
addr := "beijing"
user := &User{Address: &addr}
a, _ := anypb.New(&Content{Msg: "hello"}) // 转换成Any类型
return &ProductResponse{ProdStock: stock, User: user, Data: a}, nil
}
func (p *productService) GetStockById(id int32) int32 {
return 100
}
func (*productService) mustEmbedUnimplementedProductServiceServer() {}
func (*productService) UpdateProductStockClientStream(stream ProductService_UpdateProductStockClientStreamServer) error {
count := 0
for {
recv, err := stream.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
fmt.Println("服务端接收到的流", recv.ProdId)
count++
if count > 10 {
rsq := &ProductResponse{ProdStock: recv.ProdId}
err := stream.SendAndClose(rsq) // 发送响应
if err != nil {
return err
}
return nil
}
}
}
// GetProductStockServerStream 新增服务端接口实现
func (p *productService) GetProductStockServerStream(request *ProductRequest, stream ProductService_GetProductStockServerStreamServer) error {
count := 0
for {
rsp := &ProductResponse{ProdStock: request.ProdId}
err := stream.SendMsg(rsp)
if err != nil {
return err
}
time.Sleep(time.Second)
count++
if count > 10 {
return nil
}
}
}
func (p *productService) SayHelloStream(stream ProductService_SayHelloStreamServer) error {
for {
// 接收消息
recv, err := stream.Recv()
if err != nil {
return nil
}
fmt.Println("服务端接收客户端的消息", recv.ProdId)
time.Sleep(time.Second)
// 发送消息
rsp := &ProductResponse{ProdStock: recv.ProdId}
err = stream.Send(rsp)
if err != nil {
return err
}
}
}
UnimplementedProductServiceServer must be embedded to have forward compatible implementations.
//type UnimplementedProductServiceServer struct {
//}
//
//func (UnimplementedProductServiceServer) GetProductStock(context.Context, *ProductRequest) (*ProductResponse, error) {
// return nil, status.Errorf(codes.Unimplemented, "method GetProductStock not implemented")
//}
//func (UnimplementedProductServiceServer) mustEmbedUnimplementedProductServiceServer() {}
3.修改客户端文件
package main
import (
"google.golang.org/grpc"
"log"
"test.com/test_grpc/client/service"
"context"
"fmt"
"google.golang.org/grpc/credentials"
"crypto/tls"
"crypto/x509"
"io/ioutil"
// "io"
"test.com/test_grpc/client/auth"
"time"
)
func main() {
// 添加公钥
// creds, err0 := credentials.NewClientTLSFromFile("../cert/server.pem", "*.test.com")
// if err0 != nil {
// log.Fatal("证书错误: ", err0)
// }
// 证书认证-双向认证
// 从证书相关文件中读取解析信息, 得到证书公钥、密钥对
cert, err0 := tls.LoadX509KeyPair("../cert/client.pem", "../cert/client.key")
if err0 != nil {
log.Fatal("证书读取失败", err0)
}
fmt.Println("证书读取成功")
// 创建一个新的、空的CertPool
certPool := x509.NewCertPool()
ca, err1 := ioutil.ReadFile("../cert/ca.crt")
if err1 != nil {
log.Fatal("ca证书读取失败", err1)
}
fmt.Println("ca证书读取成功")
// 尝试解析所传入的PEM编码的证书,如果解析成功会将其加到CertPool中,便于后面使用
certPool.AppendCertsFromPEM(ca)
// 构建基于TLS的TransportCredentials选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链, 允许包含一个或多个
Certificates: []tls.Certificate{cert},
ServerName: "*.test.com",
RootCAs: certPool,
})
fmt.Println("设置TLS的TransportCredentials选项成功")
token := &auth.Authentication{
User: "admin",
Passwd: "admin@123",
}
// 创建连接
conn, err := grpc.Dial(":8888", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(token))
if err != nil {
log.Fatal("服务端连接失败: ", err)
}
fmt.Println("证书认证通过")
// 退出时关闭连接
defer conn.Close()
// 创建客户端实例
productServiceClient := service.NewProductServiceClient(conn)
// 方法请求
// resq, err := productServiceClient.GetProductStock(context.Background(), &service.ProductRequest{ProdId: 233})
// if err != nil {
// log.Fatal("调用gRPC方法失败: ", err)
// }
// fmt.Println("调用gRPC方法成功, ProdStock = ", resq.ProdStock)
// 获取流
// stream, err := productServiceClient.UpdateProductStockClientStream(context.Background())
// if err != nil {
// log.Fatal("获取流失败", err)
// }
// rsq := make(chan struct{}, 1)
// go prodRequest(stream, rsq)
// select {
// case <-rsq:
// recv, err := stream.CloseAndRecv()
// if err != nil {
// log.Fatal(err)
// }
// stock := recv.ProdStock
// fmt.Println("客户端收到响应: ", stock)
// }
// request := &service.ProductRequest{
// ProdId: 100,
// }
// stream, err := productServiceClient.GetProductStockServerStream(context.Background(), request)
// if err != nil {
// log.Fatal("获取流失败")
// }
// for {
// recv, err := stream.Recv()
// if err != nil {
// if err == io.EOF {
// fmt.Println("客户端接收数据完成")
// stream.CloseSend()
// break
// }
// log.Fatal(err)
// }
// fmt.Println("客户端收到的流", recv.ProdStock)
// }
// 获取双向流
stream, err := productServiceClient.SayHelloStream(context.Background())
for {
// 发送消息
request := &service.ProductRequest{
ProdId: 100,
}
err = stream.Send(request)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second)
recv, err := stream.Recv()
if err != nil {
log.Fatal(err)
}
fmt.Println("客户端接收服务端的消息", recv.ProdStock)
}
}
func prodRequest(stream service.ProductService_UpdateProductStockClientStreamClient, rsq chan struct{}) {
count := 0
for {
request := &service.ProductRequest{
ProdId: 100,
}
err := stream.Send(request)
if err != nil {
log.Fatal(err)
}
time.Sleep(time.Second)
count++
if count > 10 {
rsq <- struct{}{}
break
}
}
}
4.执行结果
客户端与服务端互相发数据
纠正
proto文件生成后,拷贝*.pb.go文件到客户端即可
Reference
https://www.bilibili.com/video/BV16Z4y117yz