Go进阶之rpc和grpc-CSDN博客

Go环境安装

1windows

下载直接安装

2linux

下载解压
tar -xvf go1.15.3.linux-amd64.tar.gz

配置环境变量

vim ~/.bashrc
export GOROOT=/root/go
export GOPATH=/root/projects/go
export PATH=$PATH:$GOROOT/bin:$GPPATH/bin

编辑保存并退出vim后记得把这些环境载⼊source ~/.bashrc

设置代理加速

go env -w GOPROXY=https://goproxy.io,direct
go env -w GO111MODULE=on

goland 的安装
官⽅地址下载安装即可

goland 的配置 goimports 和 go fmt
go fmt 和 goimports 保存后⾃动格式化
settings -> tools -> file watchers

设置 keymap 为 eclipse 模式

goland 中通过 file->settings->keymap 选择 eclipse(如果没有 eclipse 选型则通过 plugins 安eclipse)

go语言编码规范

  1. 代码规范不是强制的也就是你不遵循代码规范写出来的代码运⾏也是完全没有问题的
  2. 代码规范⽬的是⽅便团队形成⼀个统⼀的代码⻛格提⾼代码的可读性规范性和统⼀性。本规范将从命名规范注释规范代码⻛格和 Go 语⾔提供的常⽤的⼯具这⼏个⽅⾯做⼀个说明。
  3. 规范并不是唯⼀的也就是说理论上每个公司都可以制定⾃⼰的规范不过⼀般来说整体上规范差异不会很⼤。

命名是代码规范中很重要的⼀部分统⼀的命名规则有利于提⾼的代码的可读性好的命名仅仅通过命名就可以获取到⾜够多的信息。

a. 当命名包括常量、变量、类型、函数名、结构字段等等以⼀个⼤写字⺟开头如Group1那么使⽤这种形式的标识符的对象就可以被外部包的代码所使⽤客户端程序需要先导⼊这个包这被称为导出像⾯向对象语⾔中的 public
b. 命名如果以⼩写字⺟开头则对包外是不可⻅的但是他们在整个包的内部是可⻅并且可⽤的像⾯向对象语⾔中的 private

1.1 包名package

保持 package 的名字和⽬录保持⼀致尽量采取有意义的包名简短有意义尽量和标准库不要冲突。包名应该为⼩写单词不要使⽤下划线或者混合⼤⼩写。

package model
package main

1.2 ⽂件名

尽量采取有意义的⽂件名简短有意义应该为⼩写单词使⽤下划线分隔各个单词。

1.3 结构体命名

  • 采⽤驼峰命名法⾸字⺟根据访问控制⼤写或者⼩写
  • struct 申明和初始化格式采⽤多⾏例如下⾯
type User struct{
	 Username string
	 Email   string
}
u := User{
	 Username: "bobby",
	 Email:  "bobby@imooc.com",
}

1.4 接⼝命名

  • 命名规则基本和上⾯的结构体类型
  • 单个函数的结构名以 “er” 作为后缀例如 Reader , Writer 。
type Reader interface {
   Read(p []byte) (n int, err error)
}

1.5 变量命名

和结构体类似变量名称⼀般遵循驼峰法⾸字⺟根据访问控制原则⼤写或者⼩写但遇到特有名词时需要遵循以下规则

  • 如果变量为私有且特有名词为⾸个单词则使⽤⼩写如 apiClient
  • 其它情况都应当使⽤该名词原有的写法如 APIClient、repoID、UserID

错误示例UrlArray应该写成 urlArray 或者 URLArray

  • 若变量类型为 bool 类型则名称应以 Has, Is, Can 或 Allow 开头
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool

1.6 常量命名

常量均需使⽤全部⼤写字⺟组成并使⽤下划线分词

如果是枚举类型的常量需要先创建相应类型

type Scheme string
const (
	 HTTP Scheme = "http"
	 HTTPS Scheme = "https"
)

Go 提供 C ⻛格的 /* */ 块注释和 C ++ ⻛格的 // ⾏注释。⾏注释是常态块注释主要显示为包注释但在表达式中很有⽤或禁⽤⼤量代码。

  • 单⾏注释是最常⻅的注释形式你可以在任何地⽅使⽤以 // 开头的单⾏注释
  • 多⾏注释也叫块注释均已以 /* 开头并以 */ 结尾且不可以嵌套使⽤多⾏注释⼀般⽤于包的⽂档描述或注释成块的代码⽚段

