一、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文件来定义数据结构的描述。

:对比JSON和XML

grpc 底层tcp掐断 grpc为啥不用tcp_python

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)
        }