Categorygithub.com/0RAJA/RPC
module
1.0.0
Repository: https://github.com/0raja/rpc.git
Documentation: pkg.go.dev

# README

gRPC小项目学习总结

教程视频

序列化对象为二进制和Json

Go

  1. 将protobuf信息写入二进制文件
  2. 从二进制文件中读取protobuf信息
  3. 写入json文件并比较大小
//json.go

// ProtobufToJson 将protobuf文件转换为json
func ProtobufToJson(message proto.Message) ([]byte, error) {
	marshaler := protojson.MarshalOptions{
		UseEnumNumbers:  true, //枚举值使用数字
		EmitUnpopulated: true, //未填充字段使用默认值
	}
	bin, err := marshaler.Marshal(proto.MessageV2(message))
	if err != nil {
		return nil, err
	}
	return bin, nil
}
//file.go

//序列化对象

// WriteProtobufToBinaryFile 将message对象序列化并写入二进制文件中
func WriteProtobufToBinaryFile(message proto.Message, filename string) error {
	data, err := proto.Marshal(message) //序列化
	if err != nil {
		return err
	}
	if err = ioutil.WriteFile(filename, data, 0644); err != nil {
		return err
	}
	return nil
}

gRPC 的四种通信方式

  1. 类似REST的单请求+单回复
  2. 客户端多请求+服务端单回复
  3. 客户端单请求+服务端多回复
  4. 客户端多请求+服务端多回复
service LaptopService {
  //一元RPC 创建电脑
  rpc CreateLaptop(CreateLaptopRequest) returns (CreateLaptopResponse){
    option (google.api.http) = {
      post: "/v1/laptop/create"
      body: "*"
    };
  };
  //服务器流式RPC 检索电脑
  rpc SearchLaptop(SearchLaptopRequest) returns (stream SearchLaptopResponse){
    option (google.api.http) = {
      get: "/v1/laptop/search"
    };
  };
  //客户端流式RPC 上传图片
  rpc UploadLaptop(stream UploadLaptopRequest) returns (UploadLaptopResponse){
    option (google.api.http) = {
      post: "/v1/laptop/upload"
      body: "*"
    };
  };
  //双向流式RPC 评分
  rpc RateLaptop(stream RateLaptopRequest) returns (stream RateLaptopResponse){
    option (google.api.http) = {
      post: "/v1/laptop/rate"
      body: "*"
    };
  };
}

gRPC 反射

grpc反射 gRPC 服务器反射提供有关服务器上可公开访问的 gRPC 服务的信息,并帮助客户端在运行时构造 RPC 请求和响应,而无需预编译的服务信息。 它由 gRPC CLI 使用,可用于自省服务器原型和发送/接收测试 RPC。

evans

一个grpc客户端,在服务端开启反射并运行时,通过evans -r repl -p 端口进入shell, 通过show package查看反射的包信息,通过package 包名选择不同的包,通过show service查看反射的服务信息,通过service 服务选择服务,通过call CreateLaptop 调用服务,中途使用ctrl+D取消重复字段的输入... evans

gRPC 拦截器

类似于中间件,可以在服务端和客户端之间添加的额外功能,服务器端拦截器是gRPC服务器在调用实际RPC方法前将调用的函数,可以用于日志记录,跟踪,限流,身份验证,限流等。 客户端拦截器是gRPC客户端在调用实际RPC方法前将调用的函数.

服务器端拦截器将采用JWT来验证,客户端拦截器将添加JWT到请求。

其实和go web 的token很像,都是注册到服务的前面,只不过方式不同而已。

// 拦截器的编写
const Authorization = "authorization"

//权限校验

type AuthInterceptor struct {
	jwtMaker        token.Maker
	accessibleRoles map[string][]string //RPC对应的Roles
}

func NewAuthInterceptor(jwtMaker token.Maker, accessibleRoles map[string][]string) *AuthInterceptor {
	return &AuthInterceptor{jwtMaker: jwtMaker, accessibleRoles: accessibleRoles}
}

// Unary 一元拦截器
func (interceptor *AuthInterceptor) Unary() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		log.Println("-->unary Interceptor: ", info.FullMethod)
		if err := interceptor.authorized(ctx, info.FullMethod); err != nil {
			return nil, err
		}
		return handler(ctx, req)
	}
}