go 语⾔⾃带的 godoc ⼯具可以根据注释⽣成⽂档⽣成可以⾃动⽣成对应的⽹站golang.org 就是使⽤ godoc⼯具直接⽣成的注释的质量决定了⽣成的⽂档的质量。每个包都应该有⼀个包注释在 package ⼦句之前有⼀个块注释。对于多⽂件包包注释只需要存在于⼀个⽂件中任何⼀个都可以。包评论应该介绍包并提供与整个包相关的信息。它将⾸先出现在 godoc ⻚⾯上并应设置下⾯的详细⽂档。

2.1 包注释

每个包都应该有⼀个包注释⼀个位于 package ⼦句之前的块注释或⾏注释。包如果有多个 go ⽂件只需要出现在⼀个 go ⽂件中⼀般是和包同名的⽂件即可。 包注释应该包含下⾯基本信息 (请严格按照这个顺序简介创建⼈创建时间

  • 包的基本简介包名简介
  • 创建者格式 创建⼈ rtx 名
  • 创建时间格式创建时间 yyyyMMdd

2.2 结构接⼝注释

每个⾃定义的结构体或者接⼝都应该有注释说明该注释对结构进⾏简要介绍放在结构体定义的前⼀⾏格式为 结构体名 结构体说明。同时结构体内的每个成员变量都要有说明该说明放在成员变量的后⾯注意对⻬实例如下

type User struct{
	 Username string
	 Email   string
}

2.3 函数⽅法注释

每个函数或者⽅法结构体或者接⼝下的函数称为⽅法都应该有注释说明函数的注释应该包括三个⽅⾯严格按照此顺序撰写

  • 简要说明格式说明以函数名开头“” 分隔说明部分
  • 参数列表每⾏⼀个参数参数名开头“” 分隔说明部分
  • 返回值 每⾏⼀个返回值
func NewAttrModel(ctx *common.Context) *AttrModel {
}

2.4 代码逻辑注释

对于⼀些关键位置的代码逻辑或者局部较为复杂的逻辑需要有相应的逻辑说明⽅便其他开发者阅读该段代码实例如下

xxxxx
xxxxxxx
xxxxxxx

2.5 注释⻛格

统⼀使⽤中⽂注释对于中英⽂字符之间严格使⽤空格分隔 这个不仅仅是中⽂和英⽂之间英⽂和中⽂标点之间也都要使⽤空格分隔例如
上⾯ Redis 、 id 、 DB 和其他中⽂字符之间都是⽤了空格分隔。建议全部使⽤单⾏注释
和代码的规范⼀样单⾏注释不要过⻓禁⽌超过 120 字符。
import 在多⾏的情况下goimports 会⾃动帮你格式化但是我们这⾥还是规范⼀下 import 的⼀些规范如果你在⼀个⽂件⾥⾯引⼊了⼀个 package还是建议采⽤如下格式
如果你的包引⼊了三种类型的包标准库包程序内部包第三⽅包建议采⽤如下⽅式进⾏组织你的包

import (
	 "encoding/json"
	 "strings"
	 
	 "myproject/models"
	 "myproject/controller"
	 "myproject/utils"
	 
	 "github.com/astaxie/beego"
	 "github.com/go-sql-driver/mysql"
) 

有顺序的引⼊包不同的类型采⽤空格分离第⼀种实标准库第⼆是项⽬包第三是第三⽅包。在项⽬中不要使⽤相对路径引⼊包

import../net”
import “github.com/repo/proj/src/net”

但是如果是引⼊本项⽬中的其他包最好使⽤相对路径。

  • 错误处理的原则就是不能丢弃任何有返回 err 的调⽤不要使⽤ _ 丢弃必须全部处理。接收到错误要么返回 err或者使⽤ log 记录下来
  • 尽早 return⼀旦有错误发⽣⻢上返回
  • 尽量不要使⽤ panic除⾮你知道你在做什么
  • 错误描述如果是英⽂必须为⼩写不需要标点结尾
  • 采⽤独⽴的错误流进⾏处理
if err != nil {
 
} else {
 
}
if err != nil {
 
 return
}

远程过程调⽤带来的新问题

在远程调⽤时我们需要执⾏的函数体是在远程的机器上的也就是说add 是在另⼀个进程中执⾏的。这就带来了⼏个新问题

  1. Call ID 映射。我们怎么告诉远程机器我们要调⽤ add⽽不是 sub 或者 Foo 呢在本地调⽤中函数体是直接通过函数指针来指定的我们调⽤ add编译器就⾃动帮我们调⽤它相应的函数指针。但是在远程调⽤中函数指针是不⾏的因为两个进程的地址空间是完全不⼀样的。所以在 RPC 中所有的函数都必须有⾃⼰的⼀个 ID。这个 ID 在所有进程中都是唯⼀确定的。客户端在做远程过程调⽤时必须附上这个 ID。然后我们还需要在客户端和服务端分别维护⼀个 {函数 <–> Call ID} 的对应表。两者的表不⼀定需要完全相同但相同的函数对应的 Call ID 必须相同。当客户端需要进⾏远程调⽤时它就查⼀下这个表找出相应的 Call ID然后把它传给服务端服务端也通过查表来确定客户端需要调⽤的函数然后执⾏相应函数的代码。
  2. 序列化和反序列化。客户端怎么把参数值传给远程的函数呢在本地调⽤中我们只需要把参数压到栈⾥然后让函数⾃⼰去栈⾥读就⾏。但是在远程过程调⽤时客户端跟服务端是不同的进程不能通过内存来传递参数。甚⾄有时候客户端和服务端使⽤的都不是同⼀种语⾔⽐如服务端⽤ C++客户端⽤ Java 或者Python。这时候就需要客户端把参数先转成⼀个字节流传给服务端后再把字节流转成⾃⼰能读取的格式。这个过程叫序列化和反序列化。同理从服务端返回的值也需要序列化反序列化的过程。
  3. ⽹络传输。远程调⽤往往⽤在⽹络上客户端和服务端是通过⽹络连接的。所有的数据都需要通过⽹络传输因此就需要有⼀个⽹络传输层。⽹络传输层需要把 Call ID 和序列化后的参数字节流传给服务端然后再把序列化后的调⽤结果传回客户端。只要能完成这两者的都可以作为传输层使⽤。因此它所使⽤的协议其实是不限的能完成传输就⾏。尽管⼤部分 RPC 框架都使⽤ TCP 协议但其实 UDP 也可以⽽ gRPC ⼲脆就⽤了 HTTP2。Java 的 Netty 也属于这层的东⻄。

解决了上⾯三个机制就能实现 RPC 了具体过程如下

client 端解决的问题

  1. 将这个调⽤映射为Call ID。这⾥假设⽤最简单的字符串当Call ID的⽅法
  2. 将Call IDa和b序列化。可以直接将它们的值以⼆进制形式打包
  3. 把2中得到的数据包发送给ServerAddr这需要使⽤⽹络传输层
  4. 等待服务器返回结果
  5. 如果服务器调⽤成功那么就将结果反序列化并赋给total

server 端解决的问题

  1. 在本地维护⼀个Call ID到函数指针的映射call_id_map可以⽤dict完成
  2. 等待请求包括多线程的并发处理能⼒
  3. 得到⼀个请求后将其数据包反序列化得到Call ID
  4. 通过在call_id_map中查找得到相应的函数指针
  5. 将a和rb反序列化后在本地调⽤add函数得到结果
  6. 将结果序列化后通过⽹络返回给Client

要实现⼀个 RPC 框架其实只需要按以上流程实现就基本完成了。

其中

  • Call ID 映射可以直接使⽤函数字符串也可以使⽤整数 ID。映射表⼀般就是⼀个哈希表。
  • 序列化反序列化可以⾃⼰写也可以使⽤ Protobuf 或者 FlatBuffers 之类的。
  • ⽹络传输库可以⾃⼰写 socket或者⽤ asioZeroMQNetty 之类。

实际上真正的开发过程中除了上⾯的基本功能以外还需要更多的细节⽹络错误、流量控制、超时和重试等。

REST 和 RPC 的差异

  • REST是 Representational State Transfer 的简写中⽂描述表述性状态传递是指某个瞬间状态的资源数据的快照包括资源数据的内容、表述格式 (XML、JSON) 等信息。

  • REST 是⼀种软件架构⻛格。这种⻛格的典型应⽤就是 HTTP。其因为简单、扩展性强的特点⽽⼴受开发者的⻘睐。

  • ⽽ RPC 呢是 Remote Procedure Call Protocol 的简写中⽂描述是远程过程调⽤它可以实现客户端像调⽤本地服务 (⽅法) ⼀样调⽤服务器的服务(⽅法)。

  • ⽽ RPC 可以基于 TCP/UDP也可以基于 HTTP 协议进⾏传输的按理说它和 REST 不是⼀个层⾯意义上的东⻄不应该放在⼀起讨论但是谁让 REST 这么流⾏呢它是⽬前最流⾏的⼀套互联⽹应⽤程序的 API 设计标准某种意义下我们说 REST 可以其实就是指代 HTTP 协议。

  • 从使⽤上来看HTTP 接⼝只关注服务提供⽅对于客户端怎么调⽤并不关⼼。接⼝只要保证有客户端调⽤时返回对应的数据就⾏了。⽽ RPC 则要求客户端接⼝保持和服务端的⼀致。

  • REST 是服务端把⽅法写好客户端并不知道具体⽅法。客户端只想获取资源所以发起 HTTP 请求⽽服务端接收到请求后根据 URI 经过⼀系列的路由才定位到⽅法上⾯去 RPC 是服务端提供好⽅法给客户端调⽤客户端需要知道服务端的具体类具体⽅法然后像调⽤本地⽅法⼀样直接调⽤它。

  • 从设计上来看RPC所谓的远程过程调⽤ 是⾯向⽅法的 REST所谓的 Representational state transfer 是⾯向资源的除此之外还有⼀种叫做 SOA所谓的⾯向服务的架构它是⾯向消息的。

接⼝调⽤通常包含两个部分序列化和通信协议。

  • 通信协议上⾯已经提及了REST 是 基于 HTTP 协议⽽ RPC 可以基于 TCP/UDP也可以基于 HTTP 协议进⾏传输的。
  • 常⻅的序列化协议有json、xml、hession、protobuf、thrift、text、bytes 等REST 通常使⽤的是 JSON 或者 XML⽽ RPC 使⽤的是 JSON-RPC或者 XML-RPC。

然后第⼆个问题为什么要采⽤ RPC 呢

那到底为何要使⽤ RPC单纯的依靠 RESTful API 不可以吗为什么要搞这么多复杂的协议

  • RPC 和 REST 两者的定位不同REST ⾯向资源更注重接⼝的规范因为要保证通⽤性更强所以对外最好通过REST。⽽ RPC ⾯向⽅法主要⽤于函数⽅法的调⽤可以适合更复杂通信需求的场景。RESTful API 客户端与服务端之间采⽤的是同步机制当发送 HTTP 请求时客户端需要等待服务端的响应。当然对于这⼀点是可以通过⼀些技术来实现异步的机制的。采⽤ RESTful API客户端与服务端之间虽然可以独⽴开发但还是存在耦合。⽐如客户端在发送请求的时必须知道服务器的地址且必须保证服务器正常⼯作。⽽ rpc + ralbbimq 中间件可以实现低耦合的分布式集群架构。

  • 说了这么多我们该如何选择这两者呢我总结了如下两点供你参考

  • REST 接⼝更加规范通⽤适配性要求⾼建议对外的接⼝都统⼀成 REST。⽽组件内部的各个模块可以选择RPC⼀个是不⽤耗费太多精⼒去开发和维护多套的 HTTP 接⼝⼀个 RPC 的调⽤性能更⾼⻅下条从性能⻆度看由于 HTTP 本身提供了丰富的状态功能与扩展功能但也正由于 HTTP 提供的功能过多导致在⽹络传输时需要携带的信息更多从性能⻆度上讲较为低效。⽽ RPC 服务⽹络传输上仅传输与业务内容相关的数据传输数据更⼩性能更⾼。

为什么⼀定要 rpc不能只学 http 协议和 restful 协议吗

  1. rpc 可以基于 tcp 直接开发⾃⼰的协议这个是可以保持⻓连接的tcp 的传输效率⾼并且可以⼀直维持链接
  2. ⾃定义协议可以优化数据的传输

如果我们只是开发 web ⽹站或者⼀些服务的使⽤者 那么我们⽤ restful 看起来已经⾜够了但是 rpc 的这种模式在⼤量的服务中都有⽐如 redis 协议 rabbitmq 的 AMQP 协议 聊天软件的协议也就是说我们想要开发⼀个 redis 的客户端我们只需要⽤我们喜欢的语⾔实现 redis 定义的协议就⾏了这对于开发服务来说⾮常有⽤⼀般这种协议的价值在于我们⾃⼰开发的服务之间需要。通信的时候 - 那你会问了⾃⼰开发的组件之间协作
直接调⽤函数不就⾏了吗 - 对了有些⼈已经反映过来了 – 分布式系统分布式系统中⾮常常⽤ ⽐如openstack 中。 还有就是微服务所以掌握 rpc 开发对于进阶和分布式开发就变得⾮常重要。
http 协议 1.x ⼀般情况下⼀个来回就关闭连接虽然提供了 keep-alive 可以保持⻓连接但是依然不⽅便所以就出现了 http2.0 http2.0 基本上可以当做 tcp 协议使⽤了。所以后⾯讲解到的 grpc 就会使⽤ http2.0 开发。

rpc 开发的四⼤要素

RPC 技术在架构设计上有四部分组成分别是客户端、客户端存根、服务端、服务端存根。
客户端 (Client) 服务调⽤发起⽅也称为服务消费者。
客户端存根 (Client Stub) 该程序运⾏在客户端所在的计算机机器上主要⽤来存储要调⽤的服务器的地址另外该程序还负责将客户端请求远端服务器程序的数据信息打包成数据包通过⽹络发送给服务端Stub 程序其次还要接收服务端 Stub 程序发送的调⽤结果数据包并解析返回给客户端。
服务端 (Server) 远端的计算机机器上运⾏的程序其中有客户端要调⽤的⽅法。
服务端存根 (Server Stub) 接收客户 Stub 程序通过⽹络发送的请求消息数据包并调⽤服务端中真正的程序功能⽅法完成功能调⽤其次将服务端执⾏调⽤的结果进⾏数据处理打包发送给客户端 Stub 程序。

了解完了 RPC 技术的组成结构我们来看⼀下具体是如何实现客户端到服务端的调⽤的。实际上如果我们想要在⽹络中的任意两台计算机上实现远程调⽤过程要解决很多问题⽐如

  • 两台物理机器在⽹络中要建⽴稳定可靠的通信连接。
  • 两台服务器的通信协议的定义问题即两台服务器上的程序如何识别对⽅的请求和返回结果。也就是说两台计算机必须都能够识别对⽅发来的信息并且能够识别出其中的请求含义和返回含义然后才能进⾏处理。这其实就是通信协议所要完成的⼯作。

image.png

在上述图中通过 1-10 的步骤图解的形式说明了 RPC 每⼀步的调⽤过程。具体描述为
1、客户端想要发起⼀个远程过程调⽤⾸先通过调⽤本地客户端 Stub 程序的⽅式调⽤想要使⽤的功能⽅法名
2、客户端 Stub 程序接收到了客户端的功能调⽤请求将客户端请求调⽤的⽅法名携带的参数等信息做序列化操作并打包成数据包。
3、客户端 Stub 查找到远程服务器程序的 IP 地址调⽤ Socket 通信协议通过⽹络发送给服务端。
4、服务端 Stub 程序接收到客户端发送的数据包信息并通过约定好的协议将数据进⾏反序列化得到请求的⽅法名和请求参数等信息。
5、服务端 Stub 程序准备相关数据调⽤本地 Server 对应的功能⽅法进⾏并传⼊相应的参数进⾏业务处理。
6、服务端程序根据已有业务逻辑执⾏调⽤过程待业务执⾏结束将执⾏结果返回给服务端 Stub 程序。
7、服务端 Stub 程序 ** 将程序调⽤结果按照约定的协议进⾏序列化** 并通过⽹络发送回客户端 Stub 程序。
8、客户端 Stub 程序接收到服务端 Stub 发送的返回数据** 对数据进⾏反序列化操作** 并将调⽤返回的数据传递给客户端请求发起者。
9、客户端请求发起者得到调⽤结果整个 RPC 调⽤过程结束。

rpc 需要使⽤到的术语

通过上⽂⼀系列的⽂字描述和讲解我们已经了解了 RPC 的由来和 RPC 整个调⽤过程。我们可以看到 RPC 是⼀系列操作的集合其中涉及到很多对数据的操作以及⽹络通信。因此我们对 RPC 中涉及到的技术做⼀个总结和分析

1、动态代理技术 上⽂中我们提到的 Client Stub 和 Sever Stub 程序在具体的编码和开发实践过程中都是使⽤动态代理技术⾃动⽣成的⼀段程序。

2、序列化和反序列化 在 RPC 调⽤的过程中我们可以看到数据需要在⼀台机器上传输到另外⼀台机器上。在互联⽹上所有的数据都是以字节的形式进⾏传输的。⽽我们在编程的过程中往往都是使⽤数据对象因此想要在⽹络上将数据对象和相关变量进⾏传输就需要对数据对象做序列化和反序列化的操作。

序列化 把对象转换为字节序列的过程称为对象的序列化也就是编码的过程。
反序列化 把字节序列恢复为对象的过程称为对象的反序列化也就是解码的过程。
我们常⻅的 Json,XML 等相关框架都可以对数据做序列化和反序列化编解码操作。后⾯我们要学习的 Protobuf 协议这也是⼀种数据编解码的协议在 RPC 框架中使⽤的更⼴泛。

RPC 开发案例

Go 语⾔的 RPC 包的路径为 net/rpc也就是放在了 net 包⽬录下⾯。因此我们可以猜测该 RPC 包是建⽴在 net包基础之上的。在第⼀章 “Hello, World” ⾰命⼀节最后我们基于 http 实现了⼀个打印例⼦。下⾯我们尝试基于rpc 实现⼀个类似的例⼦。

package main
import (
	"net"
	"net/rpc"
)
type HelloService struct {}
func (s *HelloService) Hello(request string, reply *string) error {
	*reply = "hello "+ request
	return nil
}
func main(){
	_ = rpc.RegisterName("HelloService", &HelloService{})
	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
		panic("监听端⼝失败")
	}
	conn, err := listener.Accept()
	if err != nil {
		panic("建⽴链接失败")
	}
		rpc.ServeConn(conn)
}

