grpc详解及在Go中的应用

一、http与http2及https

在介绍 grpc 之前先了解下 http、http2、https 的区别,因为 grpc 是基于 http2 协议标准设计开发的

1. 网络协议结构

ISO/OSI七层网络协议结构: 应用层、表示层、会话层、传输层、网络层、数据链路层、物理层 (自上而下)

TCP/IP传输协议层级结构: 应用层、运(传)输层、网际(络)层、网络接口层(又称链路层)

2. http

http 协议是位于 TCP/IP 体系结构中的应用层协议,它是万维网的数据通信的基础,也是最常用的协议,默认端口是80,默认版本就是 http1.1

http的连接建立过程:

  1. 浏览器请求,先通过dns解析域名的ip地址

  2. 通过tcp三次握手建立tcp连接

  3. 发起http请求

  4. 目标服务器接受到http请求并处理

  5. 目标服务器往浏览器发送http响应

  6. 浏览器解析并渲染页面

http 是面向报文传输,报文由请求行、首部、实体主体组成,它们之间由CRLF分隔开

3. https

https 是最流行的 http 安全形式,由网景公司首创,所有主流的浏览器和服务器都支持此协议

使用 https 时,所有的 http 请求和响应数据在发送之前,都要进行加密(对称加密和非对称加密)。加密可以使用 SSL/TLS

4. http2

http2 是 http1.x 的扩展,而非替代,所以 http 的语义不变,提供的功能不变,http 方法、状态码、URL 和首部字段等这些核心概念也不变

之所以要递增一个大版本到 2.0,主要是因为它改变了客户端与服务器之间交换数据 的方式。HTTP2.0 增加了新的二进制分帧数据层,而这一层并不兼容之前的 HTTP1.x 服务器及客户端

现在的主流浏览器 HTTP2.0 的实现都是基于 SSL/TLS 的,也就是说使用 HTTP/2 的网站都是 HTTPS 协议的

HTTP/1.1 是以文本分隔的,解析 HTTP/1.1 需要不断地读入字节,直到遇到分隔符 CRLF 为止,如果客户端想发送多个并行的请求,那么必须使用多个 TCP 连接

HTTP/2 是基于帧的协议,所有的请求和响应都在同一个 TCP 连接上发送:客户端和服务器把 HTTP 消息分解成多个帧,然后乱序发送,最后在另一端再根据流 ID 重新组合起来。

二、grpc详解

1. 介绍

grpc 是一个高性能、开源、通用的 rpc 框架,由Google推出,基于HTTP2协议标准设计开发,默认采用 Protocol Buffers(protobuf) 数据序列化协议,支持多种开发语言,可在任意环境运行

官网:https://grpc.io

官方文档:https://grpc.io/docs/languages

中文文档:https://doc.oschina.net/grpc

2. go安装grpc
  1. 安装两个组件

    go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
    
    go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
    

    安装成功后,$GOPATH/bin 目录下,会生成两个文件。如果没有配置系统环境变量,需要将此目录配置下环境变量,否则会影响后续命令执行

  2. 安装 Protocol Buffers

    下载地址:https://github.com/protocolbuffers/protobuf/releases

    根据自己的操作系统去下载安装,并且配置环境变量。配置好后,可以使用 protoc --version 进行测试

  3. 示例参考(可选)

    官方文档上提供了示例参考,可下载查看

    git clone -b v1.58.2 --depth 1 https://github.com/grpc/grpc-go
    
    cd grpc-go/examples/helloworld
    
3. grpc支持的四种类型服务方法

具体解释查看官方文档

  • 简单 RPC

  • 服务端流 RPC

  • 客户端流 RPC

  • 双向流 RPC

4. 一个简单rpc的应用示例

