ساخت API با استفاده از gRPC در Node js

در حال گشت و گذار تو ویرگول بودم که مقاله جالبی از عرفان توجهم رو جلب کرد. تو این مقاله در مورد gRPC و تفاوت اون با REST توضیح داده بود. توضیحات قبل و بعدش هم کامل بود و اینجا هر زمانی که نیاز بشه بهش استناد می‌کنم. بعد خوندنش تصمیم گرفتم gRPC رو روی Node js هم پیاده سازی کنم و ساختار API پایه بنویسم.

http://vrgl.ir/Le84W

‏gRPC چیه؟

gRPC یک فریم ورک که گوگل اون رو توسعه داده و مزیت اصلیش امنیت و سرعت فوق العادش نسبت به معماری REST هستش. این مزیت ها به خاطر دلایل پایینه:

  • با استاندارد HTTP/2 ساخته شده. تو این استاندارد دیگ برای هر درخواست یک tcp جداگونه باز نمیشه و برای انتقال دیتا از باینری استفاده می‌کنه که نسبت به استاندارد HTTP/1 خیلی سریع‌تر و چابک‌تره. برای اینکه بیشتر در مورد HTTP/2 بدونید اینجا رو بخونید.
  • از پروتکل بافر استفاده می‌کنه. پروتکل بافر در واقع یک ساختار دیتا هستش که از قواعد خاصی پیروی
    میکنه.دیتا رو کمپرس می‌کنه و تقریبا ۷ برابر از json سریعتره. اطلاعات کاملتر رو اینجا بخونید.
  • از message به جای verb ها در REST استفاده می‌کنه. وقتی که تو معماری REST توسعه می‌دیم عملاً متدها یا GET هستن یا POST . (متدهای PUT,PATCH, DELETE هم به نوعی POST محسوب میشن) این امکان دست توسعه دهنده رو باز میزاره تا متدهای دلخواه خودش رو هم بدون محدودیت ایجاد کنه.
برای مقایسه کامل‌تر REST و gRPC می‌تونید این مقاله رو بخونید.

راه اندازی پروژه
یک دایرکتوری جدید برای پروژه می‌سازیم.

$ mkdir NODE-GRPC
$ cd NODE-GRPC

و این ساختار رو ایجاد می‌کنیم.

├── client
│   ├── app.js
│   └── package.json
├── protos
└── server
    ├── index.js
    └── package.json

من برای این پروژه از PostgreSQL استفاده کردم ، ولی شما می‌تونید از هر دیتابیسی استفاده کنید.یک دیتابیس به نام grpc_products می‌سازیم.


سرویس های gRPC

کاری که امروز میخوایم انجام بدیم ساخت API با استفاده از gRPC برای انجام عملیات CRUD هستش.

در gRPC مشابه Socket ما یک سرور و یک کلاینت داریم. اول از همه سرور رو شروع می‌کنیم.

تو فولدر protos یک فایل با عنوان product.proto می‌سازیم که تو اون سرویس‌ها و مسیج‌های gRPC رو می‌نویسیم.

syntax = "proto3";
package product;

// service definition

service ProductService {
  rpc listProducts(Empty) returns (ProductList) {}
  rpc readProduct(ProductId) returns (Product) {}
  rpc createProduct(newProduct) returns (result) {}
  rpc updateProduct(Product) returns (result) {}
  rpc deleteProduct(ProductId) returns (result) {}
}

// message type definitions

message Empty {}

message ProductList {
  repeated Product products = 1;
}

message ProductId {
  int32 id = 1;
}

message Product {
  int32 id = 1;
  string name = 2;
  string price = 3;
}

message newProduct {
  string name = 1;
  string price = 2;
}

message result {
  string status = 1;
}

تو این قسمت اول ورژن پروتوبافر رو مشخص کردیم و بعد هم براش یک package ساختیم.

ساخت پکیج برای فایل پروتو الزامی نیست ولی ایده خوبیه تا تو پروژه های بزرگ گیج نشیم.

بعد از اون متدهای gRPC رو که معادل عملیات پایه ای CRUD هست رو نوشتیم.

CRUD gRPC method

Index listProducts

Store createProduct

Update updateProduct

Get readProduct

Delete deleteProduct

متد listProducts یک مسیج Empty رو به عنوان ورودی دریافت و ProductList رو برمی‌گردونه.


حالا فایل `package.json` اپدیت می‌کنیم.