其中 Hello ⽅法 必须满⾜Go语⾔的RPC规则 ⽅法 只能有两个可序列化的参数其中第⼆个参数是指针类型并且返回⼀个error类型同时必须是公开的⽅法 。
然后就可以将 HelloService 类型的对象注册为⼀个 RPC 服务(TCP RPC 服务)。
其中 rpc.Register 函数调⽤会将对象类型中所有满⾜ RPC 规则的对象⽅法注册为 RPC 函数所有注册的⽅法会放在 “HelloService” 服务空间之下。然后我们建⽴⼀个唯⼀的 TCP 链接并且通过 rpc.ServeConn 函数在该 TCP链接上为对⽅提供 RPC 服务。

func main() {
 client, err := rpc.Dial("tcp", "localhost:1234")
 if err != nil {
   log.Fatal("dialing:", err)
   }
 var reply string
 err = client.Call("HelloService.Hello", "hello", &reply)
 if err != nil {
   log.Fatal(err)
 }
 fmt.Println(reply)
}

⾸先是通过 rpc.Dial 拨号 RPC 服务然后通过 client.Call 调⽤具体的 RPC ⽅法。在调⽤ client.Call时 第⼀个参数是⽤点号链接的RPC服务名字和⽅法名字第⼆和第三个参数分别我们定义RPC⽅法的两个参数 。

