Nodejsプロセス間通信

1.シナリオ
ノードはシングルスレッドで実行されますが、これはマルチコア/マルチマシンマルチプロセスの利点を使用できないことを意味するものではありません

実際、ノードは当初、設計から分散ネットワークシナリオを検討しました。

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 NodeがNodeと呼ばれる理由については、Node.jsがNode.jsと呼ばれる理由を参照してください。

2.プロセス
作成する通信方法は、プロセス生成の方法に関連しており、Nodeには、spawn()、exec()、execFile()、およびfork()の4つのプロセス作成方法があります。


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

spawn()は、ChildProcessのインスタンスを返します。ChildProcessは、イベントメカニズム(EventEmitter API)に基づいていくつかのイベントも提供します。

exit:子プロセスが終了するとトリガーされ、プロセスの終了ステータス(コードとシグナル)を知ることができます。

切断:親プロセスがchild.disconnect()を呼び出すとトリガーされます

エラー:子プロセスは、強制終了されたときに作成またはトリガーできませんでした

close:子プロセスのstdioストリーム(標準の入力および出力ストリーム)が閉じられたときにトリガーされます

メッセージ:子プロセスがprocess.send()を介してメッセージを送信するとトリガーされ、親プロセスと子プロセスはこの組み込みメッセージメカニズムを介して通信できます。

子プロセスのstdioストリームには、child.stdin、child.stdout、child.stderrを介してアクセスできます。これらのストリームが閉じられると、子プロセスはcloseイベントをトリガーします。

PScloseとexitの違いは、主に複数のプロセスが同じstdioストリームを共有するシナリオに反映されます。プロセスのexitは、stdioストリームが閉じていることを意味するものではありません。

子プロセスでは、stdout / stderrには読み取り可能な特性があり、stdinには書き込み可能な特性があります。これはメインプロセスの反対です。


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

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

プロセスstdioストリームのパイプライン特性を使用すると、次のようなより複雑なことを実行できます。


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

効果はfindと同等です。-typef| wc -l、現在のディレクトリ内のファイルの数を再帰的にカウントします。

IPCオプション
さらに、IPCメカニズムは、spawn()メソッドのstdioオプションを介して確立できます。


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

spawn()のIPCオプションの詳細については、options.stdioを確認してください。

exec
spawn()メソッドは、デフォルトで着信コマンドを実行するためのシェルを作成しません(したがって、パフォーマンスはわずかに向上します)が、exec()メソッドはシェルを作成します。さらに、exec()はストリームに基づいていませんが、着信コマンドの実行結果を一時的にバッファーに格納してから、コールバック関数に渡します。

exec()メソッドの機能は、シェル構文を完全にサポートし、次のような任意のシェルスクリプトを直接渡すことができることです。


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

ただし、exec()メソッドには、コマンドインジェクションのセキュリティリスクもあります。ユーザー入力などの動的コンテンツを含むシナリオには特に注意してください。したがって、exec()メソッドの適用可能なシナリオは次のとおりです。シェル構文を直接使用する必要があり、予想される出力データ量が大きくない(メモリの負荷がない)

それで、シェル構文をサポートするだけでなく、ストリームIOの利点も持つ方法はありますか?

持ってる。両方の長所は次のとおりです。


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

spawn()のシェルオプションをオンにし、pipe()メソッドを介して子プロセスの標準出力を現在のプロセスの標準入力に接続するだけで、コマンドの実行結果を確認できます。実際にはもっと簡単な方法があります:


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'を使用すると、子プロセスは現在のプロセスの標準の入力と出力(stdin、stdout、stderrを共有)を継承できるため、上記の例では、現在のプロセスprocess.stdoutのデータイベントを監視することで、子プロセスの出力結果を取得できます。

さらに、stdioおよびshellオプションに加えて、spawn()は次のような他のオプションもサポートします。


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

環境変数の形式で子プロセスにデータを渡すことに加えて、envオプションを使用して、サンドボックススタイルの環境変数の分離を実装することもできます。デフォルトでは、process.envが子プロセスの環境変数セットとして使用され、子プロセスは現在のプロセスと同じものにアクセスできます。上記の例のように、子プロセスの環境変数セットとしてカスタムオブジェクトを指定すると、すべての環境変数は、子プロセスが他の環境変数にアクセスできなくなります。

したがって、環境変数を追加/削除する場合は、次のようにする必要があります。


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

デタッチオプションはもっと興味深いです:


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

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

child.unref();

この方法で作成された独立したプロセスの動作は、オペレーティングシステムによって異なります。Windowsのデタッチされた子プロセスには独自のコンソールウィンドウがあり、Linuxのプロセスは新しいプロセスグループを作成します(この機能を使用して、子プロセスファミリを管理し、同様のことを実現できます。ツリーキルの特徴)

unref()メソッドを使用して関係を切断し、「親」プロセスを独立して終了できるようにします(子プロセスを終了させません)が、子プロセスのstdioもこの時点で「親」プロセスから独立している必要があります。そうでない場合は、「親」プロセスも独立している必要があります。子プロセスは終了後も影響を受けます


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

exec()メソッドに似ていますが、シェルを介して実行されないため(パフォーマンスがわずかに向上します)、実行可能ファイルを渡す必要があります。.batや.cmdなど、Windowsの一部のファイルは直接実行できません。これらのファイルは、シェルオプションがオンになっているexec()またはspawn()でのみ、execFile()では実行できません。

PSはexec()のようにストリームベースではなく、出力データ量のリスクもあります

xxxSync
spawn、exec、およびexecFileにはすべて、対応する同期ブロッキングバージョンがあり、子プロセスが終了するまで待機します

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

