Go进阶之rpc和grpc

Go环境安装

1)windows

下载直接安装

2)linux

下载解压
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 ID,a和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,或者⽤ asio,ZeroMQ,Netty 之类。

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

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 和 protobuf3,pb3 ⽐ 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。不可以使用其中的[19000-19999]( (从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 类型实际上是map,key是string,value是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.拦截器使用场景

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: RPC(Remote Procedure Call)和 gRPC 是两种不同的远程过程调用协议。RPC是一种通过网络从远程计算机程序上请求服务的协议,它可以实现不同节点之间的通信。而gRPC是一种基于RPC协议的框架,它使用了Google开发的Protocol Buffers作为序列化和协议编解码器,以及HTTP/2作为网络传输协议。\[1\]\[2\]\[3\] 区别在于,gRPC相比传统的RPC框架具有以下特点: 1. 序列化和协议编解码器:gRPC使用Protocol Buffers作为默认的序列化和协议编解码器,它可以高效地序列化和传输数据。 2. 网络传输协议:gRPC使用HTTP/2作为网络传输协议,可以提供更高的性能和更低的延迟。 3. 多语言支持:gRPC支持多种编程语言,包括C++、Java、Python等,可以方便地在不同的语言之间行通信。 4. 异步支持:gRPC支持异步调用,可以提高系统的并发性能。 5. 服务定义:gRPC使用Protocol Buffers定义服务接口和消息类型,可以自动生成客户端和服务端的代码,简化开发过程。 总之,gRPC是一种基于RPC协议的框架,通过使用Protocol Buffers和HTTP/2等技术,提供了更高效、更灵活的远程过程调用方式。 #### 引用[.reference_title] - *1* *2* *3* [RpcgRpc 简介汇总](https://blog.csdn.net/Jailman/article/details/126056469)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值