grpc

gRPC 是⼀个⾼性能、开源和通⽤的 RPC 框架⾯向移动和 HTTP/2 设计。⽬前提供 C、Java 和 Go 语⾔版本分别是grpc, grpc-java, grpc-go. 其中 C 版本⽀持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# ⽀持.

grpc地址

image.png

protobuf

java 中的 dubbo dubbo/rmi/hessian messagepack 如果你懂了协议完全有能⼒⾃⼰去实现⼀个协议

  • 习惯⽤ Json、XML 数据存储格式的你们相信⼤多都没听过 Protocol Buffer
  • Protocol Buffer 其实 是 Google 出品的⼀种轻量 & ⾼效的结构化数据存储格式性能⽐ Json、XML 真的强太多
  • protobuf 经历了 protobuf2 和 protobuf3pb3 ⽐ pb2 简化了很多⽬前主流的版本是 pb3

image.png

grpc开发

安装

go get github.com/golang/protobuf/protoc-gen-go

如果是新版本 protoc 则需要安装下⾯两个

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1

注意安装过程中会提示说 go get 会慢慢被弃⽤不是错误只是提示go 的新版本依赖安装会慢慢弃⽤ go get⽅式安装以后⼀律采⽤ go install ⽅式安装第三⽅依赖

syntax = "proto3";
option go_package = ".;proto"; #新版本的protoc和protobuf这⾥应该写成 option go_package =
"./;proto";
service Greeter {
	rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
	string name = 1;
}
message HelloReply {
	string message = 1;
}
protoc -I . goods.proto --go_out=plugins=grpc:. 
#如果⼤家使⽤最新的protoc此处可能会报错说不⽀持这种⽤法所以可以使⽤下⾯的语句⽣成
protoc --go_out=. --go-grpc_out=. goods.proto