同期メソッドは、起動プロセスなどのスクリプトタスクを簡素化するために使用されます。これらのメソッドは、他の場合は避ける必要があります。

Fork
fork()は、ノードプロセスの作成に使用されるspawn()のバリアントです。最大の機能は、親子プロセスに独自の通信メカニズム(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.

例えば:


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

fork()には独自の通信メカニズムの利点があるため、次のような時間のかかるロジックを分割するのに特に適しています。


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

これに関する致命的な問題は、誰かが/ computeにアクセスすると、イベントループがlongComputationによってブロックされたままであり、時間のかかる計算が終了するまでサービス容量を復元できないため、後続の要求を時間内に処理できないことです。

メインプロセスのイベントループをブロックする時間のかかる操作を回避するために、longComputation()を子プロセスに分割できます。


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

メインプロセスは子プロセスを開いて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);

メインプロセスのイベントループは、時間のかかる計算によってブロックされなくなりますが、プロセスの数をさらに制限する必要があります。そうしないと、プロセスによってリソースが使い果たされたときにサービス容量が影響を受けます。

PS実際、クラスターモジュールはマルチプロセスサービス機能のカプセル化です。考え方はこの単純な例に似ています。

3.通信方法
1.json
stdin / stdoutとJSONペイロードをstdin / stdoutに渡します

最も直接的なコミュニケーション手段、ハンドルを取得するための子プロセス、stdioストリームにアクセスすると
、特定の1種類のメッセージ形式について楽しくコミュニケーションが始まります。


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

子プロセスは似ています:


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

PSVSコードのプロセス間通信はこの方法を使用します。詳細については、vscode拡張機能からのアクセス電子APIを参照してください。

明らかな制限は、「子」プロセスのハンドルを取得する必要があり、2つの完全に独立したプロセスがこの方法で通信できないことです(クロスアプリケーションまたはクロスマシンシナリオなど)。

PSストリームとパイプの詳細については、ノードのストリームを確認してください

2.ネイティブIPCは
spawn()やfork()などの例をサポートし、プロセスは組み込みのIPCメカニズムを介して相互に通信できます。

親プロセス:

process.on( 'message')補償

child.send()は送信します

子プロセス:

process.on( 'message')補償

process.send()は送信します

制限は上記と同じであり、一方の当事者が他方の当事者のハンドルを取得できる必要があります。

3.ソケット
はネットワークを使用して、プロセス間だけでなく、マシン間でもプロセス間通信を完了します。

node-ipcは、次のようにこのスキームを使用します。


// 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その他の例については、RIAEvangelist / node-ipcを参照してください。

もちろん、スタンドアロンシナリオでネットワークを介したプロセス間通信を完了することはパフォーマンスの無駄ですが、ネットワーク通信の利点は、環境間の互換性とさらなるRPCシナリオにあります。

4.
メッセージキューの親プロセスと子プロセスの両方が外部メッセージメカニズムを介して通信し、クロスプロセス機能はMQサポートに依存します

つまり、プロセス間に直接の通信はありませんが、中間層(MQ)を介して、制御層を追加すると、柔軟性と利点が向上します。

安定性:メッセージメカニズムは、配信の確認(メッセージ受信ACK)、障害時の再送信/複数の送信の防止など、強力な安定性を保証します。

優先制御:メッセージの応答順序を調整できます

オフライン機能:メッセージをキャッシュできます

トランザクションメッセージの処理:関連するメッセージをトランザクションに結合して、配信順序と整合性を確保します

PSを達成するのは簡単ではありませんか?1つのレイヤーで解決できますか?そうでない場合は、2つのレイヤーだけで解決できます...

より人気のあるものは、例えば、smrchy / rsmqです。


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

Redisサーバーがセットアップされます。基本的な原則は次のとおりです。


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

メッセージの受信/送信/キャッシング/永続性は、Redisが提供する機能に依存し、これに基づいて完全なキューメカニズムが実装されます。

5. Redisの
基本的な考え方は、メッセージキューの考え方似ています。


Use Redis as a message bus/broker.

Redisには独自のPub / Subメカニズム(つまり、発行-サブスクライブモード)があり、メッセージの信頼性を気にしない1対1または1対多のシナリオなどの単純な通信シナリオに適しています。

さらに、Redisにはリスト構造があり、メッセージの信頼性を向上させるためのメッセージキューとして使用できます。一般的なアプローチは、LPUSHメッセージを生成し、BRPOPメッセージを消費することです。メッセージの信頼性を必要とする単純な通信シナリオに適していますが、メッセージに状態やACKメカニズムがないため、複雑な通信要件を満たすことができないという欠点があります。

PSRedis的Pub / Sub示例见最も効率的なnode.jsプロセス間通信ライブラリ/メソッドは何ですか?

4.まとめ
ノードプロセス間で通信するには、次の4つの方法があります。

jsonをstdin / stdoutに渡す:最も直接的な方法で、「子」プロセスのハンドルを取得でき、関連するプロセス間の通信に適しており、マシンをまたがることができないシナリオに適しています

ノードネイティブIPCサポート:最もネイティブな(本物の?)メソッド。これは前のメソッドよりも「通常」であり、同じ制限があります。

ソケットを介して:最も一般的な方法で、優れたクロス環境機能を備えていますが、ネットワークのパフォーマンスが低下します

メッセージキューの助けを借りて:最も強力な方法、あなたが通信したいので、シーンはまだ複雑です、あなたはさまざまな通信問題を美しく解決するためにメッセージミドルウェアの層を拡張したほうがよいでしょう

支配资料
Node.js子プロセス:あなたが知る必要があるすべて

おすすめ

転載: blog.51cto.com/15080030/2592715