介绍

微服务和单体架构

单体架构弊端

  • 紧耦合

单体应用程序中的服务模块是紧耦合的。业务逻辑纠缠不清,很难隔离应用程序,因此可扩展性成为一个挑战。

  • 缓慢的构建和发布周期

由于代码库非常庞大,这会延缓应用程序的开发和测试周期的速度。

微服务解决了单体架构的弊端

但也有新的缺点

  • 代码冗余

  • 服务和服务之间调用,进程和进程之间调用

    为了加快网络传输,在微服务中引入RPC(远程过程调用),通过自定义协议发起TCP调用

RPC框架是什么

https://zhuanlan.zhihu.com/p/411315625

RPC 框架说白了就是让你可以像调用本地方法一样调用远程服务提供的方法,而不需要关心底层的通信细节。简单地说就让远程服务调用更加简单、透明。 RPC包含了客户端(Client)和服务端(Server)

GRPC

https://grpc.io/docs/what-is-grpc/introduction/

grpc默认使用proto buffers,是谷歌一套成熟的开源的结构数据序列化机制

Protobuf安装

安装protobuf

brew install protobuf
protoc --version //验证

安装go语言的protoc生成器

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

Proto文件

// 声明版本
syntax = "proto3";
//option go_package = "path;name" path表示生成go的存放地址,自动生成目录,name表示go文件所属包名
option go_package = ".;service";

//服务方法,接受客户端参数,返回服务端响应
service SayHello {
  rpc SayHello(HelloRequest) returns (HelloResponse) {}
}

//类似结构体
message HelloRequest {
  string username = 1;
  int64 code = 2;
}

message HelloResponse {
  string responseMsg = 1;
}

利用命令生成

protoc --go_out=../rpc hello.proto
protoc --go-grpc_out=../rpc hello.proto

服务端代码实现

package main

import (
    service "LearnTest/rpc"
    "context"
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
    "log"
    "net"
    "strconv"
)

type server struct {
    service.UnimplementedSayHelloServer
}

func (s *server) SayHello(ctx context.Context, req *service.HelloRequest) (*service.HelloResponse, error) {
    fmt.Println("recv from " + req.Username)
    return &service.HelloResponse{ResponseMsg: "hello " + req.Username + "\nyour code is: " + strconv.Itoa(int(req.Code))}, nil
}

func main() {
    lis, err := net.Listen("tcp", ":2333")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    grpcServer := grpc.NewServer()
    service.RegisterSayHelloServer(grpcServer, &server{})
    reflection.Register(grpcServer)
    fmt.Println("Listening on port 2333...")
    grpcServer.Serve(lis)

}

客户端代码实现

package main

import (
    service "LearnTest/rpc"
    "context"
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    conn, err := grpc.Dial("127.0.0.1:2333", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        return
    }
    defer conn.Close()

    client := service.NewSayHelloClient(conn)
    resp, _ := client.SayHello(context.Background(), &service.HelloRequest{
        Username: "qqw",
        Code:     3377,
    })

    fmt.Println(resp.GetResponseMsg())
}

image-20230323192811509

认证以及安全传输

刚才写的代码是不安全的,在客户端中,我们可以发现,只要我们知道目标服务器的ip和端口就能够对它进行连接传输,所以我们要采取相关认证和安全的传输方式

gRPC 内默认提供了两种 内置的认证方式:

  • 基于 CA 证书的 SSL/TLS 认证方式;
  • 基于 Token 的认证方式。

gRPC 中的连接类型一共有以下 3 种:

  1. insecure connection:不使用 TLS 加密;
  2. server-side TLS:仅服务端 TLS 加密;
  3. mutual TLS:客户端、服务端都使用 TLS 加密。

如之前的连接就是不安全的

conn, err := grpc.Dial("127.0.0.1:2333", grpc.WithTransportCredentials(insecure.NewCredentials()))

TLS认证实现

参考

https://www.cnblogs.com/rickiyang/p/14981374.html

客户端CA证书

# 生成.key  私钥文件
openssl genrsa -out ca.key 2048

# 生成.csr 证书签名请求文件
openssl req -new -key ca.key -out ca.csr

# 自签名生成.crt 证书文件
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt

服务端证书

其中需要先复制本机的openssl.cnf文件到该目录,然后进行相关修改

image-20230323205820530

# 生成.key  私钥文件
openssl genrsa -out server.key 2048

# 生成.csr 证书签名请求文件
openssl req -new  -subj "/C=GB/L=Beijing/O=github/CN=qqw.com" \
-key server.key -out server.csr -config openssl.cnf

# 签名生成.crt 证书文件
openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 \
-in server.csr -out server.crt -extensions v3_req -extfile openssl.cnf

image-20230323204931797

修改相关代码

package main

