Comunicación entre procesos de Nodejs

1. El
nodo de escenario se ejecuta en un solo subproceso, pero esto no significa que no se puedan utilizar las ventajas del multiproceso de múltiples núcleos / múltiples máquinas

De hecho, Node consideró inicialmente el escenario de red distribuida desde el diseño:

Node is a single-threaded, single-process system which enforces shared-nothing design with OS process boundaries. It has rather good libraries for networking. I believe this to be a basis for designing very large distributed programs. The “nodes” need to be organized: given a communication protocol, told how to connect to each other. In the next couple months we are working on libraries for Node that allow these networks.

PD: Por la razón por la que Node se llama Node, consulte ¿Por qué Node.js se llama Node.js?

2. El
método de comunicación para crear un proceso está relacionado con el método de generación del proceso, y Node tiene 4 formas de crear un proceso: spawn (), exec (), execFile () y fork ()


spawn
const { spawn } = require('child_process');
const child = spawn('pwd');
// 带参数的形式
// const child = spawn('find', ['.', '-type', 'f']);

spawn () devuelve una instancia de ChildProcess. ChildProcess también proporciona algunos eventos basados ​​en el mecanismo de eventos (API EventEmitter):

salida: se activa cuando el proceso hijo sale, puede conocer el estado de salida del proceso (código y señal)

desconectar: ​​se activa cuando el proceso padre llama a child.disconnect ()

error: el proceso hijo no se pudo crear o desencadenar cuando se eliminó

cerrar: se activa cuando se cierra el flujo stdio (flujo estándar de entrada y salida) del proceso secundario

mensaje: se activa cuando el proceso hijo envía un mensaje a través de process.send (), los procesos padre e hijo pueden comunicarse a través de este mecanismo de mensaje incorporado

Se puede acceder a la secuencia stdio del proceso secundario a través de child.stdin, child.stdout y child.stderr. Cuando estas secuencias se cierran, el proceso secundario activará el evento de cierre

La diferencia entre PSclose y exit se refleja principalmente en el escenario donde múltiples procesos comparten el mismo flujo stdio La salida de un proceso no significa que el flujo stdio esté cerrado.

En el proceso hijo, stdout / stderr tiene características de lectura, mientras que stdin tiene características de escritura, que es lo opuesto al proceso principal:


child.stdout.on('data', (data) => {
  console.log(`child stdout:\n${data}`);
});

child.stderr.on('data', (data) => {
  console.error(`child stderr:\n${data}`);
});

Con las características de la canalización del flujo estándar del proceso, puede lograr cosas más complejas, como:


const { spawn } = require('child_process');

const find = spawn('find', ['.', '-type', 'f']);
const wc = spawn('wc', ['-l']);

find.stdout.pipe(wc.stdin);

wc.stdout.on('data', (data) => {
  console.log(`Number of files ${data}`);
});

El efecto es equivalente a find. -Type f | wc -l, cuenta recursivamente el número de archivos en el directorio actual

Opción de IPC
Además, el mecanismo de IPC se puede establecer mediante la opción stdio del método spawn ():


const { spawn } = require('child_process');

const child = spawn('node', ['./ipc-child.js'], { stdio: [null, null, null, 'ipc'] });
child.on('message', (m) => {
  console.log(m);
});
child.send('Here Here');

// ./ipc-child.js
process.on('message', (m) => {
  process.send(`< ${m}`);
  process.send('> 不要回答x3');
});

Para obtener más información sobre las opciones de IPC de spawn (), consulte options.stdio

El
método exec spawn () no crea un shell por defecto para ejecutar el comando entrante (por lo que el rendimiento es ligeramente mejor), mientras que el método exec () crea un shell. Además, exec () no se basa en la secuencia, pero almacena temporalmente el resultado de la ejecución del comando entrante en el búfer y luego lo pasa a la función de devolución de llamada.

La característica del método exec () es que es totalmente compatible con la sintaxis de shell y puede pasar directamente cualquier script de shell, por ejemplo:


const { exec } = require('child_process');

exec('find . -type f | wc -l', (err, stdout, stderr) => {
  if (err) {
    console.error(`exec error: ${err}`);
    return;
  }

  console.log(`Number of files ${stdout}`);
});

Sin embargo, el método exec () también tiene el riesgo de seguridad de la inyección de comandos. Preste especial atención a los escenarios que contienen contenido dinámico, como la entrada del usuario. Por lo tanto, el escenario aplicable del método exec () es: desea usar la sintaxis de shell directamente y el volumen de datos de salida esperado no es grande (no hay presión de memoria)

Entonces, ¿hay alguna manera que no solo admita la sintaxis de shell, sino que también tenga las ventajas de stream IO?

