grpc的使用

grpc 整理(nodejs)

gRPC 是什么?

在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。

图1

gRPC 有什么好处以及在什么场景下需要用 gRPC

既然是 server/client 模型,那么我们直接用 restful api 不是也可以满足吗,为什么还需要 RPC 呢?下面我们就来看看 RPC 到底有哪些优势

gRPC vs. Restful API

gRPC 和 restful API 都提供了一套通信机制,用于 server/client 模型通信,而且它们都使用 http 作为底层的传输协议(严格地说, gRPC 使用的 http2.0,而 restful api 则不一定)。不过 gRPC 还是有些特有的优势,如下:

  • gRPC 可以通过 protobuf 来定义接口,从而可以有更加严格的接口约束条件。
  • 另外,通过 protobuf 可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能。
  • gRPC 可以方便地支持流式通信(理论上通过 http2.0 就可以使用 streaming 模式, 但是通常 web 服务的 restful api 似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如 HLS,RTMP 等,这些就不是我们通常 web 服务了,而是有专门的服务器应用。

使用场景

  • 需要对接口进行严格约束的情况,比如我们提供了一个公共的服务,很多人,甚至公司外部的人也可以访问这个服务,这时对于接口我们希望有更加严格的约束,我们不希望客户端给我们传递任意的数据,尤其是考虑到安全性的因素,我们通常需要对接口进行更加严格的约束。这时 gRPC 就可以通过 protobuf 来提供严格的接口约束。
  • 对于性能有更高的要求时。有时我们的服务需要传递大量的数据,而又希望不影响我们的性能,这个时候也可以考虑 gRPC 服务,因为通过 protobuf 我们可以将数据压缩编码转化为二进制格式,通常传递的数据量要小得多,而且通过 http2 我们可以实现异步的请求,从而大大提高了通信效率。

基本概念

gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.

gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。

服务定义

正如其他 RPC 系统,gRPC 基于如下思想:定义一个服务, 指定其可以被远程调用的方法及其参数和返回类型。gRPC 默认使用 protocol buffers 作为接口定义语言,来描述服务接口和有效载荷消息结构。

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  required string greeting = 1;
}

message HelloResponse {
  required string reply = 1;
}

gRPC 允许定义四类服务方法:

单项 RPC,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。

rpc SayHello(HelloRequest) returns (HelloResponse){
}

服务端流式 RPC,即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}

客户端流式 RPC,即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}

双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。

生命周期

单项 rpc

客户端发出单个请求,获得单个响应。

  • 客户端发起调用,服务端收到调用信息
  • 此时服务端还未收到数据信息,但是已经可以应答(默认不应答)
  • 服务端收到客户端信息,处理数据,向客户端应答,这个应答会和包含状态码以及可选的状态信息等状态明细及可选的追踪信息
  • 若是状态 OK,客户端收到数据,结束调用
流式 RPC

服务端流式 RPC 除了在得到客户端请求信息后发送回一个应答流之外,与单项 rpc 一样。在发送完所有应答后,服务端的状态详情(状态码和可选的状态信息)和可选的跟踪元数据被发送回客户端,以此来完成服务端的工作。客户端在接收到所有服务端的应答后也完成了工作

客户端流式 RPC

客户端流式 RPC 也基本与单项 rpc 一样,区别在于客户端通过发送一个请求流给服务端,取代了原先发送的单个请求。服务端通常(但并不必须)会在接收到客户端所有的请求后发送回一个应答,其中附带有它的状态详情和可选的跟踪数据。

截至时间

gRPC 允许客户端在调用一个远程方法前指定一个最后期限值。这个值指定了在客户端可以等待服务端多长时间来应答,超过这个时间值 RPC 将结束并返回DEADLINE_EXCEEDED错误。

RPC 终止

在 gRPC 里,客户端和服务端对调用成功的判断是独立的、本地的,他们的结论可能不一致。这意味着,比如你有一个 RPC 在服务端成功结束("我已经返回了所有应答!"),到那时在客户端可能是失败的("应答在最后期限后才来到!")。也可能在客户端把所有请求发送完前,服务端却判断调用已经完成了。

安全认证

安全认证

