如何在Ubuntu 16.04上使用PM2和Nginx开发Node.js TCP服务器应用程序

在本教程中,您将构建一个基本的Node.js TCP服务器,以及一个用于测试服务器的客户端。您将使用名为PM2的强大Node.js流程管理器将您的服务器作为后台进程运行。然后,您将Nginx配置为TCP应用程序的反向代理,并测试本地计算机的客户端 - 服务器连接。

作者选择OSMI作为Write for DOnations计划的一部分进行捐赠。

介绍

Node.js是一个流行的开源JavaScript运行时环境,基于Chrome的V8 Javascript引擎构建。 Node.js用于构建服务器端和网络应用程序。 TCP(传输控制协议)是一种网络协议,可在应用程序之间提供可靠,有序和错误检查的数据流传输。 TCP服务器可以接受TCP连接请求,一旦建立连接,双方都可以交换数据流。

在本教程中,您将构建一个基本的Node.js TCP服务器,以及一个用于测试服务器的客户端。 您将使用名为PM2的强大Node.js流程管理器将您的服务器作为后台进程运行。 然后,您将Nginx配置为TCP应用程序的反向代理,并测试本地计算机的客户端 - 服务器连接。

先决条件

要完成本教程,您需要:

第1步 - 创建Node.js TCP应用程序

我们将使用TCP套接字编写Node.js应用程序。 这是一个示例应用程序,它将帮助您了解Node.js中的Net库,它使我们能够创建原始TCP服务器和客户端应用程序。

首先,在服务器上创建一个目录,在该目录中放置Node.js应用程序。 在本教程中,我们将在~/tcp-nodejs-app目录中创建我们的应用~/tcp-nodejs-app

mkdir ~/tcp-nodejs-app

然后切换到新目录:

cd ~/tcp-nodejs-app

为您的项目创建一个名为package.json的新文件。 此文件列出了应用程序所依赖的包。 创建此文件将使构建重现,因为与其他开发人员共享此依赖项列表将更容易:

nano package.json

您还可以使用npm init命令生成package.json ,它将提示您输入应用程序的详细信息,但我们仍然需要手动更改文件以添加其他部分,包括启动命令。 因此,我们将在本教程中手动创建该文件。

将以下JSON添加到文件中,该文件指定应用程序的名称,版本,主文件,启动应用程序的命令以及软件许可证:

的package.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模块,然后定义变量以保存服务器的端口和主机:

server.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()函数开始监听端口和您定义的主机上的连接:

server.js
...
const server = net.createServer();
server.listen(port, host, () => {
    console.log('TCP Server is running on port ' + port +'.');
});

保存server.js并启动服务器:

npm start

你会看到这个输出:

TCP Server is running on port 7070

TCP服务器在端口7070上运行。 CTRL+C以停止服务器。

现在我们知道服务器正在监听,让我们编写代码来处理客户端连接。

当客户端连接到服务器时,服务器会触发connection事件,我们将会观察到。 我们将定义一组连接的客户端,我们将其称为sockets ,并在客户端连接时将每个客户端实例添加到此阵列。

我们将使用data事件处理来自连接客户端的数据流,使用sockets阵列将数据广播到所有连接的客户端。

将此代码添加到server.js文件以实现这些功能:

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阵列中删除客户端,以便我们不再向其广播。 在连接块的末尾添加此代码:

server.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的完整代码:

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.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连接到服务器:

client.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事件并打印服务器对命令行的响应:

client.js
client.on('data', function(data) {
    console.log('Server Says : ' + data);
});

最后,通过添加以下代码来优雅地处理与服务器的断开连接:

client.js
client.on('close', function() {
    console.log('Connection closed');
});

保存client.js文件。

运行以下命令以启动客户端:

node client.js

连接将建立,服务器将接收数据,并将其回送给客户端:

Connected
Server Says : 127.0.0.1:34548 said Hello From Client 127.0.0.1

切换回运行服务器的终端,您将看到以下输出:

CONNECTED: 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运行此sudo以便自动安装systemd文件。

sudo pm2 startup

你会看到这个输出:

[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 list PM2正在管理的所有进程:

pm2 list

您将在列表中看到您的应用程序,ID为0

┌──────────┬────┬──────┬──────┬────────┬─────────┬────────┬─────┬───────────┬───────┬──────────┐
│ 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

此输出显示正常运行时间,状态,日志文件路径以及有关正在运行的应用程序的其他信息:

Describing 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配置以使用Nginx的stream {}stream_proxy功能转发到我们的Node.js服务器的TCP连接。

我们必须编辑主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地址:

client.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连接并到达您的服务器:

Connected
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消息真正的IP。 如果您需要该IP地址,则可以调整服务器以处理PROXY请求并解析所需的数据。

您现在在Nginx反向代理后面运行Node.js TCP应用程序,并可以继续进一步开发服务器。

结论

在本教程中,您使用Node.js创建了一个TCP应用程序,使用PM2运行它,并在Nginx后面提供它。 您还创建了一个客户端应用程序,以便从其他计算机连接到它。 您可以使用此应用程序处理大块数据流或构建实时消息传递应用程序。