客户端向服务端发送单个请求,取回单个响应,就像普通的函数调用一样

  1. 在项目中集成 grpc 包

    mkdir grpc
    
    cd grpc
    
    go mod init
    
    go get google.golang.org/grpc
    
  2. 创建一个 .proto 的文件

    // 指定当前proto语法的版本, 有2和3
    syntax = "proto3";
    
    // option go_package = "path;name";
    // path: 生成的go文件存放地址, .表示当前目录
    // name: 生成go文件的包名
    option go_package = ".;hello";
    
    // 指定生成的go文件的package
    package hello;
    
    // 定义服务
    service Hello {
      // 定义方法
      rpc SayHello (HelloRequest) returns (HelloResponse) {}
    }
    
    // 定义请求
    // 数字为序列号,表示这个变量在message中的位置
    message HelloRequest {
      string name = 1;
    }
    
    // 定义响应
    message HelloResponse {
      int32 code = 1;
      string message = 2;
      bytes data = 3;
    }
    
  3. 生成 .pb.go 文件

    protoc --go_out=. --go-grpc_out=. hello.proto
    // 或者
    protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative hello.proto
    

    执行成功会在指定目录下生成两个文件 hello.pb.gohello_grpc.pb.go

  4. 编写服务端代码

    package main
    
    import (
        "context"
        pb "design/grpc/proto"
        "fmt"
        "google.golang.org/grpc"
        "net"
    )
    
    type helloServer struct {
        pb.UnimplementedHelloServer
    }
    
    func (h *helloServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
        resp := &pb.HelloResponse{
            Code:    200,
            Message: "ok",
            Data:    []byte("hello : " + req.Name),
        }
    
        return resp, nil
    }
    
    func main() {
        // 监听地址和端口
        listen, err := net.Listen("tcp", ":8001")
        if err != nil {
            fmt.Println(err)
        }
        // 创建grpc服务
        ser := grpc.NewServer()
        // 注册服务
        pb.RegisterHelloServer(ser, new(helloServer))
        // 启动服务
        err = ser.Serve(listen)
        if err != nil {
            fmt.Println(err)
        }
    }
    
  5. 编写客户端代码

    package main
    
    import (
        "context"
        pb "design/grpc/proto"
        "fmt"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
    )
    
    func main() {
        conn, err := grpc.Dial("127.0.0.1:8001", grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
            fmt.Println(err)
        }
        defer conn.Close()
    
        client := pb.NewHelloClient(conn)
        resp, err := client.SayHello(context.TODO(), &pb.HelloRequest{Name: "ricky"})
        if err != nil {
            mt.Println(err)
        }
    
        fmt.Println(resp) // code:200  message:"ok"  data:"hello : ricky"
    }
    

三、protobuf详解

1. 介绍

Protocol Buffers 简称 protobuf,是谷歌开发的一款无关平台、语言、可扩展、轻量级高效的序列化结构的数据格式,用于将自定义数据结构序列化成字节流和将字节流反序列化为数据结构。可以把它当成一个代码生成工具以及序列化工具,不同应用之间互相通信的数据交换格式,只要实现相同的协议格式,即后缀为proto文件被编译成不同的语言版本,加入各自的项目中,这样不同的语言可以解析其它语言通过 Protobuf 序列化的数据。

2. 优势
  • 序列化后体积比JSON和XML小,适合网络传输

  • 序列化反序列化速度快,比JSON的处理速度快

  • 消息格式升级和兼容性较好

  • 支持跨平台多语言

3. message语法

message定义一个消息类型/数据结构,和 Java的class,Go语言中的struct类似。命名采用驼峰命名方式

在一个proto文件中message可以定义多个,也支持嵌套及使用import引入其他proto文件的message

  • 字段格式:字段规则 | 数据类型 | 字段名称 | = | 字段标识号 | [字段默认值]

  • 字段规则:

    • required:消息体中必填字段,不设置会导致编码解码的异常。不设置默认这个

    • optional:可选字段,值可以存在,也可以为空

    • repeated:可重复字段,可以存在多个(包括0个)。重复的值的顺序会被保留,对应 java 的数组或者go语言的slice

  • 数据类型:

    常见的映射关系如下

    proto C++ Java Python Go PHP
    double double double float float64 float
    float float float float float32 float
    int32 int32 int int int32 integer
    int64 int64 long ing/long[3] int64 integer/string[5]
    uint32 uint32 int[1] int/long[3] uint32 integer
    uint64 uint64 long[1] int/long[3] uint64 integer/string[5]
    sint32 int32 int intj int32 integer
    sint64 int64 long int/long[3] int64 integer/string[5]
    fixed32 uint32 int[1] int uint32 integer
    fixed64 uint64 long[1] int/long[3] uint64 integer/string[5]
    sfixed32 int32 int int int32 integer
    sfixed64 int64 long int/long[3] int64 integer/string[5]
    bool bool boolean bool bool bool
    string string String str/unicode string string
    bytes string ByteString str []byte string
  • 标识号:在消息的定义中,每个字段等号后面都有唯一的标识号,用于在反序列化过程中识别各个字段的,一旦开始使用就不能改变。标识号从整数1开始,依次递增,每次增加1,标识号的范围为1~2^29 – 1,其中 [19000-19999] 为 Protobuf 协议预留字段,开发者不建议使用该范围的标识号;一旦使用,在编译时Protoc编译器会报出警告

  • 默认值:protobuf3删除了protobuf2中用来设置默认值的default关键字,为各类型定义的默认值

    类型 默认值
    bool false
    数值 0
    string ""
    enum 第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0;
    message 不是null,而是DEFAULT_INSTANCE
  • 更新规则:message定义以后如果需要进行修改,为了保证之前的序列化和反序列化能够兼容新的message,message的修改需要满足以下规则

    • 不可以修改已存在域中的标识号

    • 所有新增添的域必须是 optional 或者 repeated

    • 非required域可以被删除,但是这些被删除域的标识号不可以再次被使用

    • 非required域可以被转化,转化时可能发生扩展或者截断,此时标识号和名称都是不变的

    • sint32和sint64是相互兼容的

    • fixed32兼容sfixed32;fixed64兼容sfixed64

    • optional兼容repeated,发送端发送repeated域,用户使用optional域读取, 将会读取repeated域的最后一个元素