Tener. Lo mejor de ambos mundos es el siguiente:


const { spawn } = require('child_process');
const child = spawn('find . -type f | wc -l', {
  shell: true
});
child.stdout.pipe(process.stdout);

Active la opción de shell de spawn () y simplemente conecte la salida estándar del proceso hijo a la entrada estándar del proceso actual a través del método pipe (), para que pueda ver el resultado de la ejecución del comando. En realidad, existe una forma más sencilla:


const { spawn } = require('child_process');
process.stdout.on('data', (data) => {
  console.log(data);
});
const child = spawn('find . -type f | wc -l', {
  shell: true,
  stdio: 'inherit'
});

stdio: 'heredar' permite que el proceso hijo herede la entrada y salida estándar del proceso actual (compartir stdin, stdout y stderr), por lo que el ejemplo anterior puede obtener el resultado de salida del proceso hijo al monitorear el evento de datos del proceso actual.

Además, además de las opciones stdio y shell, spawn () también admite algunas otras opciones, como:


const child = spawn('find . -type f | wc -l', {
  stdio: 'inherit',
  shell: true,
  // 修改环境变量,默认process.env
  env: { HOME: '/tmp/xxx' },
  // 改变当前工作目录
  cwd: '/tmp',
  // 作为独立进程存在
  detached: true
});

Tenga en cuenta que, además de pasar datos al proceso hijo en forma de variables de entorno, la opción env también se puede utilizar para implementar el aislamiento de variables de entorno de estilo sandbox. De forma predeterminada, process.env se utiliza como el conjunto de variables de entorno del proceso hijo, y el proceso hijo puede acceder al mismo que el proceso actual. Todas las variables de entorno, si especifica un objeto personalizado como el conjunto de variables de entorno del proceso hijo como en el ejemplo anterior, el proceso hijo no puede acceder a otras variables de entorno

Por lo tanto, si desea agregar / eliminar variables de entorno, debe hacer esto:


var spawn_env = JSON.parse(JSON.stringify(process.env));

// remove those env vars
delete spawn_env.ATOM_SHELL_INTERNAL_RUN_AS_NODE;
delete spawn_env.ELECTRON_RUN_AS_NODE;

var sp = spawn(command, ['.'], {cwd: cwd, env: spawn_env});

La opción separada es más interesante:


const { spawn } = require('child_process');

const child = spawn('node', ['stuff.js'], {
  detached: true,
  stdio: 'ignore'
});

child.unref();

El comportamiento del proceso independiente creado de esta manera depende del sistema operativo. El proceso hijo separado en Windows tendrá su propia ventana de consola, mientras que el proceso en Linux creará un nuevo grupo de procesos (esta función se puede utilizar para administrar la familia de procesos hijo y lograr resultados similares Características de la muerte de árboles)

El método unref () se utiliza para romper la relación, de modo que el proceso "principal" pueda salir de forma independiente (no hará que el proceso secundario se cierre), pero tenga en cuenta que el stdio del proceso secundario también debe ser independiente del proceso "principal" en este momento, de lo contrario, el proceso "principal" El proceso hijo aún se verá afectado después de salir


execFile
const { execFile } = require('child_process');
const child = execFile('node', ['--version'], (error, stdout, stderr) => {
  if (error) {
    throw error;
  }
  console.log(stdout);
});

Similar al método exec (), pero no se ejecuta a través del shell (por lo que el rendimiento es un poco mejor), por lo que es necesario pasar el archivo ejecutable. Algunos archivos en Windows no se pueden ejecutar directamente, como .bat y .cmd, estos archivos no se pueden ejecutar con execFile (), solo con exec () o spawn () con la opción de shell activada

PS no está basado en flujo como exec (), y también existe el riesgo de volumen de datos de salida

xxxSync
spawn, exec y execFile tienen versiones de bloqueo sincrónico correspondientes, espere hasta que salga el proceso hijo

const { 
  spawnSync, 
  execSync, 
  execFileSync,
} = require('child_process');

Los métodos síncronos se utilizan para simplificar las tareas del script, como el proceso de inicio. Estos métodos deben evitarse en otros momentos

Fork
fork () es una variante de spawn (), que se utiliza para crear un proceso Node. La característica más importante es que el proceso padre-hijo tiene su propio mecanismo de comunicación (canalización IPC):


The child_process.fork() method is a special case of child_process.spawn() used specifically to spawn new Node.js processes. Like child_process.spawn(), a ChildProcess object is returned. The returned ChildProcess will have an additional communication channel built-in that allows messages to be passed back and forth between the parent and child. See subprocess.send() for details.

P.ej:


var n = child_process.fork('./child.js');
n.on('message', function(m) {
  console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });

