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

介绍

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

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

准备

要完成本教程,您需要:

  • 一个Ubuntu 16.04服务器,包括一个可以使用sudo权限的非root用户和一个防火墙。
  • 将Nginx安装在您的服务器上。必须使用--with-stream选项编译Nginx ,这是通过Ubuntu 16.04上的软件包管理器apt全新安装Nginx的默认选项。
  • 使用官方PPA 安装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添加到文件中,该文件指定应用程序的名称,版本,主文件,启动应用程序的命令以及软件许可证:

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

const net = require('net');
const port = 7070;
const host = '127.0.0.1';

我们将为此应用程序使用端口7070,但您可以使用任何可用的端口。我们使用HOST127.0.0.1以确保我们的服务器只在网络接口上监听。稍后我们将Nginx作为反向代理放在此应用程序的前面。Nginx非常精通处理多个连接和水平扩展。

然后添加此代码以使用模块net中的createServer()函数生成TCP服务器。然后开始使用模块netlisten()功能监听端口和您定义的主机上的连接:

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

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

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

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后,使用它来运行服务器。您将使用pm2命令而不是运行npm start以启动服务器:

pm2 start server.js

你会看到这样的输出:

[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

你会看到这个输出:

[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

┌──────────┬────┬──────┬──────┬────────┬─────────┬────────┬─────┬───────────┬───────┬──────────┐
│ 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,以使TCP连接转发到我们的Node.js服务器。

我们必须编辑主Nginx配置文件,因为配置TCP连接转发的块stream仅作为顶级块。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指令告诉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

更改port3000并更改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连接并到达您的服务器:

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

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

结论

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

想要了解更多关于使用PM2和Nginx开发Node.js TCP服务器应用程序的相关教程,请前往腾讯云+社区学习更多知识。


参考文献:《How To Develop a Node.js TCP Server Application using PM2 and Nginx on Ubuntu 16.04》

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏云计算教程系列

如何在Ubuntu 16.04上安装Bro

Bro是一个开源网络分析框架和安全监控应用程序。它将OSSEC和osquery的一些最佳功能集成到一个包中。

2075
来自专栏云计算教程系列

如何在Ubuntu 18.04上安装和配置Postfix

Postfix是一种流行的开源邮件传输代理(MTA),可用于在Linux系统上路由和传递电子邮件。据估计,互联网上约有25%的公共邮件服务器运行Postfix。

2961
来自专栏python全栈布道师

git常用操作,都在这里了(一)

47212
来自专栏CodingToDie

git 常用命令(1)

配置Name和Email 命令格式: git config --global user.name "your name" git config --global...

3416
来自专栏小樱的经验随笔

Linux目录结构

Linux 目录结构 装完Linux,首先需要弄清Linux 标准目录结构 ? / root —?启动Linux时使用的一些核心文件。如操作系统内核、引导程序G...

4136
来自专栏守望轩

Visual Studio 2008 每日提示(二十七)

#261、按Ctrl+Alt+B打开断点窗口 原文链接:You can press Ctrl+Alt+B to open the Breakpoint Win...

3206
来自专栏阮一峰的网络日志

Linux服务器的初步配置流程

开发网站的时候,常常需要自己配置Linux服务器。 本文记录配置Linux服务器的初步流程,也就是系统安装完成后,下一步要做的事情。这主要是我自己的总结和备忘,...

7526
来自专栏虚拟化云计算

oVirt之软件架构全剖析

本文是从软件架构设计的角度剖析oVirt的vdsm,另外还有一篇是从功能架构设计的角度剖析oVirt。

2034
来自专栏决胜机器学习

《Redis设计与实现》读书笔记(二十五) ——Redis主从复制具体过程

《Redis设计与实现》读书笔记(二十五) ——Redis主从复制具体过程 (原创内容,转载请注明来源,谢谢) 一、PSYNC命令执行过程 ...

3705
来自专栏云计算教程系列

如何在Debian 9上安装Linux,Apache,MariaDB,PHP(LAMP)堆栈

“LAMP”堆栈是一组开源软件,通常安装在一起以使服务器能够托管动态网站和Web应用程序。这个术语实际上是一个缩写,代表L inux操作系统,带有A pache...

3352

扫码关注云+社区

领取腾讯云代金券