import (
    service "LearnTest/rpc"
    "context"
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/reflection"
    "log"
    "net"
    "strconv"
)

type server struct {
    service.UnimplementedSayHelloServer
}


func (s *server) SayHello(ctx context.Context, req *service.HelloRequest) (*service.HelloResponse, error) {
    fmt.Println("recv from " + req.Username)
    return &service.HelloResponse{ResponseMsg: "hello " + req.Username + "\nyour code is: " + strconv.Itoa(int(req.Code))}, nil
}

func main() {
    //服务端这里添加上证书和密钥
    cert, _ := credentials.NewServerTLSFromFile("绝对路径/server.crt",
        "绝对路径/server.key")

    lis, err := net.Listen("tcp", ":2333")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    //服务端这里添加上证书和密钥
    grpcServer := grpc.NewServer(grpc.Creds(cert))
    service.RegisterSayHelloServer(grpcServer, &server{})
    reflection.Register(grpcServer)
    fmt.Println("Listening on port 2333...")
    grpcServer.Serve(lis)

}
package main

import (
    service "LearnTest/rpc"
    "context"
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

func main() {
    //客户端这里添加证书,以及配置相关域名,若不是服务端规定的则不能通过认证
    cert, _ := credentials.NewClientTLSFromFile("绝对路径/ca.crt", "kk.qqw.com")

    conn, err := grpc.Dial("127.0.0.1:2333", grpc.WithTransportCredentials(cert))
    if err != nil {
        return
    }
    defer conn.Close()

    client := service.NewSayHelloClient(conn)
    resp, _ := client.SayHello(context.Background(), &service.HelloRequest{
        Username: "qqw",
        Code:     3377,
    })

    fmt.Println(resp.GetResponseMsg())
}

image-20230323210257264

Token认证实现

这里再写一个token.proto文件

// 声明版本
syntax = "proto3";
//option go_package = "path;name" path表示生成go的存放地址,name表示go文件所属包名
option go_package = ".;service";

message LoginRequest {
  string username = 1;
  string password = 2;
}

message LoginResp {
  string status = 1;
  string token = 2;
}

message PingMessage {
  string greet = 1;
}

service TokenService {
  rpc Login(LoginRequest) returns (LoginResp) {}
  rpc Greet(PingMessage) returns (PingMessage) {}
}
protoc --go_out=../rpc token.proto
protoc --go-grpc_out=../rpc token.proto

采用jwt-go做token的验证

package token

import (
    "context"
    "fmt"
    "time"

    "github.com/dgrijalva/jwt-go"
    "google.golang.org/grpc/metadata"
)

func CreateToken(userName string) (tokenString string) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "iss":      "lora-app-server",
        "aud":      "lora-app-server",
        "nbf":      time.Now().Unix(),
        "exp":      time.Now().Add(time.Hour).Unix(),
        "sub":      "user",
        "username": userName,
    })
    tokenString, err := token.SignedString([]byte("qqw-1-sec-37"))
    if err != nil {
        panic(err)
    }
    return tokenString
}

// AuthToken 自定义认证
type AuthToken struct {
    Token string
}

func (c AuthToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "authorization": c.Token,
    }, nil
}

func (c AuthToken) RequireTransportSecurity() bool {
    return false
}

// Claims defines the struct containing the token claims.
type Claims struct {
    jwt.StandardClaims

    // Username defines the identity of the user.
    Username string `json:"username"`
}

// Step1. 从 context 的 metadata 中,取出 token

func getTokenFromContext(ctx context.Context) (string, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return "", fmt.Errorf("ErrNoMetadataInContext")
    }
    // md 的类型是 type MD map[string][]string
    token, ok := md["authorization"]
    if !ok || len(token) == 0 {
        return "", fmt.Errorf("ErrNoAuthorizationInMetadata")
    }
    // 因此,token 是一个字符串数组,我们只用了 token[0]
    return token[0], nil
}

func CheckAuth(ctx context.Context) (username string) {
    tokenStr, err := getTokenFromContext(ctx)
    if err != nil {
        panic("get token from context error")
    }
    var clientClaims Claims
    token, err := jwt.ParseWithClaims(tokenStr, &clientClaims, func(token *jwt.Token) (interface{}, error) {
        if token.Header["alg"] != "HS256" {
            panic("ErrInvalidAlgorithm")
        }
        return []byte("qqw-1-sec-37"), nil
    })
    if err != nil {
        panic("jwt parse error")
    }

    if !token.Valid {
        panic("ErrInvalidToken")
    }

    return clientClaims.Username
}

然后编写服务端代码

注意这个地方token_grpc.pb.go会自行编译出一个mustEmbedUnimplementedTokenServiceServer()方法

参考

