primers

The popularity of Web apps has led to the popularity of API architectures, with a large number of sites and services interacting with HTTP 1.1-based apis. The advantages of this type of text-based API are obvious: it is easy to write and understand; Support communication between heterogeneous platforms. The disadvantages are also obvious: being text-based makes the API transfer too large; There is a latency that the client is aware of.

If performance is critical, try an RPC framework based on binary transport, such as:

gRPC

GRPC is a high-performance, open source, general-purpose, mobile-oriented RPC framework. The transport protocol is based on HTTP/2, which means it supports bidirectional streaming, flow control, header compression, request multiplexing over a single TCP connection and other features.

At the interface level, gRPC uses Protocol Buffers (Protobuf for short) as its interface definition language (IDL) by default to describe its service interface and the format of load messages.

GRPC currently provides language support for C++, Node.js, Python, Ruby, Objective-C, PHP, C#.

With the Node. Js integration

The interface definition

Protobuf as IDL is characterized by good semantics and complete definition of data types. The current language version can be divided into Proto2 and Proto3.

The protobuf interface definition can be broken down into several parts:

  1. IDL version proto2/3
  2. Package name
  3. Service definition and method definition
  4. Message definition: Request message and response message

Here we use Proto3 to define a testService service in testPackage, which provides only a ping method that returns the result in the message field:

// test.proto syntax = "proto3"; package testPackage; service testService { rpc ping (pingRequest) returns (pingReply) {} } message pingRequest { } message pingReply { string  message = 1; }Copy the code

The Demo version

The service side

Taking Node.js as an example, we use the GRPC package to provide RPC services in a dynamically loaded.proto mode:

import grpc from 'grpc' const PROTO_PATH = __dirname + '.. /protos/test.proto' const testProto = grpc.load(PROTO_PATH).testPackage function test(call, callback) { callback(null, {message: 'Pong'}) } const server = new grpc.Server(); server.addProtoService(testProto.testService.service, {test: The test}) server. The bind (' 0.0.0.0:50051 ', GRPC. ServerCredentials. CreateInsecure ()) for server start ()Copy the code

It is important to note: the code here is trying to test, using the ServerCredentials. CreateInsecure () to create a secure connection. If RPC services are provided on the public network, see the authentication manual to select an appropriate solution: www.grpc.io/docs/guides… .

The client

The client also uses dynamic loading of the.proto interface definition to make service calls.

import grpc from 'grpc' const PROTO_PATH = __dirname + '.. /protos/test.proto' const testProto = grpc.load(PROTO_PATH).testPackage const client = new TestProto. TestService (' 0.0.0.0:50051 ', GRPC. Credentials. CreateInsecure ()); client.ping({}, function(err, response) { console.log('ping -> :', response.message); });Copy the code

 

Optimized version

The Demo version above is just a working toy. In engineering practice, we have some additional requirements:

  • Proto file in the specified folder, automatically loaded
  • Package and service names do not need to be hard-coded and are all dynamically specified by the calling end
  • Multiple RPC endpoints that can simultaneously expose/invoke multiple packages

Based on this, there is an optimized dynamic version:

The service side

RpcServer. Js:

import grpc from 'grpc' class RpcServer { constructor(ip, Port) {this. IP = IP this.port = port this.services = {} this.functions = {}} // Automatic loading proto and Server autoRun(protoDir) { fs.readdir(protoDir, (err, files) => { if (err) { return logger.error(err) } R.forEach((file) => { const filePart = path.parse(file) const serviceName = filePart.name const packageName = filePart.name const extName = filePart.ext const filePath = path.join(protoDir, file) if (extName === '.js') { const functions = require(filePath).default this.functions[serviceName] = Object.assign({}, functions) } else if (extName === '.proto') { this.services[serviceName] = grpc.load(filePath)[packageName][serviceName].service } }, files) return this.runServer() }) } runServer() { const server = new grpc.Server() R.forEach((serviceName) => { const service = this.services[serviceName] server.addProtoService(service, this.functions[serviceName]) }, R.keys(this.services)) server.bind(`${this.ip}:${this.port}`, grpc.ServerCredentials.createInsecure()) server.start() } } export default RpcServerCopy the code

Server.js uses it like this:

Logger. info('Starting RPC Server:') const rpcServer = new rpcServer ('0.0.0.0', 50051) rpcServer.autoRun(path.join(__dirname, '.. /protos/'))Copy the code
The client

RcpClient. Js:

import grpc from 'grpc' class RpcClient { constructor(ip, Port) {this. IP = IP this.port = port this.services = {} this.clients = {}} // autoRun(protoDir) { fs.readdir(protoDir, (err, files) => { if (err) { return logger.error(err) } return files.forEach((file) => { const filePart = path.parse(file) const serviceName = filePart.name const packageName = filePart.name const extName = filePart.ext const filePath = path.join(protoDir, file) if (extName === '.proto') { const proto = grpc.load(filePath) const Service = proto[packageName][serviceName] this.services[serviceName] = Service this.clients[serviceName] = new Service(`${this.ip}:${this.port}`, grpc.credentials.createInsecure()) } }, files) }) } async invoke(serviceName, name, params) { return new Promise((resolve, reject) => { function callback(error, response) { if (error) { reject(error) } else { resolve(response) } } params = params || {} if (this.clients[serviceName] && this.clients[serviceName][name]) { this.clients[serviceName][name](params, callback) } else { const error = new Error( `RPC endpoint: "${serviceName}.${name}" does not exists.`) reject(error) } }) } export default RpcClientCopy the code

Example business invocation:

logger.info('RPC Client connecting:') const rpcClient = new RpcClient(config.grpc.ip, config.grpc.port) rpcClient.autoRun(path.join(__dirname, '.. /protos/')) try { // expected: Pong const result = await rpcClient.invoke('testService', 'ping') } catch (err) { logger.error(err) }Copy the code

Unit testing

import { RpcClient } from '.. /components' describe('one million RPCs', () => { before(() => { logger.info('RPC Client connecting:') global.rpcClient = new RpcClient(config.grpc.ip, config.grpc.port) rpcClient.autoRun(path.join(__dirname, '.. /protos')) }) it('should not failed', async(done) => { const startTime = Date.now() const times = 1000000 for(let i = 0;  i < times; i++) { console.log(i) const respone = await rpcClient.invoke('testService', 'ping') respone.message.should.be.equal('Pong') } const total = Date.now() - startTime print('total(MS):', total) done() }) })Copy the code

Test result is single request millisecond response:

999992 999993 999994 999995 999996 999997 999998 999999 total(MS): 1084334 ✓ Should not fail (1084338ms) 1 passing (18m)Copy the code

Of course, this sequential EXECUTION of RPC test cases does not accurately reflect concurrency performance, just for reference.







To play to admire


Your support will encourage us to continue to create!

WeChat pay
Alipay

Scan the QR code with wechat to tip

Scan the QR code with Alipay