{
  "name": "node-grpc-server",
  "dependencies": {
    "@grpc/proto-loader": "^0.4.0",
    "google-protobuf": "^3.6.1",
    "grpc": "^1.18.0",
    "knex": "^0.16.3",
    "pg": "^7.8.0"
  },
  "scripts": {
    "start": "node index.js"
  }
}

کاربرد هرکدوم از این ماژول ها تو شکل پایینه.

@grpc/proto-loader: loads .proto files
google-protobuf: JavaScript version of the Protocol Buffers runtime library
grpc: Node gRPC Library
knex: SQL query builder for Node
pg: PostgreSQL client for Node

قبل از نصب پکیج ها باید protoc رو روی ماشین‌مون نصب کرده باشیم.از این لینک می‌تونید استفاده کنید.

بعد نصب protoc باید پیکج های سرور رو نصب کنیم.

$ cd server
$ npm install

بعد فایل knexfile.js رو در فولدر server که برای ست کردن کانفیگ دیتابیس و میگریشن ها هست رو ایجاد می‌کنیم.

const path = require('path');

module.exports = {
  development: {
    client: 'postgresql',
    connection: {
      host: '127.0.0.1',
      user: '',
      password: '',
      port: '5432',
      database: 'grpc_products',
    },
    pool: {
      min: 2,
      max: 10,
    },
    migrations: {
      directory: path.join(__dirname, 'db', 'migrations'),
    },
    seeds: {
      directory: path.join(__dirname, 'db', 'seeds'),
    },
  },
};

بعد از وارد کردن اطلاعات دیتابیس باید فولدری برای میگرشن ها و سیدرها بسازیم. ساختار پروژه بعد ساخت این فولدرها به شکل زیره.

├── client
│   ├── app.js
│   └── package.json
├── protos
│   └── product.proto
└── server
    ├── db
    │   ├── migrations
    │   └── seeds
    ├── index.js
    ├── knexfile.js
    ├── package-lock.json
    └── package.json

یک میگرشن جدید می‌سازیم.

$ ./node_modules/.bin/knex migrate:make products

و کدهای زیر رو داخلش قرار می‌دیم.

exports.up = function (knex, Promise) {
  return knex.schema.createTable('products', function (table) {
    table.increments();
    table.string('name').notNullable();
    table.string('price').notNullable();
  });
};

exports.down = function (knex, Promise) {
  return knex.schema.dropTable('products');
};


حالا یک سیدر می‌سازیم.

$ ./node_modules/.bin/knex seed:make product

و تعدای دیتای فیک می‌سازیم.

exports.seed = function (knex, Promise) {
  // Deletes ALL existing entries
  return knex('products').del()
    .then(function () {
      // Inserts seed entries
      return knex('products').insert([
        { name: 'pencil', price: '100' },
        { name: 'pen', price: '550' },
                { name: 'book', price: '98' },
      ]);
    });
};

و سیدر رو ران می‌کنیم تا دیتابیس پر شه.

$ ./node_modules/.bin/knex seed:run

حالا فایل index.js رو آپدیت می‌کنیم.

// requirements
const path = require('path');
const protoLoader = require('@grpc/proto-loader');
const grpc = require('grpc');

// knex
const environment = process.env.ENVIRONMENT || 'development';
const config = require('./knexfile.js')[environment];
const knex = require('knex')(config);

// grpc service definition
const productProtoPath = path.join(__dirname, '..', 'protos', 'product.proto');
const productProtoDefinition = protoLoader.loadSync(productProtoPath);
const productPackageDefinition = grpc.loadPackageDefinition(productProtoDefinition).product;
/*
Using an older version of gRPC?
(1) You won't need the @grpc/proto-loader package
(2) const productPackageDefinition = grpc.load(productProtoPath).product;
*/

// knex queries
function listProducts(call, callback) {}
function readProduct(call, callback) {}
function createProduct(call, callback) {}
function updateProduct(call, callback) {}
function deleteProduct(call, callback) {}

// main
function main() {
  const server = new grpc.Server();
  // gRPC service
  server.addService(productPackageDefinition.ProductService.service, {
    listProducts: listProducts,
    readProduct: readProduct,
    createProduct: createProduct,
    updateProduct: updateProduct,
    deleteProduct: deleteProduct,
  });
  // gRPC server
  server.bind('localhost:50051', grpc.ServerCredentials.createInsecure());
  server.start();
  console.log('gRPC server running at http://127.0.0.1:50051');
}

main();