// ./child.js
process.on('message', function(m) {
  console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });

Debido a que fork () tiene las ventajas de su propio mecanismo de comunicación, es especialmente adecuado para dividir la lógica que consume mucho tiempo, como:


const http = require('http');
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i;
  };
  return sum;
};
const server = http.createServer();
server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const sum = longComputation();
    return res.end(`Sum is ${sum}`);
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

El problema fatal con esto es que una vez que alguien visita / computa, las solicitudes posteriores no se pueden procesar a tiempo, porque el bucle de eventos aún está bloqueado por longComputation, y la capacidad del servicio no se puede restaurar hasta que finalice el cálculo que consume mucho tiempo.

Para evitar que las operaciones que consumen mucho tiempo bloqueen el bucle de eventos del proceso principal, longComputation () se puede dividir en procesos secundarios:


// compute.js
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i;
  };
  return sum;
};

// 开关,收到消息才开始做
process.on('message', (msg) => {
  const sum = longComputation();
  process.send(sum);
});

El proceso principal abre el proceso hijo para ejecutar longComputation:


const http = require('http');
const { fork } = require('child_process');

const server = http.createServer();

server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const compute = fork('compute.js');
    compute.send('start');
    compute.on('message', sum => {
      res.end(`Sum is ${sum}`);
    });
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

El bucle de eventos del proceso principal ya no estará bloqueado por cálculos que consumen mucho tiempo, pero la cantidad de procesos debe limitarse aún más; de lo contrario, la capacidad del servicio aún se verá afectada cuando el proceso agote los recursos.

PS De hecho, el módulo de clúster es una encapsulación de las capacidades de servicio multiproceso. La idea es similar a este ejemplo simple

3. Método de comunicación
1. Pase json
stdin / stdout y una carga útil JSON a través de stdin / stdout

El medio de comunicación más directo, el proceso del niño para obtener el control, puede visitar su flujo de stdio, y luego sobre
un tipo determinado de formato de mensaje comenzó felizmente la comunicación:


const { spawn } = require('child_process');

child = spawn('node', ['./stdio-child.js']);
child.stdout.setEncoding('utf8');
// 父进程-发
child.stdin.write(JSON.stringify({
  type: 'handshake',
  payload: '你好吖'
}));
// 父进程-收
child.stdout.on('data', function (chunk) {
  let data = chunk.toString();
  let message = JSON.parse(data);
  console.log(`${message.type} ${message.payload}`);
});

El proceso hijo es similar:


// ./stdio-child.js
// 子进程-收
process.stdin.on('data', (chunk) => {
  let data = chunk.toString();
  let message = JSON.parse(data);
  switch (message.type) {
    case 'handshake':
      // 子进程-发
      process.stdout.write(JSON.stringify({
        type: 'message',
        payload: message.payload + ' : hoho'
      }));
      break;
    default:
      break;
  }
});

La comunicación entre procesos del código PSVS utiliza este método, consulte la API de acceso electrónico desde la extensión vscode para obtener más detalles

La limitación obvia es que necesita controlar el proceso "secundario" y dos procesos completamente independientes no pueden comunicarse de esta manera (como escenarios de aplicaciones cruzadas o incluso de máquinas cruzadas)

PD Para obtener más información sobre la transmisión y la canalización, consulte la transmisión en el nodo

2. Native IPC admite
ejemplos como spawn () y fork (), y los procesos pueden comunicarse entre sí a través del mecanismo integrado de IPC

Proceso padre:

process.on ('mensaje') 收

child.send () envía

Proceso hijo:

process.on ('mensaje') 收

process.send () envía

Las restricciones son las mismas que las anteriores, y una de las partes debe poder controlar a la otra.

3. Los sockets
utilizan la red para completar la comunicación entre procesos, no solo entre procesos, sino también entre máquinas.

node-ipc usa este esquema, por ejemplo:


// server
const ipc=require('../../../node-ipc');

ipc.config.id = 'world';
ipc.config.retry= 1500;
ipc.config.maxConnections=1;

ipc.serveNet(
    function(){
        ipc.server.on(
            'message',
            function(data,socket){
                ipc.log('got a message : ', data);
                ipc.server.emit(
                    socket,
                    'message',
                    data+' world!'
                );
            }
        );

        ipc.server.on(
            'socket.disconnected',
            function(data,socket){
                console.log('DISCONNECTED\n\n',arguments);
            }
        );
    }
);
ipc.server.on(
    'error',
    function(err){
        ipc.log('Got an ERROR!',err);
    }
);
ipc.server.start();

// client
const ipc=require('node-ipc');

ipc.config.id = 'hello';
ipc.config.retry= 1500;

