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閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容