JSON ya no es genial:implementar búferes de protocolo en Node.js

Existe una mejor alternativa al ubicuo JSON como protocolo de comunicación de la web. Son búferes de protocolo (protobuf). En pocas palabras, protobuf ofrece un formato más denso (procesamiento más rápido) y proporciona esquemas de datos (aplicación de la estructura y mejor compatibilidad con el código antiguo).

Los buffers de protocolo fueron introducidos por Google. Puede leer más sobre ellos en la Guía oficial para desarrolladores de Protocol Buffers. Para algo más breve, lea 5 razones para usar búferes de protocolo en lugar de JSON para su próximo servicio, que le brindará una descripción general rápida de los beneficios de protobuf sobre JSON.

El propósito de este artículo no es resaltar por qué los protobufs son mejores o venderle el concepto. Hay muchos artículos en línea que lo harán por ti. El propósito de este artículo es mostrarle cómo puede comenzar con este formato en el entorno Node.js.

Este artículo lo guiará a través de una implementación de API RESTful de búferes de protocolo con Node.js, Express.js, Axios y Protobuf.js. El código de esta publicación se ejecuta en Node v6.2.0 porque está escrito en la versión de vanguardia ES6/ES2015 del lenguaje JavaScript. Tendremos un mensaje que consiste en dos campos text y lang enviado como protobuf desde el servidor, decodificado y mostrado en el navegador. También tendremos un botón que enviará otro mensaje protobuf al servidor. El código fuente está en el repositorio de GitHub azat-co/proto-buffer-api.

Esta será la estructura de nuestro proyecto:

/proto-buffer-api
  /public
    axios.min.js
    bytebuffer.js
    index.html
    long.js
    message.proto
    protobuf.js
  /node_modules
  index.js
  package.json

El public La carpeta es donde residirán todos los activos de nuestro navegador. Tenemos Axios para hacer solicitudes HTTP desde el navegador al servidor. Es similar a Superagent o Request. También puede usar jQuery para realizar solicitudes HTTP. Si va a utilizar una biblioteca que no sea Axios, solo asegúrese de enviar datos como ArrayBuffer y enviarlos como application/octet-stream .

Protobuf.js es la biblioteca para trabajar con los búferes de protocolo de Google en JavaScript y Node.js, por lo que necesitaremos protobuf.js archivo en el navegador. Requiere soporte para números largos (los números en JavaScript están limitados a un tamaño de 53 bits, como sabe) y hay una biblioteca ordenada que nos permite trabajar con números enteros de 64 bits llamados long.js.

El message.proto es el prototipo (esquema) del objeto de mensaje que enviaremos desde el servidor al navegador y viceversa. Se ve así:

message Message {
    required string text = 1;
    required string lang = 2;
}

Protobuf.js requiere una dependencia más:bytebuffer.js para el tipo de datos ArrayBuffer.

El formato es relativamente fácil de entender. Tenemos dos campos text y lang . Ambos son campos obligatorios. Los números junto a los nombres de campo son algo que los búferes de protocolo necesitan para decodificar/codificar.

El index.html tiene HTML mínimo que contiene bibliotecas incluidas, <pre> contenedor donde insertaremos la respuesta del servidor, el botón que activa sendMessage() (lo escribiremos más tarde), y el <script> etiqueta con solicitudes y código protobuf.

<html>
  <head>
    <script src="long.js"></script>
    <script src="bytebuffer.js"></script>
    <script src="protobuf.js"></script>
    <script src="axios.min.js"></script>
  </head>
  <body>
    <pre id="content"></pre>
    <button onClick="sendMessage()">send message to server</button>
    <script type="text/javascript">
        // Our requests and Protobuf code
    </script>
  </body>
</html>

Profundicemos más en el JavaScript del navegador e implementemos dos solicitudes:una solicitud GET para obtener un mensaje del servidor y una solicitud POST para enviar un mensaje al servidor. Ambos tendrán que trabajar con búferes de protocolo.

En primer lugar, creamos Message de nuestro archivo prototipo message.proto . En la devolución de llamada de loadProtoFile podemos invocar loadMessage() para realizar la solicitud GET al servidor.

[Nota al margen]

Leer publicaciones de blog es bueno, pero ver cursos en video es aún mejor porque son más atractivos.

Muchos desarrolladores se quejaron de la falta de material de video de calidad asequible en Node. Es una distracción ver videos de YouTube y una locura pagar $ 500 por un curso de video de Node.

Visite Node University, que tiene cursos de video GRATUITOS en Node:node.university.

[Fin de la nota al margen]

"use strict";
let ProtoBuf = dcodeIO.ProtoBuf
let Message = ProtoBuf
  .loadProtoFile('./message.proto', (err, builder)=>{
    Message = builder.build('Message')
    loadMessage()
  })

La biblioteca Axios toma como primer argumento la URL de la solicitud y como segundo las opciones de la solicitud. Una de las opciones que debemos proporcionar es arraybuffer . Esto le indicará al agente HTTP que nos devuelva el tipo de datos apropiado. Axios funciona con promesas, por lo que en then devolución de llamada, podemos obtener response , regístrelo y decodifique usando Message.decode() :

let loadMessage = ()=> {
  axios.get('/api/messages', {responseType: 'arraybuffer'})
    .then(function (response) {
      console.log('Response from the server: ', response)
      let msg = Message.decode(response.data)
      console.log('Decoded message', msg)
      document.getElementById('content').innerText = JSON.stringify(msg, null, 2)
    })
    .catch(function (response) {
      console.log(response)
    })
}

