بروتوبوف أو JSON بروتوكول الاتصالات الأمامية المنظمة؟

صورة

في المشروع الجديد في فريقنا ، اخترنا إطار الواجهة الأمامية VUE للمنتج الجديد ، والواجهة الخلفية مكتوبة بلغة PHP ، وهي تعمل بنجاح منذ 17 عامًا.

عندما بدأ الكود في النمو ، كان علي أن أفكر في تبسيط تبادل البيانات مع الخادم ، والذي سأتحدث عنه.

حول الخلفية


المشروع كبير بما فيه الكفاية ، والوظائف مشوشة للغاية ، لذلك ، كان الرمز المكتوب في DDD بهياكل بيانات معينة ، كانت معقدة وضخمة لبعض العالمية في المشروع ككل.

حول الواجهة


4 اشهر تطوير الجبهة ، استخدمنا JSON كرد من الخادم ، قمنا بتعيينه في State Vuex بتنسيق مناسب لنا. ولكن من أجل العودة إلى الخادم ، كنا بحاجة إلى التحويل في الاتجاه المعاكس ، حتى يتمكن الخادم من قراءة كائنات DTO الخاصة به وتعيينها (قد يبدو الأمر غريبًا ، ولكن يجب أن يكون كذلك :))

مشاكل


يبدو أنه لا شيء ، عملوا مع ما ، نمت الدولة إلى أشياء كبيرة. بدأوا في الانقسام إلى وحدات أصغر ، كل منها له حالاته الخاصة ، طفراته ، وما إلى ذلك ... بدأت واجهة برمجة التطبيقات في التغيير بعد المهام الجديدة من المديرين ، وأصبح الأمر أكثر صعوبة في إدارة كل هذا ، ثم تم تعيينه بشكل خاطئ ، ثم تغيرت الحقول ...

وهنا نحن بدأ التفكير في هياكل البيانات العالمية على الخادم والواجهة للقضاء على الأخطاء في التحليل ، والتعيينات ، وما إلى ذلك.

بعد إجراء بعض البحث ، توصلنا إلى خيارين:

  1. المخازن المؤقتة للبروتوكول
  2. إنشاء JS DTO التلقائي من جانب الخادم للجهة الأمامية ، مع معالجة JSON إضافية في DTOs هذه.

بعد تجربة القلم ، كان من المعتاد استخدام Protobuf من google.

ولهذا السبب:

  1. هناك بالفعل وظيفة تجمع الهياكل الموصوفة للعديد من المنصات ، بما في ذلك PHP و JS.
  2. يوجد مولد توثيق للهياكل التي تم إنشاؤها. pro
  3. يمكنك بسهولة ربط بعض الإصدارات للهياكل.
  4. يتم تسهيل البحث عن الكائنات عند إعادة البناء في كل من PHP و JS.
  5. والرقائق الأخرى مثل gRPC ، وما إلى ذلك إذا لزم الأمر.

توقف عن الحديث ، دعنا نرى كيف يبدو كل شيء


لن أصف كيف يبدو من ناحية PHP ، فكل شيء متشابه هناك ، والأشياء متشابهة.

سأعرض لك مثالاً على عميل JS بسيط وخادم صغير على Node.js.

أولاً ، نحن نصف هياكل البيانات التي نحتاجها. دوكا .

product.proto

syntax = "proto3";

package api;

import "price.proto";

message Product {
    message Id {
        uint32 value = 1;
    }
    Id id = 1;
    string name = 2;
    string text = 3;
    string url = 4;
    Price price = 5;
}

price.proto

syntax = "proto3";
package api;

message Price {
    float value = 1;
    uint32 tax = 2;
}

service.proto

syntax = "proto3";

package api;

import "product.proto";

service ApiService {
    rpc getById (Product.Id) returns (Product);
}

سأشرح قليلاً عن الخدمة ، ولماذا هناك حاجة إليها ، حتى لو لم يتم استخدامها. يتم وصف الخدمة فقط من أجل التوثيق في حالتنا ، وما تقبله وما تقدمه ، حتى نتمكن من استبدال الأشياء الضرورية. هو مطلوب فقط لـ gRPC.

