Protocolo de comunicação front-end estruturado Protobuf ou JSON?

imagem

No novo projeto de nossa equipe, escolhemos a estrutura de front-end VUE para o novo produto, o back-end é escrito em PHP e trabalha com sucesso há 17 anos.

Quando o código começou a crescer, tive que pensar em simplificar a troca de dados com o servidor, sobre o qual falarei.

Sobre back-end


O projeto é grande o suficiente e a funcionalidade é muito confusa; portanto, o código escrito em DDD tinha certas estruturas de dados, elas eram complexas e volumosas para alguma universalidade do projeto como um todo.

Sobre o frontend


4 meses No desenvolvimento da frente, usamos JSON como resposta do servidor, mapeamos no State Vuex em um formato conveniente para nós. Mas, para o retorno ao servidor, precisávamos converter na direção oposta, para que o servidor pudesse ler e mapear seus objetos DTO (pode parecer estranho, mas deve ser assim :))

Problemas


Parece não ser nada, eles trabalharam com o que é, o estado cresceu para objetos grandes. Eles começaram a dividir-se em módulos ainda menores, cada qual com seus próprios estados, mutações, etc ... A API começou a mudar depois de novas tarefas de gerentes, e tornou-se mais e mais difícil de gerir tudo isso, errado, então ele foi mapeado, os campos mudou ...

E aqui estamos nós começou a pensar em estruturas de dados universais no servidor e na frente para eliminar erros em análises, mapeamentos etc.

Após algumas pesquisas, chegamos a duas opções:

  1. buffers de protocolo
  2. Geração automática de JS DTO do lado do servidor para a frente, com processamento JSON adicional nesses DTOs.

Depois de experimentar a caneta, era costume usar o Protobuf do google.

E é por causa disso:

  1. Já existe um funcional que compila as estruturas descritas para muitas plataformas, inclusive para PHP e JS.
  2. Existe um gerador de documentação para estruturas criadas .proto
  3. Você pode facilmente danificar algumas versões para estruturas.
  4. A pesquisa de objetos é facilitada ao refatorar em PHP e JS.
  5. E outros chips como gRPC, etc., se necessário.

Pare de falar, vamos ver como tudo fica


Não vou descrever como fica do lado do PHP, tudo fica igual, os objetos são iguais.

Vou mostrar um exemplo de um cliente JS simples e um mini servidor no Node.js.

Primeiro, descrevemos as estruturas de dados que precisamos. 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);
}

Vou explicar um pouco sobre o serviço, por que ele é necessário, se não usado. O serviço é descrito apenas para fins de documentação em nosso caso, o que ele aceita e o que oferece, para que possamos substituir os objetos necessários. Só é necessário para o gRPC.

Em seguida, o gerador de código é baixado com base nas estruturas.

E o comando de geração é executado em 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

Mais detalhes no banco dos réus .

Após a geração, aparecem 3 arquivos JS, nos quais tudo é reduzido a objetos, com a funcionalidade de serialização para o buffer e desserialização do buffer.

price_pb.js
product_pb.js
service_pb.js

Em seguida, descrevemos o código 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());

Em princípio, o cliente está pronto.

Expressamos no servidor

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

O que temos no total


  1. Um único ponto de verdade na forma de objetos gerados com base em estruturas que são descritas uma vez para muitas plataformas.
  2. Não há confusão, há documentação clara, tanto na forma de HTML gerado automaticamente quanto na visualização de arquivos .proto.
  3. Em todos os lugares, o trabalho está em andamento com entidades específicas, sem suas modificações, etc. (e todos sabemos que o frontend adora vomitar :))
  4. A operação muito conveniente desse protocolo é trocada por soquetes da web.

É claro que há um pequeno sinal de menos, essa é a velocidade de serialização e desserialização, aqui está um exemplo.

Tomei lorem ipsum por 10 parágrafos, resultou em 5,5kb de dados, levando em consideração os objetos preenchidos Preço, Produto. E eu dirigi dados sobre Protobuf e JSON (todos os mesmos, apenas preenchi esquemas JSON, em vez de objetos 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

Obrigado a todos pela atenção :)

All Articles