type TokenServiceServer interface {
    Login(context.Context, *LoginRequest) (*LoginResp, error)
    Greet(context.Context, *PingMessage) (*PingMessage, error)
    mustEmbedUnimplementedTokenServiceServer()
}

可以在编译时加参数取消这个方法

protoc --go_out=../rpc --go-grpc_out=require_unimplemented_servers=false:../rpc token.proto

或者通过结构体嵌套方式默认实现这个方法,否则在gprc服务端注册服务的时候会报错

package main

import (
    service "LearnTest/rpc"
    "LearnTest/token"
    "context"
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/grpclog"
    "google.golang.org/grpc/reflection"
    "net"
)

// LoggingInterceptor 日志拦截器
func LoggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler) (interface{}, error) {
    fmt.Printf("request gRPC method: %s, %v\n", info.FullMethod, req)
    resp, err := handler(ctx, req)
    fmt.Printf("response gRPC method: %s, %v\n", info.FullMethod, resp)
    return resp, err
}

type login struct {
    //通过结构体嵌套方式默认实现这个方法
    service.UnimplementedTokenServiceServer
}

func (login *login) Login(ctx context.Context, request *service.LoginRequest) (resp *service.LoginResp, err error) {
    if request.Username == "qqw" && request.Password == "qqw" {
        token := token.CreateToken(request.Username)
        return &service.LoginResp{Status: "200", Token: token}, nil
    }
    return &service.LoginResp{Status: "401", Token: ""}, nil
}

func (login *login) Greet(ctx context.Context, request *service.PingMessage) (resp *service.PingMessage, err error) {
    auth := token.CheckAuth(ctx)
    return &service.PingMessage{Greet: auth}, nil
}

func main() {

    lis, err := net.Listen("tcp", ":2333")
    if err != nil {
        fmt.Printf("failed to listen: %v\n", err)
        return
    }

    // TLS认证
    creds, err := credentials.NewServerTLSFromFile("/Users/liuke/GolandProjects/LearnTest/ssl/server.crt", "/Users/liuke/GolandProjects/LearnTest/ssl/server.key")
    if err != nil {
        grpclog.Fatalf("Failed to generate credentials %v", err)
    }

    //开启TLS认证 注册拦截器
    s := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(LoggingInterceptor)) // 创建gRPC服务器
    service.RegisterTokenServiceServer(s, &login{})                                   // 在gRPC服务端注册服务

    reflection.Register(s) //在给定的gRPC服务器上注册服务器反射服务
    // Serve方法在list上接受传入连接,为每个连接创建一个ServerTransport和server的goroutine。
    // 该goroutine读取gRPC请求,然后调用已注册的处理程序来响应它们。
    fmt.Println("Listening on port 2333...")
    err = s.Serve(lis)

}

接着编写客户端代码

package main

import (
    service "LearnTest/rpc"
    Token "LearnTest/token"
    "context"
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/grpclog"
)

func main() {
    var err error
    var opts []grpc.DialOption

    // TLS连接
    creds, err := credentials.NewClientTLSFromFile("/Users/liuke/GolandProjects/LearnTest/ssl/ca.crt", "www.qqw.com")
    if err != nil {
        grpclog.Fatalf("Failed to create TLS credentials %v", err)
        return
    }
    opts = append(opts, grpc.WithTransportCredentials(creds))

    //连接服务端
    conn, err := grpc.Dial(":2333", opts...)
    if err != nil {
        fmt.Printf("failed to connect: %v\n", err)
        return
    }
    defer conn.Close()

    c := service.NewTokenServiceClient(conn)
    // 调用登录方法,获得token
    r, err := c.Login(context.Background(), &service.LoginRequest{Username: "qqw1", Password: "qqw"})
    if err != nil {
        fmt.Printf("could not login: %v\n", err)
        return
    }
    requestToken := new(Token.AuthToken)
    requestToken.Token = r.Token

    //连接服务端
    conn, err = grpc.Dial(":2333", grpc.WithTransportCredentials(creds),
        grpc.WithPerRPCCredentials(requestToken))
    if err != nil {
        fmt.Printf("failed to connect: %v\n", err)
        return
    }
    defer conn.Close()
    
    c = service.NewTokenServiceClient(conn)
    greet, err := c.Greet(context.Background(), &service.PingMessage{Greet: "调用greet"})
    if err != nil {
        fmt.Printf("could not greet: %v\n", err)
        return
    }

    fmt.Printf("Greeting: %s, %s\n", r.Token, greet)
}

正常登录请求返回200,登录认证不通过返回401

参考

https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIzNDcwNjQxMg==&action=getalbum&album_id=1705166100159594498&scene=173&from_msgid=2247484803&from_itemidx=1&count=3&nolastread=1#wechat_redirect

https://www.bilibili.com/video/BV1S24y1U7Xp?vd_source=81993e830ebcf48a17d58eb0b7f3a7c2