4. 序列化原理

在序列化时,Protobuf 按照TLV的格式序列化每一个字段,T即Tag,也叫Key;V是该字段对应的值value;L是Value的长度,如果一个字段是整形,这个L部分会省略

序列化后的Value是按原样保存到字符串或者文件中,Key按照一定的转换条件保存起来,序列化后的结果就是 KeyValueKeyValue…依次类推的样式

四、grpc认证

grpc认证方式与传统的rpc认证方式无差别,因为grpc本质仍然是rpc。grpc默认内置了两种认证方式:

  • SSL/TLS认证

  • Token认证

1. SSL/TLS认证实现
  1. 第一步,安装OpenSSL

    官方下载地址: https://www.openssl.org/source

    windows下载:http://slproweb.com/products/Win32OpenSSL.html

    我这里下载的win64 v3.1.4版本,安装完成后记得配置下环境变量

  2. 第二步,配置证书

    配置 ca.conf 证书文件

    [ req ]
    default_bits = 4096
    distinguished_name = req_distinguished_name
    [ req_distinguished_name ]
    countryName = Country Name (2 letter code)
    countryName_default = CN
    stateOrProvinceName = State or Province Name (full name)
    stateOrProvinceName_default = beijing
    localityName = Locality Name (eg, city)
    localityName_default = beijing
    organizationName = Organization Name (eg, company)
    organizationName_default = ricky
    commonName = Common Name (e.g. server FQDN or YOUR name)
    commonName_default = ricky
    commonName_max = 64
    

    配置 server.conf 证书文件

    [ req ]
    default_bits = 2048
    distinguished_name = req_distinguished_name
    req_extensions = req_ext
    [ req_distinguished_name ]
    countryName = Country Name (2 letter code)
    countryName_default = CN
    stateOrProvinceName = State or Province Name (full name)
    stateOrProvinceName_default = beijing
    localityName = Locality Name (eg, city)
    localityName_default = beijing
    organizationName = Organization Name (eg, company)
    organizationName_default = ricky
    commonName = Common Name (e.g. server FQDN or YOUR name)
    commonName_default = ricky
    commonName_max = 64
    [ req_ext ]
    subjectAltName = @alt_names
    [alt_names]
    DNS.1 = grpc
    IP = 127.0.0.1
    

    生成CA根证书

    openssl genrsa -out ca.key 4096
    
    openssl req -new -sha256 -out ca.csr -key ca.key -config ca.conf
    
    openssl x509 -req -days 3650 -in ca.csr -signkey ca.key -out ca.crt
    

    生成Server证书

    openssl genrsa -out server.key 2048
    
    openssl req -new -sha256 -out server.csr -key server.key -config server.conf
    
    openssl x509 -req -in ca.csr -out ca.crt -CA ca.crt -CAkey ca.key –CAcreateserial
    
    openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -in server.csr -out server.pem -extensions req_ext -extfile server.conf
    

    全部生成完后,目录下会存在这些文件

  3. 第三步,go中实现秘钥的配置

    修改服务端

     package main
    
    import (
        "context"
        pb "design/grpc/proto"
        "fmt"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials"
        "net"
    )
    
    type helloServer struct {
        pb.UnimplementedHelloServer
    }
    
    func (h *helloServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
        resp := &pb.HelloResponse{
            Code:    200,
            Message: "ok",
            Data:    []byte("hello : " + req.Name),
        }
    
        return resp, nil
    }
    
    func main() {
        // 监听地址和端口
        listen, err := net.Listen("tcp", ":8001")
        if err != nil {
            fmt.Println(err)
        }
    
        // 新增一步,与之前区别的地方
        creds, err := credentials.NewServerTLSFromFile("./cert/server.pem", "./cert/server.key")
        if err != nil {
            fmt.Println(err)
        }
    
        // 创建grpc服务
        ser := grpc.NewServer(grpc.Creds(creds))
        // 注册服务
        pb.RegisterHelloServer(ser, new(helloServer))
        // 启动服务
        err = ser.Serve(listen)
        if err != nil {
            fmt.Println(err)
        }
    }
    

    修改客户端

    package main
    
    import (
        "context"
        pb "design/grpc/proto"
        "fmt"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials"
    )
    
    func main() {
        // 新增一步,与之前区别的地方
        creds, err := credentials.NewClientTLSFromFile("./cert/server.pem", "grpc")
        if err != nil {
            fmt.Println(err)
        }
    
        conn, err := grpc.Dial("127.0.0.1:8001", grpc.WithTransportCredentials(creds))
        if err != nil {
            fmt.Println(err)
        }
        defer conn.Close()
    
        client := pb.NewHelloClient(conn)
        resp, err := client.SayHello(context.TODO(), &pb.HelloRequest{Name: "ricky"})
        if err != nil {
            mt.Println(err)
        }
    
        fmt.Println(resp) // code:200  message:"ok"  data:"hello : ricky"
    }
    