ipc.connectToNet(
    'world',
    function(){
        ipc.of.world.on(
            'connect',
            function(){
                ipc.log('## connected to world ##', ipc.config.delay);
                ipc.of.world.emit(
                    'message',
                    'hello'
                );
            }
        );
        ipc.of.world.on(
            'disconnect',
            function(){
                ipc.log('disconnected from world');
            }
        );
        ipc.of.world.on(
            'message',
            function(data){
                ipc.log('got a message from world : ', data);
            }
        );
    }
);

PD Para obtener más ejemplos, consulte RIAEvangelist / node-ipc

Por supuesto, es un desperdicio de rendimiento completar la comunicación entre procesos a través de la red en un escenario independiente, pero la ventaja de la comunicación de red radica en la compatibilidad entre entornos y otros escenarios RPC.

4. Tanto
los procesos padre como hijo de la cola de mensajes se comunican a través de un mecanismo de mensaje externo, y la capacidad de proceso cruzado depende del soporte de MQ

Es decir, no hay comunicación directa entre procesos, pero a través de la capa intermedia (MQ), agregar una capa de control puede ganar más flexibilidad y ventajas:

Estabilidad: el mecanismo de mensajes proporciona sólidas garantías de estabilidad, como la confirmación de la entrega (ACK de recepción del mensaje), retransmisión en caso de falla / prevención de múltiples transmisiones, etc.

Control de prioridad: permite ajustar el orden de respuesta de los mensajes

Capacidad sin conexión: los mensajes se pueden almacenar en caché

Procesamiento de mensajes transaccionales: combine mensajes relacionados en transacciones para garantizar su orden de entrega e integridad

¿PD no es fácil de lograr? ¿Se puede resolver con una capa ?, si no, solo con dos capas ...

Los más populares son smrchy / rsmq, por ejemplo:


// init
RedisSMQ = require("rsmq");
rsmq = new RedisSMQ( {host: "127.0.0.1", port: 6379, ns: "rsmq"} );
// create queue
rsmq.createQueue({qname:"myqueue"}, function (err, resp) {
    if (resp===1) {
      console.log("queue created")
    }
});
// send message
rsmq.sendMessage({qname:"myqueue", message:"Hello World"}, function (err, resp) {
  if (resp) {
    console.log("Message sent. ID:", resp);
  }
});
// receive message
rsmq.receiveMessage({qname:"myqueue"}, function (err, resp) {
  if (resp.id) {
    console.log("Message received.", resp)  
  }
  else {
    console.log("No messages for me...")
  }
});

Se configurará un servidor Redis. Los principios básicos son los siguientes:


Using a shared Redis server multiple Node.js processes can send / receive messages.

La recepción / envío / almacenamiento en caché / persistencia de mensajes depende de las capacidades proporcionadas por Redis, y sobre esta base se implementa un mecanismo de cola completo

5. La
idea básica de Redis es similar a la de la cola de mensajes:


Use Redis as a message bus/broker.

Redis tiene su propio mecanismo Pub / Sub (es decir, modo de publicación-suscripción), que es adecuado para escenarios de comunicación simples, como escenarios uno a uno o uno a muchos que no se preocupan por la confiabilidad del mensaje.

Además, Redis tiene una estructura de lista, que se puede usar como cola de mensajes para mejorar la confiabilidad de los mensajes. El enfoque general es producir mensajes LPUSH y consumir mensajes BRPOP. Es adecuado para escenarios de comunicación simples que requieren confiabilidad del mensaje, pero la desventaja es que el mensaje no tiene estado ni mecanismo ACK, lo que no puede cumplir con los complejos requisitos de comunicación.

PSRedis 的 Pub / Sub 示例 见 ¿Cuál es la biblioteca / método de comunicación entre procesos de node.js más eficiente?

4. Resumen
Hay 4 formas de comunicarse entre los procesos de nodo:

Pasar json a través de stdin / stdout: la forma más directa, adecuada para escenarios en los que puede controlar el proceso "secundario", adecuada para la comunicación entre procesos relacionados y no puede cruzar máquinas

Soporte IPC nativo de nodo: el método más nativo (¿auténtico?), Que es más "regular" que el anterior, y tiene las mismas limitaciones

A través de sockets: la forma más común, con buenas capacidades entre entornos, pero hay una pérdida de rendimiento en la red.

Con la ayuda de la cola de mensajes: la forma más poderosa, ya que desea comunicarse, la escena sigue siendo complicada, también podría expandir una capa de middleware de mensajes para resolver varios problemas de comunicación de manera hermosa

参考资料
Procesos secundarios de Node.js: todo lo que necesita saber

Supongo que te gusta

Origin blog.51cto.com/15080030/2592715
Recomendado
Clasificación