Communication inter-processus Nodejs

1. Scenario
Node s'exécute dans un seul thread, mais cela ne signifie pas que les avantages du multi-processus multi-core / multi-machine ne peuvent pas être utilisés

En fait, Node a initialement considéré le scénario de réseau distribué dès la conception:

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.

PS Pour la raison pour laquelle Node s'appelle Node, consultez Pourquoi Node.js s'appelle Node.js?

2. La
méthode de communication pour créer un processus est liée à la méthode de génération de processus, et Node a 4 façons de créer un processus: spawn (), exec (), execFile () et fork ()


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

spawn () renvoie une instance de ChildProcess. ChildProcess fournit également des événements basés sur le mécanisme d'événement (API EventEmitter):

exit: Déclenché lorsque le processus fils se termine, vous pouvez connaître l'état de sortie du processus (code et signal)

disconnect: déclenché lorsque le processus parent appelle child.disconnect ()

erreur: le processus enfant n'a pas pu être créé ou déclenché lorsqu'il a été tué

close: déclenché lorsque le flux stdio (flux d'entrée et de sortie standard) du processus enfant est fermé

message: Déclenché lorsque le processus enfant envoie un message via process.send (), les processus parent et enfant peuvent communiquer via ce mécanisme de message intégré

Le flux stdio du processus enfant est accessible via child.stdin, child.stdout et child.stderr. Lorsque ces flux sont fermés, le processus enfant déclenchera l'événement de fermeture.

La différence entre PSclose et exit se reflète principalement dans le scénario où plusieurs processus partagent le même flux stdio. La sortie d'un processus ne signifie pas que le flux stdio est fermé.

Dans le processus enfant, stdout / stderr a des caractéristiques lisibles, tandis que stdin a des caractéristiques inscriptibles, ce qui est l'opposé du processus principal:


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

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

En utilisant les caractéristiques de pipeline du flux stdio de processus, vous pouvez accomplir des tâches plus complexes, telles que:


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

L'effet est équivalent à find. -Type f | wc -l, compte récursivement le nombre de fichiers dans le répertoire courant

Option IPC
De plus, le mécanisme IPC peut être établi via l'option stdio de la méthode 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');
});

Pour plus d'informations sur les options IPC de spawn (), veuillez consulter options.stdio

La
méthode exec spawn () ne crée pas de shell pour exécuter la commande entrante par défaut (donc les performances sont légèrement meilleures), et la méthode exec () crée un shell. En outre, exec () n'est pas basé sur le flux, mais stocke temporairement le résultat de l'exécution de la commande entrante dans le tampon, puis le transmet à la fonction de rappel.

La caractéristique de la méthode exec () est qu'elle prend entièrement en charge la syntaxe du shell et peut passer directement dans n'importe quel script shell, par exemple:


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

Cependant, la méthode exec () présente également le risque de sécurité de l'injection de commandes. Faites particulièrement attention aux scénarios qui contiennent du contenu dynamique tel que l'entrée utilisateur. Par conséquent, le scénario applicable de la méthode exec () est le suivant: vous souhaitez utiliser directement la syntaxe du shell et le volume de données de sortie attendu n'est pas important (il n'y a pas de pression mémoire)

Alors, y a-t-il un moyen qui non seulement prend en charge la syntaxe du shell, mais présente également les avantages du flux IO?

Avoir. Le meilleur des deux mondes est le suivant:


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

Activez l'option shell de spawn (), et connectez simplement la sortie standard du processus enfant à l'entrée standard du processus actuel via la méthode pipe (), afin de voir le résultat de l'exécution de la commande. Il existe en fait un moyen plus simple:


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: 'inherit' permet au processus enfant d'hériter de l'entrée et de la sortie standard du processus actuel (partager stdin, stdout et stderr), de sorte que l'exemple ci-dessus peut obtenir le résultat de sortie du processus enfant en surveillant l'événement de données du processus de processus actuel.

De plus, en plus des options stdio et shell, spawn () prend également en charge d'autres options, telles que:


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