在 nodejs 中的使用

定义服务

//简单服务
rpc GetFeature(Point) returns (Feature) {}

//服务端流式服务
rpc ListFeatures(Rectangle) returns (stream Feature) {}

//客户端流式服务
rpc RecordRoute(stream Point) returns (RouteSummary) {}

//双向流式服务
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

创建服务端(创建服务)

方法实现(简单 rpc)
function checkFeature(point) {
    var feature
    // Check if there is already a feature object for the given point
    for (var i = 0; i < feature_list.length; i++) {
        feature = feature_list[i]
        if (
            feature.location.latitude === point.latitude &&
            feature.location.longitude === point.longitude
        ) {
            return feature
        }
    }
    var name = ''
    feature = {
        name: name,
        location: point
    }
    return feature
}
function getFeature(call, callback) {
    callback(null, checkFeature(call.request))
}
方法实现(流式 rpc)
function listFeatures(call) {
    var lo = call.request.lo
    var hi = call.request.hi
    var left = _.min([lo.longitude, hi.longitude])
    var right = _.max([lo.longitude, hi.longitude])
    var top = _.max([lo.latitude, hi.latitude])
    var bottom = _.min([lo.latitude, hi.latitude])
    // For each feature, check if it is in the given bounding box
    _.each(feature_list, function(feature) {
        if (feature.name === '') {
            return
        }
        if (
            feature.location.longitude >= left &&
            feature.location.longitude <= right &&
            feature.location.latitude >= bottom &&
            feature.location.latitude <= top
        ) {
            call.write(feature)
        }
    })
    call.end()
}
启动服务器
var server = new grpc.Server()
server.addService(hello_proto.Greeter.service, { sayHello: sayHello })
server.bind('localhost:50051', grpc.ServerCredentials.createInsecure())
server.start()

创建客户端(创建调用)

简单 rpc
var point = {latitude: 409146138, longitude: -746188906};
stub.getFeature(point, function(err, feature) {
  if (err) {
    // process error
  } else {
    // process feature
  }
});
流式 rpc
var call = client.listFeatures(rectangle);
  call.on('data', function(feature) {
      console.log('Found feature called "' + feature.name + '" at ' +
          feature.location.latitude/COORD_FACTOR + ', ' +
          feature.location.longitude/COORD_FACTOR);
  });
  call.on('end', function() {
    // The server has finished sending
  });
  call.on('status', function(status) {
    // process status
  });

Demo

第一行代码 Hello World

helloworld.proto