五、grpc拦截器

1. 介绍

grpc 服务端和客户端都提供了拦截器(interceptor)功能,功能类似中间件(middleware),很适合在这里处理验证、日志等流程

2. 案例

使用上述SSL/TLS认证的grpc代码,加入拦截器再实现一个自定义token验证

服务端:

package main

import (
    "context"
    pb "design/grpc/proto"
    "errors"
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "google.golang.org/grpc/metadata"
    "net"
)

type helloServer struct {
    pb.UnimplementedHelloServer
}

func (h *helloServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    resp := &pb.HelloResponse{
        Code:    200,
        Message: "ok",
        Data:    []byte("hello : " + req.Name),
    }

    return resp, nil
}

func main() {
    // 监听地址和端口
    listen, err := net.Listen("tcp", ":8001")
    if err != nil {
        fmt.Println(err)
    }

    // 新增一步,与之前区别的地方
    creds, err := credentials.NewServerTLSFromFile("./cert/server.pem", "./cert/server.key")
    if err != nil {
        fmt.Println(err)
    }

    var opt []grpc.ServerOption

    opt = append(opt, grpc.Creds(creds))                  // 写入SSL/TLS认证
    opt = append(opt, grpc.UnaryInterceptor(interceptor)) // 写入拦截器
    // 创建grpc服务
    ser := grpc.NewServer(opt...)
    // 注册服务
    pb.RegisterHelloServer(ser, new(helloServer))
    // 启动服务
    err = ser.Serve(listen)
    if err != nil {
        fmt.Println(err)
    }
}

// 拦截器方法
func interceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
    err = auth(ctx)
    if err != nil {
        return nil, err
    }

    return handler(ctx, req)
}

// 自定义验证
func auth(ctx context.Context) error {
    md, ok := metadata.FromIncomingContext(ctx)
    fmt.Println(md)
    if !ok {
        return errors.New("信息参数有误")
    }

    var token string

    if val, ok := md["token"]; ok {
        fmt.Println(val)
        token = val[0]
    }
    if token != "123456" {
        return errors.New("token 认证失败")
    }

    return nil
}

客户端:

package main

import (
    "context"
    pb "design/grpc/proto"
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

type credentialsToken struct{}

// GetRequestMetadata 生成自定义验证的信息,如token
func (c *credentialsToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "token": "123456", // 具体token生成就不写了,直接写死一个
    }, nil
}

// RequireTransportSecurity 是否开启SSL/TLS认证
// true:则两个认证方式同时使用;
// false:不开启SSL/TLS认证,只使用自定义认证
func (c *credentialsToken) RequireTransportSecurity() bool {
    return true
}

func main() {
    // 新增一步,与之前区别的地方
    creds, err := credentials.NewClientTLSFromFile("./cert/server.pem", "grpc")
    if err != nil {
        fmt.Println(err)
    }

    var opt []grpc.DialOption

    opt = append(opt, grpc.WithPerRPCCredentials(new(credentialsToken))) // 写入自定义认证
    opt = append(opt, grpc.WithTransportCredentials(creds))              // 写入SSL/TLS认证
    opt = append(opt, grpc.WithUnaryInterceptor(cInterceptor))           // 写入拦截器

    conn, err := grpc.Dial("127.0.0.1:8001", opt...)
    if err != nil {
        fmt.Println(err)
    }
    defer conn.Close()

    client := pb.NewHelloClient(conn)
    resp, err := client.SayHello(context.TODO(), &pb.HelloRequest{Name: "ricky"})
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(resp)
}

// 拦截器方法
func cInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    fmt.Println("client interceptor")
    return invoker(ctx, method, req, reply, cc, opts...)
}

/**
token失败,会输出:
client interceptor
rpc error: code = Unknown desc = token 认证失败
<nil>
*/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容