Ubuntu16.04でPM2とNginxを使用してNode.jsTCPサーバーアプリケーションを開発する方法
著者は、 Write forDOnationsプログラムの一環として寄付を受け取るためにOSMIを選択しました。
序章
Node.js は、ChromeのV8Javascriptエンジン上に構築された人気のあるオープンソースのJavaScriptランタイム環境です。 Node.jsは、サーバー側およびネットワークアプリケーションの構築に使用されます。 TCP(伝送制御プロトコル)は、アプリケーション間でデータストリームの信頼性が高く、順序付けられ、エラーチェックされた配信を提供するネットワークプロトコルです。 TCPサーバーはTCP接続要求を受け入れることができ、接続が確立されると、両側でデータストリームを交換できます。
このチュートリアルでは、サーバーをテストするためのクライアントとともに、基本的なNode.jsTCPサーバーを構築します。 PM2 と呼ばれる強力なNode.jsプロセスマネージャーを使用して、サーバーをバックグラウンドプロセスとして実行します。 次に、 Nginx をTCPアプリケーションのリバースプロキシとして構成し、ローカルマシンからのクライアント/サーバー接続をテストします。
前提条件
このチュートリアルを完了するには、次のものが必要です。
- Ubuntu16.04初期サーバーセットアップガイドに従ってセットアップされた1つのUbuntu16.04サーバー。これには、sudo非rootユーザーとファイアウォールが含まれます。
- Ubuntu 16.04にNginxをインストールする方法に示すように、サーバーにNginxがインストールされています。 Nginxは、
--with-stream
オプションを使用してコンパイルする必要があります。これは、Ubuntu16.04のapt
パッケージマネージャーを介したNginxの新規インストールのデフォルトです。 - Ubuntu 16.04にNode.jsをインストールする方法で説明されているように、Node.jsは公式PPAを使用してインストールされます。
ステップ1—Node.jsTCPアプリケーションを作成する
TCPソケットを使用してNode.jsアプリケーションを作成します。 これは、Node.jsの Net ライブラリを理解するのに役立つサンプルアプリケーションであり、生のTCPサーバーおよびクライアントアプリケーションを作成できます。
まず、Node.jsアプリケーションを配置するディレクトリをサーバー上に作成します。 このチュートリアルでは、~/tcp-nodejs-app
ディレクトリにアプリケーションを作成します。
- mkdir ~/tcp-nodejs-app
次に、新しいディレクトリに切り替えます。
- cd ~/tcp-nodejs-app
プロジェクト用にpackage.json
という名前の新しいファイルを作成します。 このファイルには、アプリケーションが依存するパッケージがリストされています。 このファイルを作成すると、この依存関係のリストを他の開発者と共有しやすくなるため、ビルドの再現性が高まります。
- nano package.json
npm init
コマンドを使用してpackage.json
を生成することもできます。これにより、アプリケーションの詳細を入力するよう求められますが、ファイルを手動で変更して、起動コマンド。 したがって、このチュートリアルではファイルを手動で作成します。
次のJSONをファイルに追加します。これは、アプリケーションの名前、バージョン、メインファイル、アプリケーションを起動するコマンド、およびソフトウェアライセンスを指定します。
{
"name": "tcp-nodejs-app",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"license": "MIT"
}
scripts
フィールドでは、アプリケーションのコマンドを定義できます。 ここで指定した設定では、node server.js
を実行する代わりに、npm start
を実行してアプリを実行できます。
package.json
ファイルには、ランタイムと開発の依存関係のリストを含めることもできますが、このアプリケーションのサードパーティの依存関係はありません。
プロジェクトディレクトリとpackage.json
の設定が完了したので、サーバーを作成しましょう。
アプリケーションディレクトリに、server.js
ファイルを作成します。
- nano server.js
Node.jsは、TCPサーバーとクライアントの通信を可能にするnet
と呼ばれるモジュールを提供します。 net
モジュールにrequire()
をロードし、サーバーのポートとホストを保持する変数を定義します。
const net = require('net');
const port = 7070;
const host = '127.0.0.1';
このアプリではポート7070
を使用しますが、使用可能な任意のポートを使用できます。 HOST
に127.0.0.1
を使用しているため、サーバーはローカルネットワークインターフェイスでのみリッスンしています。 後で、リバースプロキシとしてこのアプリの前にNginxを配置します。 Nginxは、複数の接続と水平スケーリングの処理に精通しています。
次に、このコードを追加して、net
モジュールのcreateServer()
関数を使用してTCPサーバーを生成します。 次に、net
モジュールのlisten()
機能を使用して、定義したポートとホストで接続のリッスンを開始します。
...
const server = net.createServer();
server.listen(port, host, () => {
console.log('TCP Server is running on port ' + port +'.');
});
server.js
を保存し、サーバーを起動します。
- npm start
次の出力が表示されます。
OutputTCP Server is running on port 7070
TCPサーバーはポート7070
で実行されています。 CTRL+C
を押してサーバーを停止します。
サーバーがリッスンしていることがわかったので、クライアント接続を処理するコードを記述しましょう。
クライアントがサーバーに接続すると、サーバーはconnection
イベントをトリガーします。これを確認します。 接続されたクライアントの配列を定義します。これをsockets
と呼び、クライアントが接続したときに各クライアントインスタンスをこの配列に追加します。
data
イベントを使用して、接続されたクライアントからのデータストリームを処理し、sockets
アレイを使用して、接続されているすべてのクライアントにデータをブロードキャストします。
次の機能を実装するには、次のコードをserver.js
ファイルに追加します。
...
let sockets = [];
server.on('connection', function(sock) {
console.log('CONNECTED: ' + sock.remoteAddress + ':' + sock.remotePort);
sockets.push(sock);
sock.on('data', function(data) {
console.log('DATA ' + sock.remoteAddress + ': ' + data);
// Write the data back to all the connected, the client will receive it as data from the server
sockets.forEach(function(sock, index, array) {
sock.write(sock.remoteAddress + ':' + sock.remotePort + " said " + data + '\n');
});
});
});
これは、接続されたクライアントによって送信されたdata
イベントをリッスンするようにサーバーに指示します。 接続されたクライアントがサーバーにデータを送信すると、sockets
配列を反復処理することにより、接続されているすべてのクライアントにデータをエコーバックします。
次に、接続されたクライアントが接続を終了したときにトリガーされるclose
イベントのハンドラーを追加します。 クライアントが切断するたびに、sockets
アレイからクライアントを削除して、ブロードキャストしないようにします。 接続ブロックの最後に次のコードを追加します。
let sockets = [];
server.on('connection', function(sock) {
...
// Add a 'close' event handler to this instance of socket
sock.on('close', function(data) {
let index = sockets.findIndex(function(o) {
return o.remoteAddress === sock.remoteAddress && o.remotePort === sock.remotePort;
})
if (index !== -1) sockets.splice(index, 1);
console.log('CLOSED: ' + sock.remoteAddress + ' ' + sock.remotePort);
});
});
server.js
の完全なコードは次のとおりです。
const net = require('net');
const port = 7070;
const host = '127.0.0.1';
const server = net.createServer();
server.listen(port, host, () => {
console.log('TCP Server is running on port ' + port + '.');
});
let sockets = [];
server.on('connection', function(sock) {
console.log('CONNECTED: ' + sock.remoteAddress + ':' + sock.remotePort);
sockets.push(sock);
sock.on('data', function(data) {
console.log('DATA ' + sock.remoteAddress + ': ' + data);
// Write the data back to all the connected, the client will receive it as data from the server
sockets.forEach(function(sock, index, array) {
sock.write(sock.remoteAddress + ':' + sock.remotePort + " said " + data + '\n');
});
});
// Add a 'close' event handler to this instance of socket
sock.on('close', function(data) {
let index = sockets.findIndex(function(o) {
return o.remoteAddress === sock.remoteAddress && o.remotePort === sock.remotePort;
})
if (index !== -1) sockets.splice(index, 1);
console.log('CLOSED: ' + sock.remoteAddress + ' ' + sock.remotePort);
});
});
ファイルを保存してから、サーバーを再起動します。
- npm start
マシン上で完全に機能するTCPサーバーが実行されています。 次に、サーバーに接続するクライアントを作成します。
ステップ2—Node.jsTCPクライアントを作成する
Node.js TCPサーバーが実行されているので、サーバーに接続してサーバーをテストするためのTCPクライアントを作成しましょう。
作成したNode.jsサーバーはまだ実行中であり、現在のターミナルセッションをブロックしています。 クライアントを開発する間、それを実行し続けたいので、新しいターミナルウィンドウまたはタブを開きます。 次に、新しいタブからサーバーに再度接続します。
- ssh sammy@your_server_ip
接続したら、tcp-nodejs-app
ディレクトリに移動します。
- cd tcp-nodejs-app
同じディレクトリに、client.js
という名前の新しいファイルを作成します。
- nano client.js
クライアントは、server.js
ファイルで使用されているのと同じnet
ライブラリを使用してTCPサーバーに接続します。 このコードをファイルに追加して、ポート7070
のIPアドレス127.0.0.1
を使用してサーバーに接続します。
const net = require('net');
const client = new net.Socket();
const port = 7070;
const host = '127.0.0.1';
client.connect(port, host, function() {
console.log('Connected');
client.write("Hello From Client " + client.address().address);
});
このコードは、最初にTCPサーバーへの接続を試みて、作成したサーバーが実行されていることを確認します。 接続が確立されると、クライアントはclient.write
機能を使用して "Hello From Client " + client.address().address
をサーバーに送信します。 サーバーはこのデータを受信し、クライアントにエコーバックします。
クライアントがサーバーからデータを受信したら、サーバーの応答を出力する必要があります。 次のコードを追加して、data
イベントをキャッチし、コマンドラインに対するサーバーの応答を出力します。
client.on('data', function(data) {
console.log('Server Says : ' + data);
});
最後に、次のコードを追加して、サーバーからの切断を適切に処理します。
client.on('close', function() {
console.log('Connection closed');
});
client.js
ファイルを保存します。
次のコマンドを実行して、クライアントを起動します。
- node client.js
接続が確立され、サーバーがデータを受信して、クライアントにエコーバックします。
client.js OutputConnected
Server Says : 127.0.0.1:34548 said Hello From Client 127.0.0.1
サーバーが実行されているターミナルに戻ると、次の出力が表示されます。
server.js OutputCONNECTED: 127.0.0.1:34550
DATA 127.0.0.1: Hello From Client 127.0.0.1
サーバーとクライアントアプリの間にTCP接続を確立できることを確認しました。
CTRL+C
を押してサーバーを停止します。 次に、他のターミナルセッションに切り替え、CTRL+C
を押してクライアントを停止します。 これで、このターミナルセッションをサーバーから切断して、元のターミナルセッションに戻ることができます。
次のステップでは、PM2を使用してサーバーを起動し、バックグラウンドで実行します。
ステップ3—PM2でサーバーを実行する
クライアント接続を受け入れる稼働中のサーバーがありますが、フォアグラウンドで実行されています。 PM2を使用してサーバーを実行し、バックグランドで実行され、正常に再起動できるようにします。
まず、npm
を使用してサーバーにPM2をグローバルにインストールします。
- sudo npm install pm2 -g
PM2をインストールしたら、それを使用してサーバーを実行します。 npm start
を実行してサーバーを起動する代わりに、pm2
コマンドを使用します。 サーバーを起動します。
- pm2 start server.js
次のような出力が表示されます。
[secondary_label Output
[PM2] Spawning PM2 daemon with pm2_home=/home/sammy/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/sammy/tcp-nodejs-app/server.js in fork_mode (1 instance)
[PM2] Done.
┌────────┬──────┬────────┬───┬─────┬───────────┐
│ Name │ mode │ status │ ↺ │ cpu │ memory │
├────────┼──────┼────────┼───┼─────┼───────────┤
│ server │ fork │ online │ 0 │ 5% │ 24.8 MB │
└────────┴──────┴────────┴───┴─────┴───────────┘
Use `pm2 show <id|name>` to get more details about an app
これで、サーバーはバックグラウンドで実行されます。 ただし、マシンを再起動すると、マシンは実行されなくなるので、そのためのsystemdサービスを作成しましょう。
次のコマンドを実行して、PM2のsystemdスタートアップスクリプトを生成してインストールします。 必ずsudo
でこれを実行して、systemdファイルが自動的にインストールされるようにしてください。
- sudo pm2 startup
次の出力が表示されます。
Output[PM2] Init System found: systemd
Platform systemd
...
[PM2] Writing init configuration in /etc/systemd/system/pm2-root.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-root...
Created symlink from /etc/systemd/system/multi-user.target.wants/pm2-root.service to /etc/systemd/system/pm2-root.service.
[PM2] [v] Command successfully executed.
+---------------------------------------+
[PM2] Freeze a process list on reboot via:
$ pm2 save
[PM2] Remove init script via:
$ pm2 unstartup systemd
PM2は現在systemdサービスとして実行されています。
pm2 list
コマンドを使用して、PM2が管理しているすべてのプロセスを一覧表示できます。
- pm2 list
IDが0
のアプリケーションがリストに表示されます。
Output┌──────────┬────┬──────┬──────┬────────┬─────────┬────────┬─────┬───────────┬───────┬──────────┐
│ App name │ id │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │
├──────────┼────┼──────┼──────┼────────┼─────────┼────────┼─────┼───────────┼───────┼──────────┤
│ server │ 0 │ fork │ 9075 │ online │ 0 │ 4m │ 0% │ 30.5 MB │ sammy │ disabled │
└──────────┴────┴──────┴──────┴────────┴─────────┴────────┴─────┴───────────┴───────┴──────────┘
上記の出力では、watching
が無効になっていることがわかります。 これは、アプリケーションファイルに変更を加えたときにサーバーをリロードする機能です。 開発には役立ちますが、本番環境ではその機能は必要ありません。
実行中のプロセスに関する詳細情報を取得するには、pm2 show
コマンドに続けてそのIDを使用します。 この場合、IDは0
です。
- pm2 show 0
この出力には、稼働時間、ステータス、ログファイルパス、および実行中のアプリケーションに関するその他の情報が表示されます。
OutputDescribing process with id 0 - name server
┌───────────────────┬──────────────────────────────────────────┐
│ status │ online │
│ name │ server │
│ restarts │ 0 │
│ uptime │ 7m │
│ script path │ /home/sammy/tcp-nodejs-app/server.js │
│ script args │ N/A │
│ error log path │ /home/sammy/.pm2/logs/server-error-0.log │
│ out log path │ /home/sammy/.pm2/logs/server-out-0.log │
│ pid path │ /home/sammy/.pm2/pids/server-0.pid │
│ interpreter │ node │
│ interpreter args │ N/A │
│ script id │ 0 │
│ exec cwd │ /home/sammy/tcp-nodejs-app │
│ exec mode │ fork_mode │
│ node.js version │ 8.11.2 │
│ watch & reload │ ✘ │
│ unstable restarts │ 0 │
│ created at │ 2018-05-30T19:29:45.765Z │
└───────────────────┴──────────────────────────────────────────┘
Code metrics value
┌─────────────────┬────────┐
│ Loop delay │ 1.12ms │
│ Active requests │ 0 │
│ Active handles │ 3 │
└─────────────────┴────────┘
Add your own code metrics: http://bit.ly/code-metrics
Use `pm2 logs server [--lines 1000]` to display logs
Use `pm2 monit` to monitor CPU and Memory usage server
アプリケーションのステータスにエラーが表示された場合は、エラーログパスを使用してエラーログを開き、確認してエラーをデバッグできます。
- cat /home/tcp/.pm2/logs/server-error-0.log
サーバーコードに変更を加えた場合は、次のように、アプリケーションのプロセスを再起動して変更を適用する必要があります。
- pm2 restart 0
PM2は現在アプリケーションを管理しています。 次に、Nginxを使用してリクエストをサーバーにプロキシします。
ステップ4—Nginxをリバースプロキシサーバーとして設定する
アプリケーションは127.0.0.1
で実行およびリッスンしています。つまり、ローカルマシンからの接続のみを受け入れます。 Nginxをリバースプロキシとして設定し、着信トラフィックを処理してサーバーに転送します。
これを行うには、Nginxの stream{}およびstream_proxy機能を使用してTCP接続をNode.jsサーバーに転送するように、Nginx構成を変更します。
TCP接続転送を構成するstream
ブロックはトップレベルのブロックとしてのみ機能するため、メインのNginx構成ファイルを編集する必要があります。 UbuntuのデフォルトのNginx構成は、ファイルのhttp
ブロック内にサーバーブロックをロードし、stream
ブロックをそのブロック内に配置することはできません。
エディターでファイル/etc/nginx/nginx.conf
を開きます。
- sudo nano /etc/nginx/nginx.conf
構成ファイルの最後に次の行を追加します。
...
stream {
server {
listen 3000;
proxy_pass 127.0.0.1:7070;
proxy_protocol on;
}
}
これは、ポート3000
でTCP接続をリッスンし、ポート7070
で実行されているNode.jsサーバーにリクエストをプロキシします。 アプリケーションが別のポートでリッスンするように設定されている場合は、プロキシパスのURLポートを正しいポート番号に更新します。 proxy_protocol
ディレクティブは、 PROXYプロトコルを使用してクライアント情報をバックエンドサーバーに送信するようにNginxに指示します。バックエンドサーバーは、必要に応じてその情報を処理できます。
ファイルを保存して、エディターを終了します。
Nginx構成をチェックして、構文エラーが発生していないことを確認します。
- sudo nginx -t
次に、Nginxを再起動して、TCPおよびUDPプロキシ機能を有効にします。
- sudo systemctl restart nginx
次に、そのポートでサーバーへのTCP接続を許可します。 ufw
を使用して、ポート3000
での接続を許可します。
- sudo sudo ufw allow 3000
Node.jsアプリケーションが実行されていて、アプリケーションとNginxの構成が正しいと仮定すると、Nginxリバースプロキシを介してアプリケーションにアクセスできるようになります。
ステップ5—クライアント/サーバー接続のテスト
client.js
スクリプトを使用して、ローカルマシンからTCPサーバーに接続して、サーバーをテストしてみましょう。 そのためには、開発したclient.js
ファイルをローカルマシンにダウンロードし、スクリプトのポートとIPアドレスを変更する必要があります。
まず、ローカルマシンで、scp
を使用してclient.js
ファイルをダウンロードします。
- [environment local
- scp sammy@your_server_ip:~/tcp-nodejs-app/client.js client.js
エディタでclient.js
ファイルを開きます。
- [environment local
- nano client.js
port
を3000
に変更し、host
をサーバーのIPアドレスに変更します。
// A Client Example to connect to the Node.js TCP Server
const net = require('net');
const client = new net.Socket();
const port = 3000;
const host = 'your_server_ip';
...
ファイルを保存し、エディターを終了し、クライアントを実行してテストします。
- node client.js
以前に実行したときと同じ出力が表示されます。これは、クライアントマシンがNginxを介して接続し、サーバーに到達したことを示しています。
client.js OutputConnected
Server Says : 127.0.0.1:34584 said PROXY TCP4 your_local_ip_address your_server_ip 52920 3000
Hello From Client your_local_ip_address
Nginxはサーバーへのクライアント接続をプロキシしているため、Node.jsサーバーはクライアントの実際のIPアドレスを認識しません。 NginxのIPアドレスのみが表示されます。 Nginxは、セキュリティに影響を与える可能性のあるシステムに変更を加えずに、実際のIPアドレスをバックエンドに直接送信することをサポートしていませんが、NginxでPROXYプロトコルを有効にしたため、Node.jsサーバーは追加のPROXY
リクエストを処理し、必要なデータを解析するようにサーバーを調整できます。
これで、Node.js TCPアプリケーションがNginxリバースプロキシの背後で実行され、サーバーの開発をさらに進めることができます。
結論
このチュートリアルでは、Node.jsを使用してTCPアプリケーションを作成し、PM2を使用して実行し、Nginxの背後で提供しました。 また、他のマシンから接続するためのクライアントアプリケーションを作成しました。 このアプリケーションを使用して、データストリームの大きなチャンクを処理したり、リアルタイムメッセージングアプリケーションを構築したりできます。