开发

package main
import (
	 "context"
	 "fmt"
	 "google.golang.org/grpc"
	 "grpc_demo/hello"
	 "net"
)
type Server struct {
}
func (s *Server) SayHello(ctx context.Context,request *hello.HelloRequest)
	(*hello.HelloReply,error){
	 return &hello.HelloReply{Message:"Hello "+request.Name},nil
}
func main() {
	 g := grpc.NewServer()
	 s := Server{}
	 hello.RegisterGreeterServer(g,&s)
	 lis, err := net.Listen("tcp", fmt.Sprintf(":8080"))
	 if err != nil {
	   	panic("failed to listen: "+err.Error())
	 }
	 g.Serve(lis)
}
package main
import (
	 "context"
	 "fmt"
	 "google.golang.org/grpc"
	 "grpc_demo/proto"
)
func main() {
 	conn,err := grpc.Dial("127.0.0.1:8080",grpc.WithInsecure())
	 if err!=nil{
	   panic(err)
 }
 defer conn.Close()
	 c := hello.NewGreeterClient(conn)
	 r,err := c.SayHello(context.Background(),&hello.HelloRequest{Name:"bobby"})
	 if err!=nil{
	   	panic(err)
	 }
	 fmt.Println(r.Message)
}

grpc进阶之protobuf参考文档

