项目仓库
原教程视频
序列化对象为二进制和Json Go- 将protobuf信息写入二进制文件
- 从二进制文件中读取protobuf信息
- 写入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 的四种通信方式
- 类似REST的单请求+单回复
- 客户端多请求+服务端单回复
- 客户端单请求+服务端多回复
- 客户端多请求+服务端多回复
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。
一个grpc客户端,在服务端开启反射并运行时,通过evans -r repl -p 端口
进入shell,
通过show package
查看反射的包信息,通过package 包名
选择不同的包,通过show service
查看反射的服务信息,通过service 服务
选择服务,通过call CreateLaptop
调用服务,中途使用ctrl+D
取消重复字段的输入…
evans
类似于中间件,可以在服务端和客户端之间添加的额外功能,服务器端拦截器是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的前身。
TLS在网络(HTTPS = HTTP+TLS),邮件(SMTPS = SMTP+TLS),文件传输(FTPS = FTP+TLS)等中使用。
作用:
-
身份验证 证明访问的网站不是伪造的
将服务器公钥放入到数字证书中,解决了冒充的风险
-
信息加密 交互信息无法被窃取
通过混合加密的方式可以保证信息的机密性,解决了窃听的风险
HTTPS 采用的是对称加密和非对称加密结合的「混合加密」方式:
- 在通信建立前采用非对称加密的方式交换「会话秘钥」,后续就不再使用非对称加密。
- 在通信过程中全部使用对称加密的「会话秘钥」的方式加密明文数据。
-
校验机制 无法篡改通信内容
摘要算法用来实现完整性,能够为数据生成独一无二的「指纹」,用于校验数据的完整性,解决了篡改的风险。
一般采用ECDHE密钥协商算法生成会话密钥
-
TLS第一次握手
客户端首先会发一个「Client Hello」消息,消息里面有客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数(*Client Random*)。
-
TLS第二次握手
服务端收到客户端的「打招呼」返回「Server Hello」消息,消息面有服务器确认的 TLS 版本号,也给出了一个随机数(*Server Random*),然后从客户端的密码套件列表选择了一个合适的密码套件。接着,服务端为了证明自己的身份,发送「Certificate」消息,会把证书也发给客户端。
因为服务端选择了 ECDHE 密钥协商算法,所以会在发送完证书后,发送「Server Key Exchange」消息。
- 选择了椭圆曲线,选好了椭圆曲线相当于椭圆曲线基点 G 也定好了,这些都会公开给客户端;
- 生成随机数作为服务端椭圆曲线的私钥,保留到本地;
- 根据基点 G 和私钥计算出服务端的椭圆曲线公钥,这个会公开给客户端。
为了保证这个椭圆曲线的公钥不被第三方篡改,服务端会用 RSA 签名算法给服务端的椭圆曲线公钥做个签名。
-
TLS第三次握手
客户端收到了服务端的证书后,校验证书是否合法。
客户端会生成一个随机数作为客户端椭圆曲线的私钥,然后再根据服务端前面给的信息,生成客户端的椭圆曲线公钥,然后用「Client Key Exchange」消息发给服务端
最终的会话密钥,就是用「客户端随机数 + 服务端随机数 + x(ECDHE 算法算出的共享密钥) 」三个材料生成的。
算好会话密钥后,客户端会发一个「Change Cipher Spec」消息,告诉服务端后续改用对称算法加密通信。
接着,客户端会发「Encrypted Handshake Message」消息,把之前发送的数据做一个摘要,再用对称密钥加密一下,让服务端做个验证,验证下本次生成的对称密钥是否可以正常使用。
-
TLS第四次握手
最后,服务端也会有一个同样的 *** 作,发「Change Cipher Spec」和「Encrypted Handshake Message」消息,如果双方都验证加密和解密没问题,那么握手正式完成。于是,就可以正常收发加密的 HTTP 请求和响应了。
-
INSECURE 无安全验证
-
SERVER-SIDE TLS 服务端证书
服务端采用TLS加密数据,传递证书给客户端,客户端通过CA进行校验
-
MUTUAL SSL 客户端与服务端证书
双向进行加密并校验
客户端发送请求到代理服务器,代理服务器负责负载均衡
便于部署,且适用于面向不确定使用者的环境下。
但是会增加一个跳点,增加延迟。
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/protoc-gen-grpc-gateway@v1.16.0
go get google.golang.org/protobuf/cmd/protoc-gen-go
# 增加google.api.http 引入第三方protobuf包
cp -r $GOPATH/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/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文件生成路径
-
进程间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的好习惯,宝藏博主!
煎鱼大佬的书写的更加细致,之后就过一遍。
遇到的问题- protoc 版本问题 可以通过指定版本号解决
- rpc走代理导致连接出问题 注意你的rpc服务可能会走代理,从而没法连接,记得关了就行
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)