El resultado de la solicitud GET se muestra en DevTools en la siguiente captura de pantalla. Puedes observar que la respuesta está en application/octet-stream :

En cuanto al envío de los búferes de protocolo al servidor, asegúrese de crear un objeto con new Message(data) y luego invoque msg.toArrayBuffer() . Es una buena idea configurar el Content-Type encabezado a application/octet-stream para que el servidor conozca el formato de los datos entrantes:

let sendMessage = ()=>{
  let msg = new Message({text: 'yo', lang: 'slang'})
  axios.post('/api/messages', msg.toArrayBuffer(),
      { responseType: 'arraybuffer',
      headers: {'Content-Type': 'application/octet-stream'}}
    ).then(function (response) {
      console.log(response)
    })
    .catch(function (response) {
      console.log(response)
    })
}

El resultado de POST con el Content-Type apropiado y la carga útil se muestra en la siguiente captura de pantalla:

Tenemos el front-end terminado, pero no funcionará con nuestro código del servidor, así que implementemos el código Node/Express a continuación. En primer lugar, querrá crear el package.json . Siéntase libre de copiar este archivo que tiene las dependencias:

{
  "name": "proto-buffer-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Azat Mardan",
  "license": "MIT",
  "dependencies": {
    "express": "^4.13.4",
    "protobufjs": "^5.0.1"
  }
}

Una vez que tengas package.json , puede instalar las dependencias con npm i . Instalará express para construir el servidor HTTP y protobufjs para trabajar con Protocol Buffers en el servidor.

Primero implementemos el código del servidor. En index.js , importamos dependencias, creamos el express objeto y aplicar el middleware estático para el public carpeta:

let path = require('path')
let express = require('express')
let app = express()
let publicFolderName = 'public'
app.use(express.static(publicFolderName))

A continuación, usaremos un almacén en memoria para simplificar este proyecto. En otras palabras, los datos para la solicitud GET provendrán de una matriz:

let messages = [
  {text: 'hey', lang: 'english'},
  {text: 'isänme', lang: 'tatar'},
  {text: 'hej', lang: 'swedish'}
]

Por lo general, usaría body-parser para analizar solicitudes JSON. Para procesar correctamente el protobuf entrante, debemos analizarlo como una matriz de búferes. Implementemos nuestro propio middleware personalizado para analizar protobufs y almacenarlos en body.raw (el patrón decorador). Necesitamos crear body.raw solo cuando el encabezado Content-Type es application/octet-stream y cuando hay datos (data.length>0 ):

app.use (function(req, res, next) {
  if (!req.is('application/octet-stream')) return next()
  var data = [] // List of Buffer objects
  req.on('data', function(chunk) {
      data.push(chunk) // Append Buffer object
  })
  req.on('end', function() {
    if (data.length <= 0 ) return next()
    data = Buffer.concat(data) // Make one large Buffer of it
    console.log('Received buffer', data)
    req.raw = data
    next()
  })
})

Ahora podemos crear el objeto constructor y el mensaje "construir" desde nuestro archivo prototipo. Usamos el mismo archivo prototipo public/message.proto como nuestro código de front-end:

let ProtoBuf = require('protobufjs')
let builder = ProtoBuf.loadProtoFile(
  path.join(__dirname,
  publicFolderName,
  'message.proto')
)
let Message = builder.build('Message')

Ahora podemos implementar GET en el que creamos un nuevo mensaje, lo codificamos y lo convertimos al tipo Buffer antes de enviarlo de vuelta al cliente front-end. response.send() de Express se está ocupando de agregar el 'Tipo de contenido' adecuado. Puedes usar response.end() también:

app.get('/api/messages', (req, res, next)=>{
  let msg = new Message(messages[Math.round(Math.random()*2)])
  console.log('Encode and decode: ',
    Message.decode(msg.encode().toBuffer()))
  console.log('Buffer we are sending: ', msg.encode().toBuffer())
  // res.end(msg.encode().toBuffer(), 'binary') // alternative
  res.send(msg.encode().toBuffer())
  // res.end(Buffer.from(msg.toArrayBuffer()), 'binary') // alternative
})

En el controlador de solicitudes POST, decodificamos desde body.raw (se llena con el middleware que definimos anteriormente) e inicie sesión en la terminal:

app.post('/api/messages', (req, res, next)=>{
  if (req.raw) {
    try {
        // Decode the Message
      var msg = Message.decode(req.raw)
      console.log('Received "%s" in %s', msg.text, msg.lang)
    } catch (err) {
      console.log('Processing failed:', err)
      next(err)
    }
  } else {
    console.log("Not binary data")
  }
})

app.all('*', (req, res)=>{
  res.status(400).send('Not supported')
})

app.listen(3000)

Si escribiste todo el código como yo lo hice, o copiaste de mi repositorio de GitHub azat-co/proto-buffer-api, deberías ver en una página web un mensaje aleatorio del servidor. Luego, si hace clic en el botón, debería ver "yo" en la terminal/símbolo del sistema donde se ejecuta su servidor Node.js.

Eso es todo. Implementamos GET y POST para comunicarse en los búferes de protocolo entre Node.js/Express.js y JavaScript del navegador con Protobuf.js. Utilizamos Axios para realizar solicitudes HTTP desde el navegador. Nos permitió trabajar con promesas y abstraer parte de la interfaz XMLHttpRequest de bajo nivel para trabajar con datos binarios.

Google está usando Protocol Buffers para su API. Protobufs en muchos sentidos es superior a JSON o XML, y con Protobuf.js y este breve tutorial debería estar listo para comenzar a usar Protocol Buffers para sus API RESTful.