Protocole de communication frontal structuré Protobuf ou JSON?

image

Dans le nouveau projet de notre équipe, nous avons choisi le framework frontend VUE pour le nouveau produit, le backend est écrit en PHP et fonctionne avec succès depuis 17 ans maintenant.

Lorsque le code a commencé à croître, j'ai dû penser à simplifier l'échange de données avec le serveur, dont je vais parler.

À propos du backend


Le projet est assez grand et la fonctionnalité est très confuse, par conséquent, le code écrit en DDD avait certaines structures de données, elles étaient complexes et volumineuses pour une certaine universalité dans le projet dans son ensemble.

À propos de frontend


4 mois développement du front, nous avons utilisé JSON comme réponse du serveur, nous avons mappé dans State Vuex dans un format qui nous convient. Mais pour le retour au serveur, nous devions convertir dans le sens opposé, afin que le serveur puisse lire et mapper ses objets DTO (cela peut sembler étrange, mais cela devrait l'être :))

Problèmes


Il semble que ce ne soit rien, ils ont travaillé avec ce qui est, l'État est devenu de gros objets. Ils ont commencé à se décomposer en modules encore plus petits, dont chacun avait ses propres états, mutations, etc. ... L'API a commencé à changer après de nouvelles tâches des gestionnaires, et il est devenu de plus en plus difficile à gérer tout cela, il a été mis en correspondance erronée, les champs changé ...

Et nous sommes commencé à penser aux structures de données universelles sur le serveur et sur le devant pour éliminer les erreurs dans les analyses, les mappages, etc.

Après quelques recherches, nous sommes arrivés à deux options:

  1. tampons de protocole
  2. Génération automatique JS DTO côté serveur pour le front, avec un traitement JSON supplémentaire dans ces DTO.

Après avoir essayé le stylet, il était habituel d'utiliser Protobuf de Google.

Et c'est pourquoi:

  1. Il existe déjà une fonctionnalité qui compile les structures décrites pour de nombreuses plateformes, y compris pour PHP et pour JS.
  2. Il existe un générateur de documentation pour les structures créées .proto
  3. Vous pouvez facilement visser des versions pour les structures.
  4. La recherche d'objets est facilitée lors de la refactorisation en PHP et JS.
  5. Et d'autres puces comme gRPC, etc. si nécessaire.

Arrête de parler, voyons à quoi ça ressemble


Je ne décrirai pas à quoi ça ressemble du côté PHP, tout est à peu près le même là-bas, les objets sont les mêmes.

Je vais vous montrer un exemple d'un simple JS client et mini serveur sur Node.js.

Tout d'abord, nous décrivons les structures de données dont nous avons besoin. Doca .

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);
}

Je vais vous expliquer un peu le service, pourquoi il est nécessaire, voire utilisé. Le service n'est décrit que dans un souci de documentation dans notre cas, ce qu'il accepte et ce qu'il donne, afin que nous puissions substituer les objets nécessaires. Il n'est nécessaire que pour gRPC.

Ensuite, le générateur de code est téléchargé en fonction des structures.

Et la commande de génération s'exécute sous JS.

./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

Plus de détails dans le dock .

Après la génération, 3 fichiers JS apparaissent, dans lesquels tout est réduit en objets, avec la fonctionnalité de sérialisation vers le tampon et de désérialisation depuis le tampon.

price_pb.js
product_pb.js
service_pb.js

Ensuite, nous décrivons le code 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());

En principe, le client est prêt.

Nous exprimons sur le serveur

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!');
});

Qu'avons-nous au total


  1. Un seul point de vérité sous la forme d'objets générés basés sur des structures décrites une fois pour de nombreuses plateformes.
  2. Il n'y a pas de confusion, il y a une documentation claire à la fois sous forme de code HTML généré automatiquement et uniquement en affichant des fichiers .proto.
  3. Partout, des travaux sont en cours avec des entités spécifiques, sans leurs modifications, etc. (et nous savons tous que le frontend aime le gag :))
  4. Le fonctionnement très pratique de ce protocole est échangé sur des sockets Web.

Il y a bien sûr un petit bémol, c'est la vitesse de sérialisation et de désérialisation, en voici un exemple.

J'ai pris lorem ipsum pour 10 paragraphes, il s'est avéré 5,5 Ko de données, en tenant compte des objets remplis Prix, Produit. Et j'ai conduit des données sur Protobuf et JSON (tout de même, juste rempli de schémas JSON, au lieu d'objets 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

Merci à tous pour votre attention :)

All Articles