​```
syntax = "proto3";

package helloworld;
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

server.js

var PROTO_PATH = __dirname + '/helloworld.proto'

var grpc = require('grpc')
var protoLoader = require('@grpc/proto-loader')
var packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true, //保留字段名称,默认将下划线处理为驼峰
    longs: String, //long类型自动转为string
    enums: String, //枚举类型转为string
    defaults: true, //在输出对象上设置默认值
    oneofs: true //虚拟属性设置为当前字段名称
})
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld

function sayHello(call, callback) {
    callback(null, { message: 'Hello ' + call.request.name })
}

function main() {
    var server = new grpc.Server()
    server.addService(hello_proto.Greeter.service, { sayHello: sayHello })
    server.bind('localhost:50051', grpc.ServerCredentials.createInsecure())
    server.start()
}

main()

client.js

var PROTO_PATH = __dirname + '/helloworld.proto'

var grpc = require('grpc')
var protoLoader = require('@grpc/proto-loader')
var packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true
})
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld

function main() {
    var client = new hello_proto.Greeter(
        'localhost:50051',
        grpc.credentials.createInsecure()
    )
    var user
    if (process.argv.length >= 3) {
        user = process.argv[2]
    } else {
        user = 'world'
    }
    client.sayHello({ name: user }, function(err, response) {
        console.log('Greeting:', response.message)
    })
}
main()

有关 grpc 接口(简单接口)和普通 http 接口(express 实现)的性能测试

服务器环境(单核,1G,1Mbps)

http 请求

const http = require('http')
const taskList = []
console.log('请求数据中...')
const start = new Date().getTime()
let count = 0
let success = 0
let error = 0
let times = 3000
for (let i = 0; i < times; i++) {
    taskList[i] = new Promise((resolve, reject) => {
        http.get('http://39.100.197.67:3000/list', function(req, res) {
            let stream = ''
            req.on('data', function(data) {
                stream += data
            })
            req.on('error', function() {
                count++
                error++
                resolve({ count, success, error })
            })
            req.on('end', function() {
                count++
                success++
                resolve({ count, success, error })
            })
        })
    })
}
Promise.all(taskList)
    .then(result => {
        console.log('count:' + count)
        console.log('success:' + success)
        console.log('error:' + error)
        const end = new Date().getTime()
        console.log('time:' + (end - start))
    })
    .catch(err => {
        console.log(err)
    })
const express = require('express')
const app = express()
app.get('/', (req, res) =>{
    res.send('HellowWorld')
})
app.get('/list', (req, res) => {
    let result = {
        err: 0,
        msg: 'ok',
        data: {
            name: 'hello world',
            id: req.query.id
        }
    }
    if (req.query.id !== 1) {
        result.data.name = 'hello grpc'
    }
    res.send(result)
})
const server = app.listen(3000, function() {
    console.log('runing 3000...')
})

grpc 请求

const PROTO_PATH = __dirname + '/helloworld.proto'

const grpc = require('grpc')
const protoLoader = require('@grpc/proto-loader')
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true
})
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld
const client = new hello_proto.Greeter(
    '39.100.197.67:50051',
    grpc.credentials.createInsecure()
)

const taskList = []
console.log('请求数据中...')
const start = new Date().getTime()
let count = 0
let success = 0
let error = 0
let times = 3000
for (let i = 0; i < times; i++) {
    taskList[i] = new Promise((resolve, reject) => {
        client.sayHello({ id: 1 }, function(err, response) {
            count++
            if (err) {
                error++
                resolve()
            } else {
                success++
                resolve()
            }
        })
    })
}
Promise.all(taskList)
    .then(result => {
        console.log('count:' + count)
        console.log('success:' + success)
        console.log('error:' + error)
        const end = new Date().getTime()
        console.log('time:' + (end - start))
    })
    .catch(err => {
        console.log(err)
    })
const PROTO_PATH = __dirname + '/helloworld.proto';
const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {
        keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
    });
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;
const sayHello = (call, callback) => {
    const data = { name: 'hello world', id: +call.request.id };
    if (call.request.id !== 1) {
        data.name = 'hello grpc'
    }
    callback(null, { message: JSON.stringify({ err: 0, msg: 'ok', data }) })
}

const main = () => {
    var server = new grpc.Server();
    server.addService(hello_proto.Greeter.service, { sayHello: sayHello });
    server.bind('localhost:50051', grpc.ServerCredentials.createInsecure());
    server.start();
};
main();

测试结果

请求量 成功量 失败量 响应时间(ms) 请求类型
500 500 0 298 grpc
500 500 0 1344 http
请求量 成功量 失败量 响应时间(ms) 请求类型
1000 1000 0 362 grpc
1000 1000 0 5251 http
请求量 成功量 失败量 响应时间(ms) 请求类型
5000 5000 0 3291 grpc
5000 5000 0 33571 http
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  •  gRPC 学习笔记,记录gprc一些基本概念.  gRPC正如其他 RPC 系统,gRPC 基于如下思想:定义一...
    Jancd阅读 1,960评论 1 7
  • Fabric的节点通过grpc向内部或外部提供接口,在学习源码之前,需要对grpc的基本使用有所了解,并了解如何在...
    史圣杰阅读 857评论 0 1
  • 1.简介 在gRPC中,客户端应用程序可以直接调用不同计算机上的服务器应用程序上的方法,就像它是本地对象一样,使您...
    第八共同体阅读 1,856评论 0 6
  • GRPC是基于protocol buffers3.0协议的. 本文将向您介绍gRPC和protocol buffe...
    二月_春风阅读 17,979评论 2 28
  • 梦想总会实现 奇迹就在身边 是天意吧,让我今生遇见你 认识你是我一生的荣幸 希望在以后的日子里我们共同成长 探索永...
    老张小时候阅读 527评论 0 7