grpc 整理(nodejs)
gRPC 是什么?
在 gRPC 里客戶端應用可以像調(diào)用本地對象一樣直接調(diào)用另一臺不同的機器上服務(wù)端應用的方法,使得您能夠更容易地創(chuàng)建分布式應用和服務(wù)。與許多 RPC 系統(tǒng)類似,gRPC 也是基于以下理念:定義一個服務(wù),指定其能夠被遠程調(diào)用的方法(包含參數(shù)和返回類型)。在服務(wù)端實現(xiàn)這個接口,并運行一個 gRPC 服務(wù)器來處理客戶端調(diào)用。在客戶端擁有一個存根能夠像服務(wù)端一樣的方法。

gRPC 有什么好處以及在什么場景下需要用 gRPC
既然是 server/client 模型,那么我們直接用 restful api 不是也可以滿足嗎,為什么還需要 RPC 呢?下面我們就來看看 RPC 到底有哪些優(yōu)勢
gRPC vs. Restful API
gRPC 和 restful API 都提供了一套通信機制,用于 server/client 模型通信,而且它們都使用 http 作為底層的傳輸協(xié)議(嚴格地說, gRPC 使用的 http2.0,而 restful api 則不一定)。不過 gRPC 還是有些特有的優(yōu)勢,如下:
- gRPC 可以通過 protobuf 來定義接口,從而可以有更加嚴格的接口約束條件。
- 另外,通過 protobuf 可以將數(shù)據(jù)序列化為二進制編碼,這會大幅減少需要傳輸?shù)臄?shù)據(jù)量,從而大幅提高性能。
- gRPC 可以方便地支持流式通信(理論上通過 http2.0 就可以使用 streaming 模式, 但是通常 web 服務(wù)的 restful api 似乎很少這么用,通常的流式數(shù)據(jù)應用如視頻流,一般都會使用專門的協(xié)議如 HLS,RTMP 等,這些就不是我們通常 web 服務(wù)了,而是有專門的服務(wù)器應用。
使用場景
- 需要對接口進行嚴格約束的情況,比如我們提供了一個公共的服務(wù),很多人,甚至公司外部的人也可以訪問這個服務(wù),這時對于接口我們希望有更加嚴格的約束,我們不希望客戶端給我們傳遞任意的數(shù)據(jù),尤其是考慮到安全性的因素,我們通常需要對接口進行更加嚴格的約束。這時 gRPC 就可以通過 protobuf 來提供嚴格的接口約束。
- 對于性能有更高的要求時。有時我們的服務(wù)需要傳遞大量的數(shù)據(jù),而又希望不影響我們的性能,這個時候也可以考慮 gRPC 服務(wù),因為通過 protobuf 我們可以將數(shù)據(jù)壓縮編碼轉(zhuǎn)化為二進制格式,通常傳遞的數(shù)據(jù)量要小得多,而且通過 http2 我們可以實現(xiàn)異步的請求,從而大大提高了通信效率。
基本概念
gRPC 是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設(shè)計。目前提供 C、Java 和 Go 語言版本,分別是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.
gRPC 基于 HTTP/2 標準設(shè)計,帶來諸如雙向流、流控、頭部壓縮、單 TCP 連接上的多復用請求等特。這些特性使得其在移動設(shè)備上表現(xiàn)更好,更省電和節(jié)省空間占用。
服務(wù)定義
正如其他 RPC 系統(tǒng),gRPC 基于如下思想:定義一個服務(wù), 指定其可以被遠程調(diào)用的方法及其參數(shù)和返回類型。gRPC 默認使用 protocol buffers 作為接口定義語言,來描述服務(wù)接口和有效載荷消息結(jié)構(gòu)。
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
required string greeting = 1;
}
message HelloResponse {
required string reply = 1;
}
gRPC 允許定義四類服務(wù)方法:
單項 RPC,即客戶端發(fā)送一個請求給服務(wù)端,從服務(wù)端獲取一個應答,就像一次普通的函數(shù)調(diào)用。
rpc SayHello(HelloRequest) returns (HelloResponse){
}
服務(wù)端流式 RPC,即客戶端發(fā)送一個請求給服務(wù)端,可獲取一個數(shù)據(jù)流用來讀取一系列消息。客戶端從返回的數(shù)據(jù)流里一直讀取直到?jīng)]有更多消息為止。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
客戶端流式 RPC,即客戶端用提供的一個數(shù)據(jù)流寫入并發(fā)送一系列消息給服務(wù)端。一旦客戶端完成消息寫入,就等待服務(wù)端讀取這些消息并返回應答。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
雙向流式 RPC,即兩邊都可以分別通過一個讀寫數(shù)據(jù)流來發(fā)送一系列消息。這兩個數(shù)據(jù)流操作是相互獨立的,所以客戶端和服務(wù)端能按其希望的任意順序讀寫,例如:服務(wù)端可以在寫應答前等待所有的客戶端消息,或者它可以先讀一個消息再寫一個消息,或者是讀寫相結(jié)合的其他方式。每個數(shù)據(jù)流里消息的順序會被保持。
生命周期
單項 rpc
客戶端發(fā)出單個請求,獲得單個響應。
- 客戶端發(fā)起調(diào)用,服務(wù)端收到調(diào)用信息
- 此時服務(wù)端還未收到數(shù)據(jù)信息,但是已經(jīng)可以應答(默認不應答)
- 服務(wù)端收到客戶端信息,處理數(shù)據(jù),向客戶端應答,這個應答會和包含狀態(tài)碼以及可選的狀態(tài)信息等狀態(tài)明細及可選的追蹤信息
- 若是狀態(tài) OK,客戶端收到數(shù)據(jù),結(jié)束調(diào)用
流式 RPC
服務(wù)端流式 RPC 除了在得到客戶端請求信息后發(fā)送回一個應答流之外,與單項 rpc 一樣。在發(fā)送完所有應答后,服務(wù)端的狀態(tài)詳情(狀態(tài)碼和可選的狀態(tài)信息)和可選的跟蹤元數(shù)據(jù)被發(fā)送回客戶端,以此來完成服務(wù)端的工作??蛻舳嗽诮邮盏剿蟹?wù)端的應答后也完成了工作
客戶端流式 RPC
客戶端流式 RPC 也基本與單項 rpc 一樣,區(qū)別在于客戶端通過發(fā)送一個請求流給服務(wù)端,取代了原先發(fā)送的單個請求。服務(wù)端通常(但并不必須)會在接收到客戶端所有的請求后發(fā)送回一個應答,其中附帶有它的狀態(tài)詳情和可選的跟蹤數(shù)據(jù)。
截至時間
gRPC 允許客戶端在調(diào)用一個遠程方法前指定一個最后期限值。這個值指定了在客戶端可以等待服務(wù)端多長時間來應答,超過這個時間值 RPC 將結(jié)束并返回DEADLINE_EXCEEDED錯誤。
RPC 終止
在 gRPC 里,客戶端和服務(wù)端對調(diào)用成功的判斷是獨立的、本地的,他們的結(jié)論可能不一致。這意味著,比如你有一個 RPC 在服務(wù)端成功結(jié)束("我已經(jīng)返回了所有應答!"),到那時在客戶端可能是失敗的("應答在最后期限后才來到!")。也可能在客戶端把所有請求發(fā)送完前,服務(wù)端卻判斷調(diào)用已經(jīng)完成了。
安全認證
在 nodejs 中的使用
定義服務(wù)
//簡單服務(wù)
rpc GetFeature(Point) returns (Feature) {}
//服務(wù)端流式服務(wù)
rpc ListFeatures(Rectangle) returns (stream Feature) {}
//客戶端流式服務(wù)
rpc RecordRoute(stream Point) returns (RouteSummary) {}
//雙向流式服務(wù)
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
創(chuàng)建服務(wù)端(創(chuàng)建服務(wù))
方法實現(xiàn)(簡單 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))
}
方法實現(xiàn)(流式 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()
}
啟動服務(wù)器
var server = new grpc.Server()
server.addService(hello_proto.Greeter.service, { sayHello: sayHello })
server.bind('localhost:50051', grpc.ServerCredentials.createInsecure())
server.start()
創(chuàng)建客戶端(創(chuàng)建調(diào)用)
簡單 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類型自動轉(zhuǎn)為string
enums: String, //枚舉類型轉(zhuǎn)為string
defaults: true, //在輸出對象上設(shè)置默認值
oneofs: true //虛擬屬性設(shè)置為當前字段名稱
})
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()
有關(guān) grpc 接口(簡單接口)和普通 http 接口(express 實現(xiàn))的性能測試
服務(wù)器環(huán)境(單核,1G,1Mbps)
http 請求
const http = require('http')
const taskList = []
console.log('請求數(shù)據(jù)中...')
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('請求數(shù)據(jù)中...')
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();
測試結(jié)果
| 請求量 | 成功量 | 失敗量 | 響應時間(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 |