Разработка приложения TCP-сервера на Node.js с помощью PM2 и Nginx в Ubuntu 16.04

Node.js – это популярная открытая среда выполнения JavaScript, разработанная на V8 Javascript. Node.js используется для создания серверных и сетевых приложений. TCP (Transmission Control Protocol) — это сетевой протокол, который обеспечивает надежную, упорядоченную и проверенную доставку потока данных между приложениями. Сервер TCP может принять запрос на соединение TCP, и как только соединение установлено, обе стороны смогут обмениваться потоками данных.

В этом мануале вы узнаете, как создать базовый TCP-сервер Node.js и клиент для тестирования сервера. Сервер будет запущен как фоновый процесс с помощью менеджера процессов Node.js под названием PM2. В качестве обратного прокси-сервера для приложения TCP используется Nginx. В конце мы протестируем соединение клиент-сервер с локального компьютера.

Требования

  • Сервер Ubuntu 16.04, настроенный по этому мануалу.
  • Сервер Nginx, который можно установить по мануалу Установка Nginx в Ubuntu 16.04. Nginx нужно скомпилировать с опцией —with-stream (она идет по умолчанию на свежей установке Nginx через apt в Ubuntu 16.04).
  • Node.js, установленный через официальный PPA (читайте Установка Node.js в Ubuntu 16.04).

1: Создание TCP-приложения Node.js

Приложение Node.js будет написано с использованием сокетов TCP. Это будет простое тестовое приложение, которое поможет вам разобраться с библиотекой Net в Node.js. Она позволяет создавать базовые приложения для сервера и клиента TCP.

Для начала создайте на своем сервере каталог, в котором можно разместить приложение Node.js. В этом мануале приложение будет в каталоге ~/tcp-nodejs-app.

mkdir ~/tcp-nodejs-app

Перейдите в этот каталог:

cd ~/tcp-nodejs-app

Создайте новый файл package.json для вашего проекта. В этом файле будут перечислены пакеты, от которых зависит приложение. Этот файл сделает сборку воспроизводимой, так будет проще поделиться списком зависимостей с другими разработчиками.

nano package.json

Вы также можете сгенерировать package.json с помощью команды npm init, которая запросит у вас подробную информацию о приложении, но все равно придется редактировать файл вручную, чтобы внести дополнительные команды (включая команду запуска). Поэтому мы сразу создадим файл вручную.

Добавьте следующий 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-серверу и клиенту взаимодействовать. Загрузите модуль net с помощью require(), затем определите переменные порта и хоста сервера:

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

В этом приложении используется порт 7070, но вы можете использовать любой другой доступный порт. Мы используем 127.0.0.1 в HOST – это гарантирует, что сервер прослушивает только локальный сетевой интерфейс. Позже перед этим приложением будет настроен Nginx в качестве обратного прокси. Nginx хорошо справляется с обработкой нескольких соединений и горизонтальным масштабированием.

Добавьте этот код для создания TCP-сервера с помощью функции createServer() модуля net. Затем настройте прослушивание подключений к порту и хосту, которые вы определили с помощью функции listen() этого модуля.

...
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 такой фрагмент:

...
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, чтобы сервер больше не передавал ему данные. Добавьте этот код в конец блока connection:

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: Создание TCP-клиента Node.js

TCP-сервер Node.js запущен, можно приступить к созданию TCP-клиента для подключения к серверу и тестирования.

Сервер Node.js, который вы только что написали, все еще работает, блокируя текущий сеанс терминала. Нам нужно, чтобы это продолжалось при разработке клиента, поэтому откройте новое окно терминала или вкладку. Затем снова подключитесь к серверу из новой вкладки.

ssh 8host@your_server_ip

Затем перейдите в каталог tcp-nodejs-app:

cd tcp-nodejs-app

В этом каталоге создайте новый файл client.js:

nano client.js

Клиент будет использовать ту же сетевую библиотеку, которая использовалась в файле server.js для подключения к TCP-серверу. Добавьте этот код в файл для подключения к серверу, используя IP-адрес 127.0.0.1 и порт 7070.

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-серверу, чтобы убедиться, что он работает. Как только соединение будет установлено, клиент отправит на сервер сообщение «Hello From Client» + client.address().address с помощью функции client.write. Сервер получит эти данные и отправит их клиенту.