Notez qu'en plus de transmettre des données au processus enfant sous la forme de variables d'environnement, l'option env peut également être utilisée pour implémenter une isolation de variable d'environnement de style bac à sable. Par défaut, process.env est utilisé comme jeu de variables d'environnement du processus enfant, et le processus enfant peut accéder au même processus que le processus actuel Toutes les variables d'environnement, si vous spécifiez un objet personnalisé comme jeu de variables d'environnement du processus enfant comme dans l'exemple ci-dessus, le processus enfant ne peut pas accéder à d'autres variables d'environnement

Par conséquent, si vous souhaitez ajouter / supprimer des variables d'environnement, vous devez procéder comme suit:


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

L'option détachée est plus intéressante:


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

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

child.unref();

Le comportement du processus indépendant créé de cette manière dépend du système d'exploitation. Le processus enfant détaché sous Windows aura sa propre fenêtre de console, tandis que le processus sous Linux créera un nouveau groupe de processus (cette fonctionnalité peut être utilisée pour gérer la famille de processus enfant et obtenir des résultats similaires. Caractéristiques de la destruction des arbres)

La méthode unref () est utilisée pour rompre la relation, afin que le processus "parent" puisse se terminer indépendamment (ne provoquera pas la fermeture du processus enfant), mais notez que le stdio du processus enfant doit également être indépendant du processus "parent" à ce moment, sinon le processus "parent" Le processus enfant sera toujours affecté après avoir quitté


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

Similaire à la méthode exec (), mais elle n'est pas exécutée via le shell (donc les performances sont légèrement meilleures), le fichier exécutable doit donc être transmis. Certains fichiers sous Windows ne peuvent pas être exécutés directement, tels que .bat et .cmd, ces fichiers ne peuvent pas être exécutés avec execFile (), uniquement avec exec () ou spawn () avec l'option shell activée

PS n'est pas basé sur un flux comme exec (), et il existe également un risque de volume de données de sortie

xxxSync
spawn, exec et execFile ont tous des versions de blocage synchrone correspondantes, attendez que le processus enfant se termine

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

Les méthodes synchrones sont utilisées pour simplifier les tâches de script, telles que le processus de démarrage. Ces méthodes doivent être évitées à d'autres moments

Fork
fork () est une variante de spawn (), utilisée pour créer un processus Node. La principale caractéristique est que le processus parent-enfant a son propre mécanisme de communication (pipeline 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.

Par exemple:


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

Parce que fork () a les avantages de son propre mécanisme de communication, il est particulièrement adapté pour diviser une logique chronophage, telle que:


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

Le problème fatal est qu'une fois que quelqu'un visite / calcule, les demandes suivantes ne peuvent pas être traitées à temps, car la boucle d'événements est toujours bloquée par longComputation et la capacité du service ne peut pas être restaurée tant que le calcul fastidieux n'est pas terminé.

Afin d'éviter des opérations fastidieuses bloquant la boucle d'événements du processus principal, longComputation () peut être divisé en processus enfants:


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

Le processus principal ouvre le processus enfant pour exécuter 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);

La boucle d'événements du processus principal ne sera plus bloquée par des calculs chronophages, mais le nombre de processus doit être encore limité, sinon la capacité du service sera toujours affectée lorsque les ressources seront épuisées par le processus.

PS En fait, le module cluster est une encapsulation de capacités de service multi-processus. L'idée est similaire à cet exemple simple

3. Méthode de communication
1. Passez json
stdin / stdout et une charge utile JSON via stdin / stdout

Le moyen de communication le plus direct, le processus enfant pour obtenir la poignée, vous pouvez visiter leur flux stdio, puis à propos d'
un type de format de message donné, la communication a commencé avec bonheur:


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

Le processus enfant est similaire:


// ./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 communication inter-processus de code PSVS utilise cette méthode, voir API électronique d'accès à partir de l'extension vscode pour plus de détails

La limitation évidente est que vous devez maîtriser le processus "enfant", et deux processus complètement indépendants ne peuvent pas communiquer de cette manière (comme des scénarios inter-applications ou même inter-machines)

PS Pour plus d'informations sur le flux et le tuyau, veuillez vérifier le flux dans Node

