如何在Ubuntu16.04上使用PM2和Nginx开发Node.jsTCP服务器应用程序
作为 Write for DOnations 计划的一部分,作者选择了 OSMI 来接受捐赠。
介绍
Node.js 是基于 Chrome 的 V8 Javascript 引擎构建的流行的开源 JavaScript 运行时环境。 Node.js 用于构建服务器端和网络应用程序。TCP(传输控制协议) 是一种网络协议,可在应用程序之间提供可靠、有序且经过错误检查的数据流传输。 TCP 服务器可以接受 TCP 连接请求,一旦连接建立,双方就可以交换数据流。
在本教程中,您将构建一个基本的 Node.js TCP 服务器,以及一个用于测试服务器的客户端。 您将使用名为 PM2 的强大 Node.js 进程管理器将服务器作为后台进程运行。 然后将 Nginx 配置为 TCP 应用程序的反向代理,并从本地计算机测试客户端-服务器连接。
先决条件
要完成本教程,您需要:
- 按照 Ubuntu 16.04 初始服务器设置指南 设置一台 Ubuntu 16.04 服务器,包括 sudo 非 root 用户和防火墙。
- 在你的服务器上安装了 Nginx,如如何在 Ubuntu 16.04 上安装 Nginx。 Nginx 必须使用
--with-stream
选项编译,这是通过 Ubuntu 16.04 上的apt
包管理器全新安装 Nginx 的默认设置。 - 使用官方 PPA 安装的 Node.js,如 如何在 Ubuntu 16.04 上安装 Node.js 中所述。
第 1 步 — 创建 Node.js TCP 应用程序
我们将使用 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 添加到文件中,其中指定应用程序的名称、版本、主文件、启动应用程序的命令和软件许可证:
包.json
{ "name": "tcp-nodejs-app", "version": "1.0.0", "main": "server.js", "scripts": { "start": "node server.js" }, "license": "MIT" }
scripts
字段允许您为应用程序定义命令。 您在此处指定的设置允许您通过运行 npm start
而不是运行 node server.js
来运行应用程序。
package.json
文件还可以包含运行时和开发依赖项的列表,但我们不会为该应用程序提供任何第三方依赖项。
现在您已经设置了项目目录和 package.json
,让我们创建服务器。
在您的应用程序目录中,创建一个 server.js
文件:
nano server.js
Node.js 提供了一个名为 net
的模块,它支持 TCP 服务器和客户端通信。 使用 require()
加载 net
模块,然后定义变量来保存服务器的端口和主机:
服务器.js
const net = require('net'); const port = 7070; const host = '127.0.0.1';
我们将为此应用程序使用端口 7070
,但您可以使用任何您喜欢的可用端口。 我们将 127.0.0.1
用于 HOST
以确保我们的服务器仅在本地网络接口上侦听。 稍后我们会将 Nginx 作为反向代理放在这个应用程序的前面。 Nginx 精通处理多个连接和水平扩展。
然后添加此代码以使用 net
模块中的 createServer()
函数生成 TCP 服务器。 然后使用 net
模块的 listen()
函数开始监听您定义的端口和主机上的连接:
服务器.js
... 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
文件以实现这些功能:
服务器.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
数组中删除客户端,以便不再向它广播。 在连接块的末尾添加此代码:
服务器.js
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
的完整代码:
服务器.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.js TCP 客户端
我们的 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
连接到服务器:
客户端.js
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
事件并将服务器的响应打印到命令行:
客户端.js
client.on('data', function(data) { console.log('Server Says : ' + data); });
最后,通过添加以下代码来优雅地处理与服务器的断开连接:
客户端.js
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 运行服务器,以便它在 backgrand 中运行并且可以优雅地重新启动。
首先,使用 npm
在您的服务器上全局安装 PM2:
sudo npm install pm2 -g
安装 PM2 后,使用它来运行您的服务器。 您将使用 pm2
命令,而不是运行 npm start
来启动服务器。 启动服务器:
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
运行它,以便系统文件自动安装。
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
如果应用状态显示错误,可以使用【X58X】错误日志路径【X76X】打开查看错误日志,调试错误:
cat /home/tcp/.pm2/logs/server-error-0.log
如果您对服务器代码进行更改,则需要重新启动应用程序的进程以应用更改,如下所示:
pm2 restart 0
PM2 现在正在管理应用程序。 现在我们将使用 Nginx 将请求代理到服务器。
第 4 步 — 将 Nginx 设置为反向代理服务器
您的应用程序正在 127.0.0.1
上运行并侦听,这意味着它将只接受来自本地计算机的连接。 我们将 Nginx 设置为反向代理,它将处理传入流量并将其定向到我们的服务器。
为此,我们将修改 Nginx 配置以使用 Nginx 的 stream {} 和 stream_proxy 功能将 TCP 连接转发到我们的 Node.js 服务器。
我们必须将 Nginx 主配置文件编辑为配置 TCP 连接转发的 stream
块仅用作顶级块。 Ubuntu 上的默认 Nginx 配置在文件的 http
块中加载服务器块,而 stream
块不能放在该块中。
在编辑器中打开文件 /etc/nginx/nginx.conf
:
sudo nano /etc/nginx/nginx.conf
在配置文件的末尾添加以下行:
/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
指令告诉 Nginx 使用 PROXY 协议 将客户端信息发送到后端服务器,然后后端服务器可以根据需要处理该信息。
保存文件并退出编辑器。
检查您的 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 地址:
客户端.js
// 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 服务器现在收到了额外的 [ X249X] 包含真实 IP 的消息。 如果您需要该 IP 地址,您可以调整您的服务器以处理 PROXY
请求并解析出您需要的数据。
现在,您的 Node.js TCP 应用程序在 Nginx 反向代理后面运行,并且可以继续进一步开发您的服务器。
结论
在本教程中,您使用 Node.js 创建了一个 TCP 应用程序,使用 PM2 运行它,并在 Nginx 后面提供它。 您还创建了一个客户端应用程序以从其他机器连接到它。 您可以使用此应用程序来处理大量数据流或构建实时消息传递应用程序。