// Stream 流式拦截器
func (interceptor *AuthInterceptor) Stream() grpc.StreamServerInterceptor {
	return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
		log.Println("-->stream Interceptor: ", info.FullMethod)
		if err := interceptor.authorized(ss.Context(), info.FullMethod); err != nil {
			return err
		}
		return handler(srv, ss)
	}
}

func (interceptor *AuthInterceptor) authorized(ctx context.Context, method string) error {
	accessibleRoles, ok := interceptor.accessibleRoles[method]
	if !ok {
		//没有设置拦截
		return nil
	}
	//从ctx中获取访问信息
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return status.Errorf(codes.Unauthenticated, "metadata is not provided")
	}
	values := md[Authorization]
	if len(values) == 0 {
		return status.Errorf(codes.Unauthenticated, "authorization token is not provided")
	}
	//存储在第一个位置
	accessToken := values[0]
	payload, err := interceptor.jwtMaker.VerifyToken(accessToken)
	if err != nil {
		return status.Errorf(codes.Unauthenticated, "access token is not valid:%v", err)
	}
	for _, role := range accessibleRoles {
		if payload.Role == role {
			return nil
		}
	}
	return status.Errorf(codes.PermissionDenied, "no permissions to access this rpc")
}

//注册拦截器
func runGRPCServer(config *serverConfig) error {
	//初始化拦截器
	interceptor := service.NewAuthInterceptor(config.maker, accessibleRoles())
	serviceOptions := []grpc.ServerOption{
		//安装一个一元拦截器
		grpc.UnaryInterceptor(interceptor.Unary()),
		//安装一个流拦截器
		grpc.StreamInterceptor(interceptor.Stream()),
	}
	...
	//配置gRPC服务器
	grpcServer := grpc.NewServer(
		serviceOptions...,
	)
	...
	//启动gRPC服务
	return grpcServer.Serve(config.listener)
}

SSL/TLS

详解

TLS 是传输层安全协议,它用于实现客户端和服务器之间的加密通信。SSL是TLS的前身。

image-20220504204623342

TLS在网络(HTTPS = HTTP+TLS),邮件(SMTPS = SMTP+TLS),文件传输(FTPS = FTP+TLS)等中使用。

作用:

  1. 身份验证 证明访问的网站不是伪造的

    将服务器公钥放入到数字证书中,解决了冒充的风险

  2. 信息加密 交互信息无法被窃取

    通过混合加密的方式可以保证信息的机密性,解决了窃听的风险

    HTTPS 采用的是对称加密非对称加密结合的「混合加密」方式:

    • 在通信建立前采用非对称加密的方式交换「会话秘钥」,后续就不再使用非对称加密。
    • 在通信过程中全部使用对称加密的「会话秘钥」的方式加密明文数据。
  3. 校验机制 无法篡改通信内容

    摘要算法用来实现完整性,能够为数据生成独一无二的「指纹」,用于校验数据的完整性,解决了篡改的风险。

一般采用ECDHE密钥协商算法生成会话密钥

  1. TLS第一次握手

    客户端首先会发一个「Client Hello」消息,消息里面有客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数(*Client Random*)

  2. TLS第二次握手

    服务端收到客户端的「打招呼」返回「Server Hello」消息,消息面有服务器确认的 TLS 版本号,也给出了一个随机数(*Server Random*),然后从客户端的密码套件列表选择了一个合适的密码套件。接着,服务端为了证明自己的身份,发送「Certificate」消息,会把证书也发给客户端。

    因为服务端选择了 ECDHE 密钥协商算法,所以会在发送完证书后,发送「Server Key Exchange」消息。

    • 选择了椭圆曲线,选好了椭圆曲线相当于椭圆曲线基点 G 也定好了,这些都会公开给客户端;
    • 生成随机数作为服务端椭圆曲线的私钥,保留到本地;
    • 根据基点 G 和私钥计算出服务端的椭圆曲线公钥,这个会公开给客户端。

    为了保证这个椭圆曲线的公钥不被第三方篡改,服务端会用 RSA 签名算法给服务端的椭圆曲线公钥做个签名。

  3. TLS第三次握手

    客户端收到了服务端的证书后,校验证书是否合法。

    客户端会生成一个随机数作为客户端椭圆曲线的私钥,然后再根据服务端前面给的信息,生成客户端的椭圆曲线公钥,然后用「Client Key Exchange」消息发给服务端

    最终的会话密钥,就是用「客户端随机数 + 服务端随机数 + x(ECDHE 算法算出的共享密钥) 」三个材料生成的

    算好会话密钥后,客户端会发一个「Change Cipher Spec」消息,告诉服务端后续改用对称算法加密通信。

    接着,客户端会发「Encrypted Handshake Message」消息,把之前发送的数据做一个摘要,再用对称密钥加密一下,让服务端做个验证,验证下本次生成的对称密钥是否可以正常使用。

  4. TLS第四次握手

    最后,服务端也会有一个同样的操作,发「Change Cipher Spec」和「Encrypted Handshake Message」消息,如果双方都验证加密和解密没问题,那么握手正式完成。于是,就可以正常收发加密的 HTTP 请求和响应了。

  5. INSECURE 无安全验证

  6. SERVER-SIDE TLS 服务端证书

    服务端采用TLS加密数据,传递证书给客户端,客户端通过CA进行校验

  7. MUTUAL SSL 客户端与服务端证书

    双向进行加密并校验