2. IPC natif prend en charge des
exemples tels que spawn () et fork (), et les processus peuvent communiquer entre eux via le mécanisme IPC intégré

Processus parent:

process.on ('message') 收

child.send () envoie

Processus enfant:

process.on ('message') 收

process.send () envoie

Les restrictions sont les mêmes que ci-dessus, et une partie doit être en mesure d'obtenir la poignée de l'autre partie.

3. Les sockets
utilisent le réseau pour compléter la communication inter-processus, non seulement entre les processus, mais aussi entre les machines

node-ipc utilise ce schéma, par exemple:


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

PS Pour plus d'exemples, voir RIAEvangelist / node-ipc

Bien sûr, terminer la communication inter-processus via le réseau dans un scénario autonome est un gaspillage de performances, mais l'avantage de la communication réseau réside dans la compatibilité entre environnements et d'autres scénarios RPC.

4. Les
processus parents et enfants de la file d'attente de messages communiquent via un mécanisme de messagerie externe et la capacité inter-processus dépend de la prise en charge de MQ.

Autrement dit, il n'y a pas de communication directe entre les processus, mais via la couche intermédiaire (MQ), l'ajout d'une couche de contrôle peut gagner plus de flexibilité et d'avantages:

Stabilité: le mécanisme de message offre de solides garanties de stabilité, telles que la confirmation de livraison (réception de message ACK), la retransmission en cas d'échec / la prévention de plusieurs transmissions, etc.

Contrôle de priorité: permet d'ajuster l'ordre de réponse des messages

Capacité hors ligne: les messages peuvent être mis en cache

Traitement des messages transactionnels: combinez les messages associés en transactions pour garantir leur ordre de livraison et leur intégrité

PS n'est pas facile à réaliser? Peut-il être résolu avec une seule couche? Sinon, juste deux couches ...

Les plus populaires sont smrchy / rsmq, par exemple:


// 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...")
  }
});

Un serveur Redis sera mis en place. Les principes de base sont les suivants:


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

La réception / l'envoi / la mise en cache / la persistance des messages dépend des capacités fournies par Redis, et un mécanisme de file d'attente complet est implémenté sur cette base

5. L'
idée de base de Redis est similaire à celle de la file d'attente de messages:


Use Redis as a message bus/broker.

Redis a son propre mécanisme Pub / Sub (c'est-à-dire le mode publication-abonnement), qui convient aux scénarios de communication simples, tels que les scénarios un-à-un ou un-à-plusieurs qui ne se soucient pas de la fiabilité des messages

De plus, Redis a une structure de liste, qui peut être utilisée comme file d'attente de messages pour améliorer la fiabilité des messages. L'approche générale consiste à produire des messages LPUSH et à consommer des messages BRPOP. Il convient aux scénarios de communication simples qui nécessitent la fiabilité des messages, mais l'inconvénient est que le message n'a ni état ni mécanisme ACK, ce qui ne peut pas répondre aux exigences de communication complexes.

PSRedis 的 Pub / Sub 示例 见 Quelle est la bibliothèque / méthode de communication inter-processus node.js la plus efficace?

4. Résumé
Il existe 4 manières de communiquer entre les processus Node:

Passer json via stdin / stdout: le moyen le plus direct, adapté aux scénarios où vous pouvez obtenir la poignée du processus "enfant", adapté à la communication entre processus associés, et ne peut pas traverser les machines

Prise en charge IPC native des nœuds: la méthode la plus native (authentique?), Qui est plus "régulière" que la précédente et présente les mêmes limitations

Via les sockets: le moyen le plus courant, avec de bonnes capacités inter-environnements, mais il y a une perte de performance dans le réseau

Avec l'aide de la file d'attente de messages: le moyen le plus puissant, puisque vous voulez communiquer, la scène est encore compliquée, vous pouvez aussi bien étendre une couche de middleware de message pour résoudre magnifiquement divers problèmes de communication

参考资料
Processus enfants Node.js: tout ce que vous devez savoir

Je suppose que tu aimes

Origine blog.51cto.com/15080030/2592715
conseillé
Classement