برای تبدیل فایل پروتو به js از پیکج زیر استفاده می‌کنیم.

$ npm install -g grpc-tools

بعد از نصب پکیج تو روت پروژه کامند زیر رو ران می‌کنیم تا پروتو به js تبدیل شه.

$ protoc -I=. ./protos/product.proto \
  --js_out=import_style=commonjs,binary:./server \
  --grpc_out=./server \
  --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin`

بعد ران شدن کامند دو تا فایل در پوشه server/protos ساخته میشه. فعلا کار ما با سرور تمومه می‌تونیم با دستور npm start ن رو ران کنیم و اگر مشکلی نباشه باید پیام زیر رو ببینید.

gRPC server running at http://127.0.0.1:50051

حالا قبل از اینکه کویری بزنیم و اطلاعات رو فتچ کنیم بیاید کلاینت رو بسازیم.مثل سرور از فایل package.json شروع می‌کنیم.

{
  "name": "node-grpc-client",
  "dependencies": {
    "@grpc/proto-loader": "^0.4.0",
    "body-parser": "^1.18.3",
    "express": "^4.16.4",
    "google-protobuf": "^3.6.1",
    "grpc": "^1.18.0"
  },
  "scripts": {
    "start": "node app.js"
  }
}


پکیج ها رو نصب می‌کنیم و فایل app.js و آپدیت می‌کنیم.

// requirements
const express = require('express');
const bodyParser = require('body-parser');
const productRoutes = require('./routes/productRoutes');

// express
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

// routes
app.use('/api', productRoutes);

// run server
app.listen(3000, () => {
  console.log('Server listing on port 3000');
});

در اینجا یک سرور با اکسپرس به همراه روت‌ها تعریف کردیم.

هم چنین در فایل client/routes/grpcRoutes.js کدهای زیر رو اضافه می‌کنیم.

// requirements
const path = require('path');
const protoLoader = require('@grpc/proto-loader');
const grpc = require('grpc');

// gRPC client
const productProtoPath = path.join(__dirname, '..', '..', 'protos', 'product.proto');
const productProtoDefinition = protoLoader.loadSync(productProtoPath);
const productPackageDefinition = grpc.loadPackageDefinition(productProtoDefinition).product;
const client = new productPackageDefinition.ProductService(
  'localhost:50051', grpc.credentials.createInsecure());
/*
Using an older version of gRPC?
(1) You won't need the @grpc/proto-loader package
(2) const productPackageDefinition = grpc.load(productProtoPath).product;
(3) const client = new productPackageDefinition.ProductService(
  'localhost:50051', grpc.credentials.createInsecure());
*/

// handlers
const listProducts = (req, res) => {};
const readProduct = (req, res) => {};
const createProduct = (req, res) => {};
const updateProduct = (req, res) => {};
const deleteProduct = (req, res) => {};

module.exports = {
  listProducts,
  readProduct,
  createProduct,
  updateProduct,
  deleteProduct,
};

حالا با ران کردن کامند npm start باید پیام زیر رو ببینیم.

Server listing on port 3000
بعد از هر تغییری سرور و کلاینت رو ریستارت کنید.

حاال به سرور برگردیم و مثلا عملیات خوندن لیست کامل همه محصولات رو بنویسیم.

Server :

function listProducts(call, callback) {
  /*
  Using 'grpc.load'? Send back an array: 'callback(null, { data });'
  */
  knex('products')
    .then((data) => { callback(null, { products: data }); });
}

Client :

const listProducts = (req, res) => {
  /*
  gRPC method for reference:
  listProducts(Empty) returns (ProductList)
  */
  client.listProducts({}, (err, result) => {
    res.json(result);
  });
};

حالا اگر این اندپوینت رو تست کنیم باید دیتای محصولات که با سیدر ران کرده بویم ببینیم.

$ curl http://127.0.0.1:3000/api/products

خروجی باید به صورت زیر باشه.

{
  "products": [
    {
      "id": 1,
      "name": "pencil",
      "price": "100"
    },
    {
      "id": 2,
      "name": "pen",
      "price": "550"
    },
       {      
        "id": 3,  
         "name": "book", 
          "price": "98"  
           }
  ]
}

بقیه اندپوینت‌ها هم مشابه این ساخته میشه و میشه هر کنترلر دلخواهی روش نوشت.

اگر مشتاقید مقاله کامل این موضوع رو بخونید میتونید از لینک زیر پیداش کنید.

https://mherman.org/blog/node-grpc-postgres/