nginx 负载均衡

服务端

客户端发送请求到代理服务器,代理服务器负责负载均衡

便于部署,且适用于面向不确定使用者的环境下。

但是会增加一个跳点,增加延迟。

worker_processes  1;

error_log  /var/log/nginx/error.log;

events {
    worker_connections  1024;
}


http {
    access_log  /var/log/nginx/access.log;

    # 上游
    upstream laptop_services {
        server 0.0.0.0:50051;
        server 0.0.0.0:50052;
    }

    server {
        listen       8080 http2;

        location / {
            grpc_pass grpc://laptop_services;
        }
    }
}

一般部署情况下,grpc服务器位于安全的环境下,所以只需要让nginx服务器开启SSL/TLS加密。即将服务器私钥,服务器证书以及签署客户端证书的CA证书提供给nginx

worker_processes  1;

error_log  /var/log/nginx/error.log;

events {
    worker_connections  1024;
}


http {
    access_log  /var/log/nginx/access.log;

    # 上游
    upstream laptop_services {
        server 0.0.0.0:50051;
        server 0.0.0.0:50052;
    }

    server {
        listen       8080 ssl http2;
        # 服务器证书和密钥
        ssl_certificate         cert/server-cert.pem;
        ssl_certificate_key     cert/server-key.pem;

        # 签署客户端证书的CA证书
        ssl_client_certificate  cert/ca-cert.pem;
        # 开启客户端证书验证
        ssl_verify_client on;

        # grpcs 开启服务端TLS
        location / {
            grpc_pass grpcs://laptop_services;
        }
    }
}

但是如果真的需要开启双向TLS,即nginx和grpc服务器之间的双向TLS 则需要将nginx证书传递给grpc服务器

worker_processes  1;

error_log  /var/log/nginx/error.log;

events {
    worker_connections  1024;
}


http {
    access_log  /var/log/nginx/access.log;

    # 上游
    upstream laptop_services {
        server 0.0.0.0:50051;
        server 0.0.0.0:50052;
    }

    server {
        listen       8080 ssl http2;
        # 服务器证书和密钥
        ssl_certificate         cert/server-cert.pem;
        ssl_certificate_key     cert/server-key.pem;

        # 签署客户端证书的CA证书
        ssl_client_certificate  cert/ca-cert.pem;
        # 开启客户端证书验证
        ssl_verify_client on;

        # grpcs 开启nginx服务端TLS
        location / {
            grpc_pass grpcs://laptop_services;

            # 开启nginx TLS (可以为nginx生成指定证书) 向服务端发送TLS证书
            grpc_ssl_certificate cert/server-cert.pem;
            grpc_ssl_certificate_key cert/server-key.pem;
        }
    }
}

其次实现业务分离

worker_processes  1;

error_log  /var/log/nginx/error.log;

events {
    worker_connections  1024;
}