定义一个消息类型

先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了

syntax = "proto3";
	message SearchRequest {
	string query = 1;
	int32 page_number = 2;
	int32 result_per_page = 3;
}
  • 文件的第一行指定了你正在使用proto3语法如果你没有指定这个编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。
  • SearchRequest消息格式有3个字段在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。

指定字段类型

在上面的例子中所有字段都是标量类型两个整型page_number和result_per_page一个string类型query。当然你也可以为字段指定其他的合成类型包括枚举enumerations或其他消息类型。

分配标识号

正如你所见在消息定义中每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的一旦开始使用就不能够再改变。注[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记要为将来有可能添加的、频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始最大到2^29 - 1, or 536,870,911。不可以使用其中的[1900019999] (从FieldDescriptor::kFirstReservedNumber 到FieldDescriptor::kLastReservedNumber) 的 标 识 号 Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号编译时就会报警。同样你也不能使用早期保留的标识号。

指定字段规则

所指定的消息字段修饰符必须是如下之一

  • singular一个格式良好的消息应该有0个或者1个这种字段但是不能超过1个。
  • repeated在一个格式良好的消息中这种字段可以重复任意多次包括0次。重复的值的顺序会被保留。在proto3中repeated的标量域默认情况虾使用packed。

添加更多消息类型

在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候这一点特别有用——例如如果想定义与SearchResponse消息类型对应的回复消息格式的话你可以将它添加到相同的.proto文件中如

message SearchRequest {
	string query = 1;
	int32 page_number = 2;
	int32 result_per_page = 3;
}
message SearchResponse {
	...
}

添加注释

向.proto文件添加注释可以使用C/C++/java风格的双斜杠// 语法格式如

message SearchRequest {
	string query = 1;
	int32 page_number = 2; // Which page number do we want?
	int32 result_per_page = 3; // Number of results to return per page.
}

grpc的metadata

  1. 新建metadata

MD 类型实际上是mapkey是stringvalue是string类型的slice。

type MD map[string][]string 

创建的时候可以像创建普通的map类型一样使用new关键字进行创建

/第⼀种⽅式
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
//第⼆种⽅式 key不区分⼤⼩写会被统⼀转成⼩写。
md := metadata.Pairs(
	"key1", "val1",
	"key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
	"key2", "val2",
)
  1. 发送metadata
md := metadata.Pairs("key", "val")
// 新建⼀个有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 单向 RPC
response, err := client.SomeRPC(ctx, someRequest)
  1. 接收metadata
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeRespon
	md, ok := metadata.FromIncomingContext(ctx)
// do something with metadata
}