Как только клиент получит данные от сервера, он выведет ответ на экран. Добавьте этот код, чтобы перехватить событие data  и отобразить ответ сервера в командной строке:

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

В конце добавьте корректную обработку отключений:

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, чтобы иметь возможность перевести его в фоновый режим и корректно перезапускать.

Для начала установите PM2 глобально:

sudo npm install pm2 -g

После установки используйте PM2 для запуска вашего сервера. Вместо npm start нужно использовать команду pm2. Запустите сервер:

pm2 start server.js

Вы получите вывод:

[PM2] Spawning PM2 daemon with pm2_home=/home/8host/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/8host/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, который будет запускаться автоматически.

Выполните следующую команду, чтобы сгенерировать и установить сценарии запуска systemd PM2. Обязательно запустите команду с sudo, чтобы системные файлы устанавливались автоматически.

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 с помощью команды:

pm2 list

Вы получите такой вывод:

┌──────────┬────┬──────┬──────┬────────┬─────────┬────────┬─────┬───────────┬───────┬──────────┐
│ App name │ id │ mode │ pid  │ status │ restart │ uptime │ cpu │ mem       │ user  │ watching │
├──────────┼────┼──────┼──────┼────────┼─────────┼────────┼─────┼───────────┼───────┼──────────┤
│ server   │ 0  │ fork │ 9075 │ online │ 0       │ 4m     │ 0%  │ 30.5 MB   │ 8host │ 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/8host/tcp-nodejs-app/server.js     │
│ script args       │ N/A                                      │
│ error log path    │ /home/8host/.pm2/logs/server-error-0.log │
│ out log path      │ /home/8host/.pm2/logs/server-out-0.log   │
│ pid path          │ /home/8host/.pm2/pids/server-0.pid       │
│ interpreter       │ node                                     │
│ interpreter args  │ N/A                                      │
│ script id         │ 0                                        │
│ exec cwd          │ /home/8host/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. Сейчас блок stream, который настраивает переадресацию TCP-соединений, работает только как блок верхнего уровня. Конфигурация Nginx по умолчанию в Ubuntu загружает блоки server внутри блока 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;
}
}

Он прослушивает TCP-соединения через порт 3000 и передает запросы на сервер Node.js, работающий по порту 7070. Если приложение настроено на прослушивание другого порта, укажите правильный порт. Директива proxy_protocol включает поддержку протокола PROXY для отправки информации о клиенте на внутренние серверы, которые затем могут по мере необходимости обрабатывать эту информацию.

Сохраните файл и выйдите из редактора.

Проверьте конфигурацию Nginx на наличие синтаксических ошибок:

sudo nginx -t

Затем перезапустите Nginx, чтобы активировать функции TCP и UDP проксирования.

sudo sudo ufw allow 3000

Если все было выполнено верно, вы теперь сможете получить доступ к своему приложению через обратный прокси-сервер Nginx.

5: Тестирование соединения между клиентом и сервером

Давайте проверим работу сервера, подключившись к TCP-серверу с локального компьютера с помощью client.jsscript. Для этого вам нужно загрузить разработанный вами файл client.js на локальный компьютер и изменить порт и IP-адрес.

Сначала загрузите файл client.js на локальную машину с помощью scp:

scp 8host@your_server_ip:~/tcp-nodejs-app/client.js client.js

Откройте файл:

nano client.js

Измените порт на 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 и достиг вашего сервера:

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-адреса клиентов; он увидит только IP-адрес Nginx. Nginx не поддерживает отправку реального IP-адреса бэкэнду напрямую (не внося изменений в систему, которые могут повлиять на безопасность), но поскольку вы включили протокол PROXY в Nginx, сервер Node.js теперь получает дополнительное сообщение PROXY, которое содержит настоящий IP. Если вам нужен этот IP-адрес, вы можете адаптировать свой сервер для обработки PROXY-запросов и анализа необходимых вам данных.

Теперь у вас есть написанное на Node.js TCP-приложение и обратный прокси-сервер Nginx. Вы можете использовать это базовое приложение в качестве основы для разработки более сложного приложения.

Заключение

В этом мануале вы научились создавать простые TCP-приложения в Node.js, запускать их в фоновом режиме с помощью

PM2 и настраивать обратный прокси Nginx.

Tags: , , , ,