بعد ذلك ، يتم تنزيل منشئ التعليمات البرمجية بناءً على الهياكل.

ويعمل أمر التوليد تحت شبيبة.

./protoc --proto_path=/Users/user/dev/habr_protobuf/public/proto --js_out=import_style=commonjs,binary:/Users/user/dev/habr_protobuf/src/proto/ /Users/user/dev/habr_protobuf/public/proto/*.proto

مزيد من التفاصيل في قفص الاتهام .

بعد الجيل ، تظهر 3 ملفات JS ، حيث يتم تقليل كل شيء إلى كائنات ، مع وظيفة التسلسل إلى المخزن المؤقت وإلغاء التسلسل من المخزن المؤقت.

price_pb.js
product_pb.js
service_pb.js

بعد ذلك ، سنصف شفرة JS.

import { Product } from '../proto/product_pb';

//         Product.Id
const instance = new Product.Id().setValue(12345);
let message = instance.serializeBinary();

let response = await fetch('http://localhost:3008/api/getById', {
    method: 'POST',
    body: message
});

let result = await response.arrayBuffer();

//     Product,   ,    , 
//      .
const data = Product.deserializeBinary(result);
console.log(data.toObject());

من حيث المبدأ ، العميل جاهز.

نعبر عن الخادم

const express = require('express');
const cors = require('cors');

const app = express();
app.use(cors());

//           .
const Product = require('./src/proto/product_pb').Product;
const Price = require('./src/proto/price_pb').Price;

//    , .     .
app.use (function(req, res, next) {
  let data = [];
  req.on('data', function(chunk) {
    data.push(chunk);
  });
  req.on('end', function() {
    if (data.length <= 0 ) return next();
    data = Buffer.concat(data);
    console.log('Received buffer', data);
    req.raw = data;
    next();
  })
});

app.post('/api/getById', function (req, res) {
  //     Product.Id,   
  const prId = Product.Id.deserializeBinary(req.raw);
  const id = prId.toObject().value;

  //   " "      
  const product = new Product();
  product.setId(new Product.Id().setValue(id));
  product.setName('Sony PSP');
  product.setUrl('http://mysite.ru/product/psp/');

  const price = new Price();
  price.setValue(35500.00);
  price.setTax(20);

  product.setPrice(price);

  //       Product
  res.send(Buffer.from(product.serializeBinary()));
});

app.listen(3008, function () {
  console.log('Example app listening on port 3008!');
});

ماذا لدينا في المجموع


  1. نقطة واحدة من الحقيقة في شكل كائنات ولدت على أساس الهياكل التي تم وصفها مرة واحدة للعديد من المنصات.
  2. لا يوجد ارتباك ، فهناك وثائق واضحة في شكل HTML تم إنشاؤه تلقائيًا وعرض ملفات .proto فقط.
  3. في كل مكان ، يتم العمل مع كيانات محددة ، دون تعديلات ، وما إلى ذلك (ونعلم جميعًا أن الواجهة الأمامية تحب الكمامات :))
  4. يتم تبادل تشغيل هذا البروتوكول بشكل مريح للغاية عبر مقابس الويب.

هناك بالطبع ناقص صغير ، هذه هي سرعة التسلسل وإلغاء التسلسل ، هنا مثال.

لقد أخذت lorem ipsum لـ 10 فقرات ، وتبين أن 5.5 كيلو بايت من البيانات ، مع الأخذ في الاعتبار السعر المملوء ، المنتج. وقمت بقيادة البيانات على Protobuf و JSON (كل نفس ، تم ملؤها للتو في مخططات JSON ، بدلاً من كائنات Protobuf)

   

Protobuf parsing

client
2.804999ms
1.8150000ms
0.744999ms
server
1.993ms
0.495ms
0.412ms

JSON

client
0.654999ms
0.770000ms
0.819999ms

server
0.441ms
0.307ms
0.242ms

شكرا لكم جميعا على اهتمامكم :)

All Articles