Лабораторная работа: TCP клиент-серверное приложение с поддержкой пользовательских переменных X, Y, Z.
Сервер реализован как однопроцессное однопоточное приложение. Для одновременной работы с несколькими клиентами используются неблокирующие сокеты и системный вызов poll(2).
Проект состоит из двух частей:
- клиент — консольная программа, через которую пользователь вводит команды;
- сервер — программа, которая принимает TCP-подключения, хранит значения переменных пользователей и вычисляет выражения.
Клиент не выполняет вычисления самостоятельно. Он только отправляет команды на сервер и выводит полученный ответ.
Сервер для каждого пользователя хранит значения трёх переменных:
X, Y, Z
Значения переменных сохраняются между подключениями пользователя. То есть если пользователь Danil задал X = 24, отключился, а потом снова подключился под именем Danil, сервер восстановит значение X.
Общая архитектура приложения:
+-------------------+ TCP +----------------------+
| | <---------------------------> | |
| Клиент | | Сервер |
| | | |
+-------------------+ +----------------------+
| |
| ввод команд |
| |
| "X = 3 * 8" |
|------------------------------------------------------>|
| |
| разбор выражения |
| вычисление |
| сохранение X |
| |
| "OK X = 24" |
|<------------------------------------------------------|
Файлы проекта:
netcalc-lab/
├── CMakeLists.txt
└── src/
├── main.cpp # точка входа, выбор режима server/client
├── server.hpp # описание класса сервера
├── server.cpp # сервер, poll(2), неблокирующие сокеты
├── client.hpp # описание клиента
├── client.cpp # консольный клиент
├── expression.hpp # описание вычислителя выражений
└── expression.cpp # разбор и вычисление арифметических выражений
Протокол прикладного уровня построен поверх TCP.
Формат обмена — текстовый. Каждое сообщение передаётся отдельной строкой и заканчивается символом перевода строки:
\n
Клиент отправляет серверу команды в виде строк. Сервер отвечает строкой с результатом.
Общий формат ответа сервера:
OK <результат>
или
ERR <описание ошибки>
Также при завершении работы сервер может отправить:
BYE
После подключения сервер отправляет клиенту приветствие:
HELLO enter username
Клиент отправляет имя пользователя:
Danil
Сервер создаёт нового пользователя или загружает уже существующие значения переменных и отвечает:
OK user Danil X=0 Y=0 Z=0
Формат:
X = <выражение>
Y = <выражение>
Z = <выражение>
Пример:
X = 3 * 8
Ответ:
OK X = 24
Формат:
вычислить <выражение>
Пример:
вычислить X + 1
Ответ:
OK 25
Формат:
показать
Ответ:
OK X=24 Y=0 Z=0
Формат:
выход
Ответ:
BYE
Для проверки производительности без разбора выражений используется команда:
PING
Ответ:
PONG
Эта команда нужна, чтобы измерять скорость сетевого обмена без затрат на вычисления и проверку корректности выражений.
Диаграмма взаимодействия клиента и сервера:
sequenceDiagram
actor User as Пользователь
participant Client as Клиент
participant Server as Сервер калькулятора
User->>Client: Запуск клиента
Client->>Server: TCP connect 127.0.0.1:4321
Server-->>Client: HELLO enter username
User->>Client: Ввод имени Danil
Client->>Server: Danil
Server-->>Client: OK user Danil X=0 Y=0 Z=0
User->>Client: X = 3 * 8
Client->>Server: X = 3 * 8
Server->>Server: Разбор выражения
Server->>Server: Вычисление результата
Server->>Server: Сохранение X = 24
Server-->>Client: OK X = 24
Client-->>User: SERVER: OK X = 24
User->>Client: вычислить X + 1
Client->>Server: вычислить X + 1
Server->>Server: Подстановка X = 24
Server->>Server: Вычисление 24 + 1
Server-->>Client: OK 25
Client-->>User: SERVER: OK 25
User->>Client: показать
Client->>Server: показать
Server-->>Client: OK X=24 Y=0 Z=0
Client-->>User: SERVER: OK X=24 Y=0 Z=0
User->>Client: выход
Client->>Server: выход
Server-->>Client: BYE
Client-->>User: SERVER: BYE
Client->>Server: Закрытие соединения
Или короче эту диаграмму можно представить так:
Пользователь Клиент Сервер
| | |
| запуск | |
|----------------->| |
| | TCP connect |
| |----------------------------->|
| | HELLO enter username |
| |<-----------------------------|
| имя Danil | |
|----------------->| Danil |
| |----------------------------->|
| | OK user Danil X=0 Y=0 Z=0 |
| |<-----------------------------|
| X = 3 * 8 | |
|----------------->| X = 3 * 8 |
| |----------------------------->|
| | OK X = 24 |
| |<-----------------------------|
| вычислить X + 1 | |
|----------------->| вычислить X + 1 |
| |----------------------------->|
| | OK 25 |
| |<-----------------------------|
| показать | |
|----------------->| показать |
| |----------------------------->|
| | OK X=24 Y=0 Z=0 |
| |<-----------------------------|
| выход | |
|----------------->| выход |
| |----------------------------->|
| | BYE |
| |<-----------------------------|
Требование лабораторной работы:
Сервер должен быть однопроцессным и однопоточным, использовать неблокирующие сокеты и
poll(2).
Это реализовано в файле:
src/server.cpp
В серверной части используется заголовочный файл:
#include <poll.h>Для перевода сокета в неблокирующий режим используется функция fcntl и флаг O_NONBLOCK:
void setNonBlocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1)
{
throw std::runtime_error("fcntl(F_GETFL) failed");
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
{
throw std::runtime_error("fcntl(F_SETFL) failed");
}
}Эта функция вызывается для слушающего сокета сервера и для каждого нового клиентского сокета.
Сервер формирует список файловых дескрипторов и передаёт его в poll:
std::vector<pollfd> fds;
fds.push_back({listenFd, POLLIN, 0});
for (auto &[fd, client] : clients)
{
short events = POLLIN;
if (!client.output.empty())
{
events |= POLLOUT;
}
fds.push_back({fd, events, 0});
}
int rc = poll(fds.data(), fds.size(), -1);poll сообщает серверу, какие сокеты готовы к чтению или записи.
Если готов слушающий сокет, сервер принимает нового клиента.
Если готов клиентский сокет, сервер читает команду или отправляет накопленный ответ.
В сервере не используются:
fork()
std::thread
pthread_create()Все клиенты обслуживаются одним циклом poll.
mkdir -p build
cd build
cmake ..
cmake --build .В первом терминале запустить сервер:
./netcalc server 4321Во втором терминале запустить клиента:
./netcalc client 127.0.0.1 4321SERVER: HELLO enter username
Введите имя пользователя: Danil
SERVER: OK user Danil X=0 Y=0 Z=0
Команды: X = 1+2, вычислить X*3, показать, выход
> X = 3*8
SERVER: OK X = 24
> вычислить X+1
SERVER: OK 25
> показать
SERVER: OK X=24 Y=0 Z=0
> выход
SERVER: BYE
Пример деления на ноль:
> X = 10 / 0
SERVER: ERR division by zero
Пример неизвестной переменной:
> вычислить A + 1
SERVER: ERR unknown variable
Для тестирования производительности используется команда PING, которая не выполняет разбор и вычисление выражений.
Запуск клиента в режиме тестирования:
./netcalc client 127.0.0.1 4321 --perf 100000Пример результата:
Requests: 100000
Time: 1.25 sec
RPS: 80000
Average latency: 0.0125 ms