一、Http和Rpc
误区:http协议是支持连接池复用的,也就是建立一定数量的连接不断开,并不会频繁的创建和销毁连接。
- 传输协议
- RPC,可以基于TCP协议,也可以基于HTTP协议
- HTTP,基于HTTP协议,一种对TCP协议的应用
- 传输效率
- RPC,使用自定义的TCP协议,可以让请求报文体积更小,或者使用HTTP2协议,也可以很好的减少报文的体积,提高传输效率
- HTTP,如果是基于HTTP1.1的协议,请求中会包含很多无用的内容,如果是基于HTTP2.0,那么简单的封装以下是可以作为一个RPC来使用的,这时标准RPC框架更多的是服务治理
- 性能消耗,主要在于序列化和反序列化的耗时
- RPC,可以基于protobuf实现高效的二进制传输
- HTTP,大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能
二、Protobuf
protobuf 是一种与语言无关、与平台无关,是一种可扩展的用于序列化和结构化数据的方法,常用于用于通信协议,数据存储等。是一种灵活,高效,自动化的机制,用于序列化结构化数据,对比于 XML,他更小,更快。
但是,由于依赖语言,所以使用时候稍微会麻烦些,它依赖工具包来进行编译成 python 文件或 go 文件等。
Protobuf 数据的体量更小,所以自然失去了人类的直接可读性, JSON 数据结构是可以很直观地阅读的,但是 Protobuf 我们需要借助工具来进行更友好地使用,所以,我们需要自定义一个 .proto文件来定义数据结构的描述。
1、protobuf 数据结构
定义一个 Message 类型
message LoginRequest {
required string username = 1; // 必须的参数,类型为字符串,变量名称username
required string password = 2; // 必须的参数,类型为字符串,变量名称password
int32 age = 3; // 可选的参数,类型int32,变量名称age
}
你指定的 message 字段可以是下面几种情况之一:
- required: 格式良好的 message 必须包含该字段一次。
- optional: 格式良好的 message 可以包含该字段零次或一次(proto3中废除,默认使用该参数)
- repeated: 该字段可以在格式良好的消息中重复任意多次(包括零)。其中重复值的顺序会被保留。
支持的数据类型
标量 message 字段可以具有以下几种类型之一 - 该表显示 .proto 文件中指定的类型,以及自动生成的类中的相应类型:
.proto Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type |
double | double | double | float | *float64 | |
float | float | float | float | *float32 | |
int32 | 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint32 | int32 | int | int | *int32 |
int64 | 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint64 | int64 | long | int/long[3] | *int64 |
uint32 | 使用可变长度编码 | uint32 | int[1] | int/long[3] | *uint32 |
uint64 | 使用可变长度编码 | uint64 | long[1] | int/long[3] | *uint64 |
sint32 | 使用可变长度编码。有符号的 int 值。这些比常规 int32 对负数能更有效地编码 | int32 | int | int | *int32 |
sint64 | 使用可变长度编码。有符号的 int 值。这些比常规 int64 对负数能更有效地编码 | int64 | long | int/long[3] | *int64 |
fixed32 | 总是四个字节。如果值通常大于 228,则比 uint32 更有效。 | uint32 | int[1] | int/long[3] | *uint32 |
fixed64 | 总是八个字节。如果值通常大于 256,则比 uint64 更有效。 | uint64 | long[1] | int/long[3] | *uint64 |
sfixed32 | 总是四个字节 | int32 | int | int | *int32 |
sfixed64 | 总是八个字节 | int64 | long | int/long[3] | *int64 |
bool | bool | boolean | bool | *bool | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本 | string | String | str/unicode[4] | *string |
bytes | 可以包含任意字节序列 | string | ByteString | str | []byte |
复杂数据类型
例:jarvis.proto
syntax = "proto3";
// 定义服务
service JarvisServices {
rpc segment(SegmentRequest) returns (SegmentReply) {}
rpc sentence_rewrite(SentenceRewriteRequest) returns (SentenceRewriteReply) {}
}
// 生成数据结构
// base
message Map { // repeated 和 map 不能共用
map <string, string> element = 1;
}
message listStr {
repeated string element = 1;
}
message RewriteType {
repeated Map result = 1;
double score = 2;
}
// Segment
message SegmentRequest {
string doc = 1;
string mode = 2;
repeated string dicts = 3;
}
message SegmentReply {
repeated Map result = 1;
}
// sentence_rewrite
message SentenceRewriteRequest {
string sentence = 1;
string embedding_type = 2;
string mode = 3;
int32 count = 4;
repeated string dicts = 5;
}
message SentenceRewriteReply {
repeated RewriteType result = 1;
}
三、在Python中使用Grpc
1、 所需依赖
# python grpc 项目依赖
pip install grpcio
# 安装 python 下的 protoc 编译器
pip install grpcio-tools
2、编译 proto 文件
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. jrvis.proto
-
python -m grpc_tools.protoc
:python
下的protoc
编译器通过python
模块(module) 实现, 所以说这一步非常省心 -
--python_out=.
: 编译生成处理protobuf
相关的代码的路径, 这里生成到当前目录 -
--grpc_python_out=.
: 编译生成处理grpc
相关的代码的路径, 这里生成到当前目录 -
-I. jarvis.proto
:proto
文件的路径, 这里的proto
文件在当前目录
编译后生成的代码:
-
jarvis_pb2.py
: 用来和 protobuf 数据进行交互 -
jarvis_pb2_grpc.py
: 用来和 grpc 进行交互
3、Grpc服务端
实现proto中定义的方法:
import grpc
import jarvis_pb2
import jarvis_pb2_grpc
# 实现 proto 文件中定义的 GrpcServices
class GrpcServices(jarvis_pb2_grpc.JarvisServicesServicer):
def __init__(self):
print(F"service start")
def segment(self, request, context):
results = NLP.word_segment(request.doc, request.mode or 'normal', request.dicts)
# 把结果变成proto文件中定义的类型(message)
result_list = [jarvis_pb2.Map(element=result) for result in results]
# 返回值也需要定义成proto中定义的类型(message)
return jarvis_pb2.SegmentReply(result=result_list)
启动服务器
一旦我们实现了proto
中所有的方法,下一步就是启动一个gRPC服务器,这样客户端才可以使用服务:
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
def serve():
grpcServer = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
jarvis_pb2_grpc.add_JarvisServicesServicer_to_server(GrpcServices(), grpcServer)
grpcServer.add_insecure_port("{0}:{1}".format(_HOST, _PORT))
grpcServer.start()
try:
while True:
sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
grpcServer.stop(0)
if __name__ == '__main__':
server()
因为 start()
不会阻塞,如果运行时你的代码没有其它的事情可做,你可能需要循环等待。
4、Grpc客户端
生成客户端实例
为了能调用服务的方法,我们得先创建一个 实例。
我们使用 .proto 中生成的 jarvis_pb2_grpc
模块的函数JarvisServicesStub
。
channel = grpc.insecure_channel("{0}:{1}".format(_HOST, _PORT))
CLIENT = jarvis_pb2_grpc.JarvisServicesStub(channel=channel)
返回的对象实现了定义在 JarvisServicesStub
接口中的所有对象。
调用服务方法
# message listStr {
# repeated string element = 1;
# }
#
# message DocSimilarityRequest {
# repeated listStr sentence_pairs = 1;
# string method = 2;
# string embedding_type = 3;
# }
# listStr
sentence_pairs = [jarvis_pb2.listStr(element=["腾讯 股价", "阿里 股价"]), jarvis_pb2.listStr(element=["腾讯 股价", "腾讯 股价"])]
# DocSimilarityRequest
seg_result = CLIENT.doc_similarity(jarvis_pb2.DocSimilarityRequest(sentence_pairs=sentence_pairs))
四、在Golang中使用Grpc
1、所需依赖
先安装Protobuf 编译器 protoc,下载地址:https://github.com/google/protobuf/releases 我的是mac,将压缩包bin
目录放到环境 ~/.bash_profile
目录中即可。
export PATH=$PATH:/Users/xxxx/Documents/protoc-3.11.4/bin
然后获取插件支持库(推荐使用)
// gRPC运行时接口编解码支持库
go get -u github.com/golang/protobuf/proto
// 从 Proto文件(gRPC接口描述文件) 生成 go文件 的编译器插件
go get -u github.com/golang/protobuf/protoc-gen-go
获取go的gRPC包
go get google.golang.org/grpc
2、编译 proto 文件
然后将proto文件编译为go文件
// protoc --go_out=plugins=grpc:{输出目录} {proto文件}
protoc --go_out=plugins=grpc:./test/ ./test.proto
注意:原则上不要修改编译出来的*.bp.go
文件的代码,因为双方接口基于同一个proto文件编译成自己的语言源码,此文件只作为接口数据处理,业务具体实现不在*.bp.go
中。
3、服务端
package main
import (
"log"
"net"
"golang.org/x/net/context"
"google.golang.org/grpc"
"test"
"google.golang.org/grpc/reflection"
"fmt"
"crypto/md5"
)
// 业务实现方法的容器
type server struct{}
// 为server定义 DoMD5 方法 内部处理请求并返回结果
// 参数 (context.Context[固定], *test.Req[相应接口定义的请求参数])
// 返回 (*test.Res[相应接口定义的返回参数,必须用指针], error)
func (s *server) DoMD5(ctx context.Context, in *test.Req) (*test.Res, error) {
fmt.Println("MD5方法请求JSON:"+in.JsonStr)
return &test.Res{BackJson: "MD5 :" + fmt.Sprintf("%x", md5.Sum([]byte(in.JsonStr)))}, nil
}
func main() {
lis, err := net.Listen("tcp", ":8028") //监听所有网卡8028端口的TCP连接
if err != nil {
log.Fatalf("监听失败: %v", err)
}
s := grpc.NewServer() //创建gRPC服务
/**注册接口服务
* 以定义proto时的service为单位注册,服务中可以有多个方法
* (proto编译时会为每个service生成Register***Server方法)
* 包.注册服务方法(gRpc服务实例,包含接口方法的结构体[指针])
*/
test.RegisterWaiterServer(s, &server{})
/**如果有可以注册多个接口服务,结构体要实现对应的接口方法
* user.RegisterLoginServer(s, &server{})
* minMovie.RegisterFbiServer(s, &server{})
*/
// 在gRPC服务器上注册反射服务
reflection.Register(s)
// 将监听交给gRPC服务处理
err = s.Serve(lis)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
4、客户端
package main
import (
"log"
"os"
"golang.org/x/net/context"
"google.golang.org/grpc"
"test"
)
func main() {
// 建立连接到gRPC服务
conn, err := grpc.Dial("127.0.0.1:8028", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
// 函数结束时关闭连接
defer conn.Close()
// 创建Waiter服务的客户端
t := test.NewWaiterClient(conn)
// 模拟请求数据
res := "test123"
// os.Args[1] 为用户执行输入的参数 如:go run ***.go 123
if len(os.Args) > 1 {
res = os.Args[1]
}
// 调用gRPC接口
tr, err := t.DoMD5(context.Background(), &test.Req{JsonStr: res})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("服务端响应: %s", tr.BackJson)
}