2. grpc中使用metadata

1. proto

	syntax = "proto3";
	option go_package=".;proto";
	// The greeting service definition.
	service Greeter {
	// Sends a greeting
	rpc SayHello (HelloRequest) returns (HelloReply) {
	}
	}
	// The request message containing the user's name.
	message HelloRequest {
	string name = 1;
	}
	// The response message containing the greetings
	message HelloReply {
	string message = 1;
}

2. client

package main
import (
	"OldPackageTest/grpc_test/proto"
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)
func main(){
	//stream
	conn, err := grpc.Dial("127.0.0.1:50051", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	c := proto.NewGreeterClient(conn)
	//md := metadata.Pairs("timestamp", time.Now().Format(timestampFormat))
	md := metadata.New(map[string]string{
		"name":"bobby",
		"pasword":"imooc",
	})
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	r, err := c.SayHello(ctx, &proto.HelloRequest{Name:"bobby"})
	if err != nil {
		panic(err)
	}
	fmt.Println(r.Message)
}

3. server

package main
import (
	"context"
	"fmt"
	"google.golang.org/grpc/metadata"
	"net"
	"google.golang.org/grpc"
	"OldPackageTest/grpc_test/proto"
)
type Server struct{}
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*pro
	error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if ok {
		fmt.Println("get metadata error")
	}
	if nameSlice, ok := md["name"]; ok {
		fmt.Println(nameSlice)
	for i, e := range nameSlice {
		fmt.Println(i, e)
		}
	}
	return &proto.HelloReply{
		Message: "hello " + request.Name,
		}, nil
	}
	func main() {
		g := grpc.NewServer()
		proto.RegisterGreeterServer(g, &Server{})
		lis, err := net.Listen("tcp", "0.0.0.0:50051")
		if err != nil {
		panic("failed to listen:" + err.Error())
	}
	err = g.Serve(lis)
	if err != nil {
		panic("failed to start grpc:" + err.Error())
}

grpc拦截器

1. proto

syntax = "proto3";
option go_package = ".;proto";
service Greeter {
	rpc SayHello (HelloRequest) returns (HelloReply);
	}
	//将 sessionid放⼊ 放⼊cookie中 http协议
	message HelloRequest {
	string name = 1;
	}
	message HelloReply {
	string message = 1;
}

2. 客户端

package main
import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"time"
	"start/grpc_interceptor/proto"
)
func interceptor(ctx context.Context, method string, req, reply interface{}, cc *
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Printf("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, rep
return err
}
func main(){
//stream
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
// 指定客户端interceptor
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
conn, err := grpc.Dial("localhost:50051", opts...)
if err != nil {
	panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name:"bobby"})
if err != nil {
	panic(err)
}
	fmt.Println(r.Message)
}

3. 服务端

package main
import (
	"context"
	"fmt"
	"net"
	"google.golang.org/grpc"
	"start/grpc_interceptor/proto"
)
type Server struct{}
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*pro
error){
	return &proto.HelloReply{
	Message: "hello "+request.Name,
	}, nil
}
func main(){
var interceptor grpc.UnaryServerInterceptor
interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServ
	// 继续处理请求
	fmt.Println("接收到新请求")
	res, err := handler(ctx, req)
	fmt.Println("请求处理完成")
		return res, err
	}
	var opts []grpc.ServerOption
	opts = append(opts, grpc.UnaryInterceptor(interceptor))
	g := grpc.NewServer(opts...)
	proto.RegisterGreeterServer(g, &Server{})
	lis, err := net.Listen("tcp", "0.0.0.0:50051")
	if err != nil{
		panic("failed to listen:"+err.Error())
	}
	err = g.Serve(lis)
	if err != nil{
		panic("failed to start grpc:"+err.Error())
	}
}

4.拦截器使用场景

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: go