http {
    access_log  /var/log/nginx/access.log;

    # auth上游
    upstream auth_services {
        server 0.0.0.0:50051;
    }
    # laptop上游
    upstream laptop_services {
        server 0.0.0.0:50052;
    }

    server {
        listen       8080 ssl http2;
        # 服务器证书和密钥
        ssl_certificate         cert/server-cert.pem;
        ssl_certificate_key     cert/server-key.pem;

        # 签署客户端证书的CA证书
        ssl_client_certificate  cert/ca-cert.pem;
        # 开启客户端证书验证
        ssl_verify_client on;

        # grpcs 开启nginx服务端TLS
        # 转发auth
        location /rpc.proto.AuthService {
            grpc_pass grpcs://auth_services;

            # 开启nginx TLS (可以为nginx生成指定证书) 向服务端发送TLS证书
            grpc_ssl_certificate cert/server-cert.pem;
            grpc_ssl_certificate_key cert/server-key.pem;
        }
        # 转发laptop
        location /rpc.proto.LaptopService {
            grpc_pass grpcs://laptop_services;

            # 开启nginx TLS (可以为nginx生成指定证书) 向服务端发送TLS证书
            grpc_ssl_certificate cert/server-cert.pem;
            grpc_ssl_certificate_key cert/server-key.pem;
        }
    }
}

客户端

客户端为每个RPC选择不同的后端服务器,通过服务注册来注册后端服务器,客户端访问服务注册来获取服务器地址。

延迟低,但是实现复杂且适用于安全的场景下。

grpc 网关

gRPC网关可以通过protobuf服务定义生成代理服务器,然后将REST请求翻译为grpc请求

安装插件

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
get -u github.com/grpc-ecosystem/grpc-gateway/[email protected]
go get google.golang.org/protobuf/cmd/protoc-gen-go

# 增加google.api.http 引入第三方protobuf包
cp -r $GOPATH/pkg/mod/github.com/grpc-ecosystem/[email protected]/third_party/googleapis/google ./proto
//增加
import "google/api/annotations.proto";
//修改
service AuthService{
  rpc Login(LoginRequest) returns (LoginResponse){
    option (google.api.http) = {
      post: "/v1/auth/login"
      body: "*"
    };
  }
}

之后生成grpc网管和swagger文件

protoc --proto_path=proto --go_out=plugins=grpc:pb proto/*.proto --grpc-gateway_out=:pb --swagger_out=:swagger
# --grpc-gateway_out=:pb 指定网关生成路径
# --swagger_out=:swagger swaager文件生成路径
  1. 进程间RPC转换

    不需要运行单独的gRPC服务器,但是目前只支持一元RPC

    修改main文件 增加启动REST的方式

    type serverConfig struct {
    	laptopServer *service.LaptopServer
    	authServer   *service.AuthServer
    	enableTLS    bool
    	listener     net.Listener
    	maker        token.Maker
    }
    
    func runRESTServer(config *serverConfig) error {
    	mux := runtime.NewServeMux()
    	ctx, cancel := context.WithCancel(context.Background())
    	defer cancel()
    	//gRPC到REST的进程间转换
    	if err := pb.RegisterAuthServiceHandlerServer(ctx, mux, config.authServer); err != nil {
    		return err
    	}
    	if err := pb.RegisterLaptopServiceHandlerServer(ctx, mux, config.laptopServer); err != nil {
    		return err
    	}
    	log.Println("server port:", config.listener.Addr().String(), " tls=", config.enableTLS)
    	if config.enableTLS {
    		return http.ServeTLS(config.listener, mux, ServerCert, ServerKey)
    	}
    	return http.Serve(config.listener, mux)
    }
    func runGRPCServer(config *serverConfig) error {
    	//初始化拦截器
    	interceptor := service.NewAuthInterceptor(config.maker, accessibleRoles())
    	serviceOptions := []grpc.ServerOption{
    		//安装一个一元拦截器
    		grpc.UnaryInterceptor(interceptor.Unary()),
    		//安装一个流拦截器
    		grpc.StreamInterceptor(interceptor.Stream()),
    	}
    	//添加TLS凭证
    	if config.enableTLS {
    		tlsCredentials, err := loadTLSCredentials()
    		if err != nil {
    			return fmt.Errorf("can't load TLS credentials,error: %w", err)
    		}
    		serviceOptions = append(serviceOptions, grpc.Creds(tlsCredentials))
    	}
    	//配置gRPC服务器
    	grpcServer := grpc.NewServer(
    		serviceOptions...,
    	)
    	reflection.Register(grpcServer)                                 //注册反射服务
    	pb.RegisterLaptopServiceServer(grpcServer, config.laptopServer) //注册laptop服务
    	pb.RegisterAuthServiceServer(grpcServer, config.authServer)     //注册auth服务
    	log.Println("server port:", config.listener.Addr().String(), " tls=", config.enableTLS)
    	//启动gRPC服务
    	return grpcServer.Serve(config.listener)
    }
    
    func main() {
    	portPtr := flag.Int("port", 8080, "server port")
    	enableTLSPtr := flag.Bool("tls", false, "enable tls") //是否开启TLS
    	serverType := flag.String("type", "grpc", "type of server(grpc/rest)")
    	flag.Parse()
    	//初始化持久层
    	userStore := service.NewInMemoryUserStoreStore()
    	maker, err := token.NewPasetoMaker([]byte(Secret))
    	if err != nil {
    		log.Fatalln("create maker error:", err)
    	}
    	//初始化用户存储
    	if err := seedUsers(userStore); err != nil {
    		log.Fatalln("cannot seed user store:", err)
    	}
    	laptopStore := service.NewInMemoryLaptopStore()
    	imageStore := service.NewDiskImageStore("img", ImageMaxSize)
    	scoreStore := service.NewInMemoryRateStoreStore()
    
    	laptopServer := service.NewLaptopServer(laptopStore, imageStore, scoreStore)
    	authServer := service.NewAuthServer(userStore, maker)
    	addr := fmt.Sprintf("0.0.0.0:%d", *portPtr)
    	listener, err := net.Listen("tcp", addr)
    	if err != nil {
    		log.Fatalln("listener error: ", err)
    	}
    	config := &serverConfig{
    		laptopServer: laptopServer,
    		authServer:   authServer,
    		enableTLS:    *enableTLSPtr,
    		listener:     listener,
    		maker:        maker,
    	}
    	if *serverType == "grpc" {
    		err = runGRPCServer(config)
    	} else {
    		err = runRESTServer(config)
    	}
    }
    

    2, 可以使用grpc网关支持REST流式RPC

    需要同时开启gRPC服务器和REST服务器,REST服务器会将接收到的请求发送到gRPC服务器并返回结果

    type serverConfig struct {
    	laptopServer *service.LaptopServer
    	authServer   *service.AuthServer
    	enableTLS    bool
    	listener     net.Listener
    	maker        token.Maker
    	grpcEndpoint string
    }
    
    func runRESTServer(config *serverConfig) error {
    	mux := runtime.NewServeMux()
    	ctx, cancel := context.WithCancel(context.Background())
    	defer cancel()
    	//设置拨号选项
    	dialOpts := []grpc.DialOption{grpc.WithInsecure()}
    	// gRPC到REST的进程间转换 pb.RegisterAuthServiceHandlerServer
    	// gRPC网关 RegisterAuthServiceHandlerFromEndpoint
    	if err := pb.RegisterAuthServiceHandlerFromEndpoint(ctx, mux, config.grpcEndpoint, dialOpts); err != nil {
    		return err
    	}
    	if err := pb.RegisterLaptopServiceHandlerFromEndpoint(ctx, mux, config.grpcEndpoint, dialOpts); err != nil {
    		return err
    	}
    	log.Println("server port:", config.listener.Addr().String(), " tls=", config.enableTLS, "grpcEndpoint=", config.grpcEndpoint)
    	if config.enableTLS {
    		return http.ServeTLS(config.listener, mux, ServerCert, ServerKey)
    	}
    	return http.Serve(config.listener, mux)
    }
    

总结

这个写的还是不是很详细,很多步骤都是直接敲了没记录下来。

比如遇到的protoc版本问题,导致网上的资料和视频资料不同,这边建议去看一看https://golang2.eddycjy.com/posts/ch3/01-simple-grpc-protobuf/,里面讲的更细,而且有对应的版本,可以使用那本书中指明的版本。

protoc版本问题有个链接可以分享

https://www.cnblogs.com/xinliangcoder/p/15647996.html#!comments

教程视频链接:

https://www.youtube.com/watch?v=2Sm_O75I7H0&list=PLy_6D98if3UJd5hxWNfAqKMr15HZqF

这个作者的系列教程讲解的真的非常好,学习到了非常多方面的东西,比如编码习惯,单元测试,养成书写Makefile的好习惯,宝藏博主!

煎鱼大佬的书写的更加细致,之后就过一遍。

遇到的问题

  1. protoc 版本问题 可以通过指定版本号解决
  2. rpc走代理导致连接出问题 注意你的rpc服务可能会走代理,从而没法连接,记得关了就行

# Packages

No description provided by the author
No description provided by the author
Package pb is a reverse proxy.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author