From d5218da883af866e7757befc4a5ddf735e892c8d Mon Sep 17 00:00:00 2001 From: tish Date: Mon, 17 Nov 2025 11:26:08 +0100 Subject: [PATCH] lab5 --- lab5/.gitignore | 4 + lab5/Makefile | 16 ++ lab5/daemon.c | 51 ++++++ lab5/daemon.h | 8 + lab5/main.c | 53 ++++++ lab5/server.c | 435 ++++++++++++++++++++++++++++++++++++++++++++++++ lab5/server.h | 8 + 7 files changed, 575 insertions(+) create mode 100644 lab5/.gitignore create mode 100644 lab5/Makefile create mode 100644 lab5/daemon.c create mode 100644 lab5/daemon.h create mode 100644 lab5/main.c create mode 100644 lab5/server.c create mode 100644 lab5/server.h diff --git a/lab5/.gitignore b/lab5/.gitignore new file mode 100644 index 0000000..ea606a3 --- /dev/null +++ b/lab5/.gitignore @@ -0,0 +1,4 @@ +*.o +chatserver + + diff --git a/lab5/Makefile b/lab5/Makefile new file mode 100644 index 0000000..0a2c0b2 --- /dev/null +++ b/lab5/Makefile @@ -0,0 +1,16 @@ +CC = gcc +CFLAGS = -Wall -Wextra -O2 +OBJ = main.o server.o daemon.o + +chatserver: $(OBJ) + $(CC) $(CFLAGS) -o $@ $(OBJ) + +%.o: %.c + $(CC) $(CFLAGS) -c $< + +clean: + rm -f *.o chatserver + +.PHONY: clean + + diff --git a/lab5/daemon.c b/lab5/daemon.c new file mode 100644 index 0000000..925aa83 --- /dev/null +++ b/lab5/daemon.c @@ -0,0 +1,51 @@ +/* Вспомогательные функции для демонизации сервера */ + +#include +#include +#include +#include +#include +#include +#include + +pid_t pid, sid; + +void daemonize() { + syslog(LOG_DEBUG, " --- STARTING DAEMON --- "); + + /* Закрываем стандартные файловые дескприторы */ + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + pid = fork(); + + if (pid < 0) { + syslog(LOG_CRIT, "Unable to fork process!"); + exit(EXIT_FAILURE); + } + + /* Если fork получился, то родительский процесс можно завершить */ + if (pid > 0) { + syslog(LOG_DEBUG, "Killed parent process"); + exit(EXIT_SUCCESS); + } + + umask(0); + + /* Sid для дочернего процесса */ + sid = setsid(); + + if (sid < 0) { + syslog(LOG_CRIT, "Unable to set session id"); + exit(EXIT_FAILURE); + } + + /* Изменяем текущий рабочий каталог */ + if ((chdir("/")) < 0) { + syslog(LOG_CRIT, "Unable to change working directory"); + exit(EXIT_FAILURE); + } +} + + diff --git a/lab5/daemon.h b/lab5/daemon.h new file mode 100644 index 0000000..62c70f7 --- /dev/null +++ b/lab5/daemon.h @@ -0,0 +1,8 @@ +#ifndef DAEMON_H +#define DAEMON_H + +void daemonize(); + +#endif + + diff --git a/lab5/main.c b/lab5/main.c new file mode 100644 index 0000000..33e1d23 --- /dev/null +++ b/lab5/main.c @@ -0,0 +1,53 @@ +/* + * Сетевой чат с использованием неблокирующих сокетов и select() + * + * Компиляция: make + * + * Запуск: ./chatserver [порт] + * Примеры: + * ./chatserver + * ./chatserver 3425 + * + * Остановка: pkill chatserver + * + * Просмотр логов: + * sudo journalctl -f -t CHAT-SERVER + * sudo journalctl -f -t CHAT-SERVER --since "now" -p info + * + * Подключение клиентов: + * telnet localhost 3425 + * + * Использование чата: + * 1. При подключении введите свое имя + * 2. Отправляйте сообщения - они будут видны всем (broadcast) + * 3. Для личного сообщения: @имя: текст сообщения + * Пример: @Alice: привет, как дела? + * 4. Для группового сообщения: @имя1,имя2,имя3: текст сообщения + * Пример: @Alice,Bob: привет вам обоим! + */ + +#include "server.h" +#include +#include + +int main(int argc, char *argv[]) { + int port = 3425; + + if (argc > 1) { + port = atoi(argv[1]); + if (port <= 0 || port > 65535) { + fprintf(stderr, "Invalid port: %s\n", argv[1]); + fprintf(stderr, "Port must be between 1 and 65535\n"); + return 1; + } + } + + printf("Starting chat server on port %d...\n", port); + printf("Connect with: telnet localhost %d\n", port); + printf("View logs: sudo journalctl -f -t CHAT-SERVER\n"); + printf("Stop server: pkill chatserver\n"); + + chat_server(port); + + return 0; +} diff --git a/lab5/server.c b/lab5/server.c new file mode 100644 index 0000000..1171f43 --- /dev/null +++ b/lab5/server.c @@ -0,0 +1,435 @@ +/* +Реализовать приложение сетевой чат. Сервер должен +использовать неблокирующие сокеты, как способ параллельной +обработки и передачи данных клиентам. Сетевой сервер +должен быть реализован ввиде демона, с контролем и +журналированием ошибок через syslog. Сервер должен +обеспечивать одноадресную и широковещательную отправку +текстовых сообщений, одному или всем участникам соответственно. +*/ + +#include "daemon.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_CLIENTS 100 +#define BUFFER_SIZE 1024 +#define NAME_SIZE 32 + +typedef struct { + int socket; + char name[NAME_SIZE]; + int has_name; +} Client; + +static int stop = 0; +static Client clients[MAX_CLIENTS]; +static int client_count = 0; + +void signal_handler(int sig) { + switch (sig) { + case SIGTERM: + case SIGINT: + syslog(LOG_INFO, "Terminate signal catched. Stopping chat server..."); + stop = 1; + break; + } +} + +void get_timestamp(char *buffer, size_t size) { + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + strftime(buffer, size, "%H:%M:%S", tm_info); +} + +void send_user_list(int client_socket) { + char list[BUFFER_SIZE * 2]; + int offset = 0; + int count = 0; + + offset += snprintf(list + offset, sizeof(list) - offset, + "=== Online users (%d) ===\n", client_count); + + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket != -1 && clients[i].has_name) { + offset += snprintf(list + offset, sizeof(list) - offset, " - %s\n", + clients[i].name); + count++; + } + } + + if (count == 0) { + offset += snprintf(list + offset, sizeof(list) - offset, + " (no other users yet)\n"); + } + + offset += snprintf(list + offset, sizeof(list) - offset, + "========================\n"); + + send(client_socket, list, strlen(list), 0); +} + +void init_clients() { + for (int i = 0; i < MAX_CLIENTS; i++) { + clients[i].socket = -1; + clients[i].has_name = 0; + memset(clients[i].name, 0, NAME_SIZE); + } + client_count = 0; +} + +int add_client(int sock) { + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket == -1) { + clients[i].socket = sock; + clients[i].has_name = 0; + client_count++; + syslog(LOG_INFO, "New client connected (slot %d, total: %d)", i, + client_count); + return i; + } + } + return -1; +} + +void remove_client(int index) { + if (clients[index].socket != -1) { + syslog(LOG_INFO, "Client '%s' disconnected (slot %d)", clients[index].name, + index); + + // Уведомляем других пользователей об отключении + if (clients[index].has_name) { + char notification[256]; + snprintf(notification, sizeof(notification), "*** %s left the chat ***\n", + clients[index].name); + + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket != -1 && i != index && clients[i].has_name) { + send(clients[i].socket, notification, strlen(notification), 0); + } + } + } + + close(clients[index].socket); + clients[index].socket = -1; + clients[index].has_name = 0; + memset(clients[index].name, 0, NAME_SIZE); + client_count--; + } +} + +Client *find_client_by_name(const char *name) { + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket != -1 && clients[i].has_name && + strcmp(clients[i].name, name) == 0) { + return &clients[i]; + } + } + return NULL; +} + +void broadcast_message(const char *message, int sender_index) { + char timestamp[16]; + char formatted[BUFFER_SIZE + NAME_SIZE + 32]; + + get_timestamp(timestamp, sizeof(timestamp)); + snprintf(formatted, sizeof(formatted), "[%s] %s: %s\n", timestamp, + clients[sender_index].name, message); + + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket != -1 && i != sender_index && clients[i].has_name) { + send(clients[i].socket, formatted, strlen(formatted), 0); + } + } + + syslog(LOG_INFO, "Broadcast from '%s': %s", clients[sender_index].name, + message); +} + +void send_private_message(const char *message, int sender_index, + const char *recipient_name) { + Client *recipient = find_client_by_name(recipient_name); + + if (recipient == NULL) { + char error_msg[BUFFER_SIZE]; + snprintf(error_msg, sizeof(error_msg), "Error: User '%s' not found\n", + recipient_name); + send(clients[sender_index].socket, error_msg, strlen(error_msg), 0); + return; + } + + char timestamp[16]; + char formatted[BUFFER_SIZE + NAME_SIZE + 32]; + + get_timestamp(timestamp, sizeof(timestamp)); + + snprintf(formatted, sizeof(formatted), "[%s] %s -> you: %s\n", timestamp, + clients[sender_index].name, message); + send(recipient->socket, formatted, strlen(formatted), 0); + + // Отправляем подтверждение отправителю + snprintf(formatted, sizeof(formatted), "[%s] you -> %s: %s\n", timestamp, + recipient_name, message); + send(clients[sender_index].socket, formatted, strlen(formatted), 0); + + syslog(LOG_INFO, "Private message from '%s' to '%s': %s", + clients[sender_index].name, recipient_name, message); +} + +void send_group_message(const char *message, int sender_index, + const char *recipients_list) { + char recipients_copy[BUFFER_SIZE]; + + strncpy(recipients_copy, recipients_list, BUFFER_SIZE - 1); + recipients_copy[BUFFER_SIZE - 1] = '\0'; + + char *token = strtok(recipients_copy, ","); + while (token != NULL) { + while (*token == ' ') + token++; + char *end = token + strlen(token) - 1; + while (end > token && *end == ' ') { + *end = '\0'; + end--; + } + + if (strlen(token) > 0) { + send_private_message(message, sender_index, token); + } + + token = strtok(NULL, ","); + } +} + +void handle_message(int client_index, char *message) { + // Убираем перевод строки + char *newline = strchr(message, '\n'); + if (newline) + *newline = '\0'; + newline = strchr(message, '\r'); + if (newline) + *newline = '\0'; + + if (strlen(message) == 0) + return; + + // Если у клиента еще нет имени, первое сообщение - это его имя + if (!clients[client_index].has_name) { + // Проверяем, не занято ли это имя + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket != -1 && clients[i].has_name && i != client_index && + strcmp(clients[i].name, message) == 0) { + // Имя уже занято + char error_msg[256]; + snprintf( + error_msg, sizeof(error_msg), + "Error: Name '%s' is already taken. Please choose another name:\n", + message); + send(clients[client_index].socket, error_msg, strlen(error_msg), 0); + syslog(LOG_WARNING, "Client in slot %d tried to use taken name: %s", + client_index, message); + return; + } + } + + strncpy(clients[client_index].name, message, NAME_SIZE - 1); + clients[client_index].has_name = 1; + + char welcome[256]; + snprintf(welcome, sizeof(welcome), "Welcome to chat, %s! Total users: %d\n", + message, client_count); + send(clients[client_index].socket, welcome, strlen(welcome), 0); + + // Отправляем список пользователей + send_user_list(clients[client_index].socket); + + // Уведомляем других о новом пользователе + char notification[256]; + snprintf(notification, sizeof(notification), "*** %s joined the chat ***\n", + message); + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket != -1 && i != client_index && clients[i].has_name) { + send(clients[i].socket, notification, strlen(notification), 0); + } + } + + syslog(LOG_INFO, "Client in slot %d set name: %s", client_index, message); + return; + } + + // Проверяем, не является ли это личным сообщением (@username: message) + if (message[0] == '@') { + char *colon = strchr(message, ':'); + if (colon != NULL) { + *colon = '\0'; + char *recipients = message + 1; // Пропускаем '@' + char *msg_text = colon + 1; + + /* Пропускаем пробелы после двоеточия */ + while (*msg_text == ' ') + msg_text++; + + // Групповые сообщения + if (strchr(recipients, ',') != NULL) { + send_group_message(msg_text, client_index, recipients); + } else { + send_private_message(msg_text, client_index, recipients); + } + return; + } + } + + /* Обычное широковещательное сообщение */ + broadcast_message(message, client_index); +} + +int create_server_socket(int port) { + struct sockaddr_in addr; + + int listener = socket(AF_INET, SOCK_STREAM, 0); + if (listener < 0) { + syslog(LOG_ERR, "Unable to create server socket"); + exit(1); + } + + // Устанавливаем неблокирующий режим + if (fcntl(listener, F_SETFL, O_NONBLOCK) < 0) { + syslog(LOG_ERR, "Unable to set non-blocking mode"); + exit(1); + } + + int opt = 1; + if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { + syslog(LOG_WARNING, "Unable to set SO_REUSEADDR"); + } + + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = INADDR_ANY; + + if (bind(listener, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + syslog(LOG_ERR, "Unable to bind socket to port %d", port); + exit(1); + } + + if (listen(listener, 10) < 0) { + syslog(LOG_ERR, "Unable to listen"); + exit(1); + } + + return listener; +} + +int chat_server(int port) { + openlog("CHAT-SERVER", 0, LOG_DAEMON); + + daemonize(); + + signal(SIGTERM, signal_handler); + signal(SIGINT, signal_handler); + + init_clients(); + + int listener = create_server_socket(port); + syslog(LOG_INFO, "Chat server started on port %d", port); + + while (!stop) { + fd_set readset; + FD_ZERO(&readset); + FD_SET(listener, &readset); + + int max_fd = listener; + + // Добавляем все активные клиентские сокеты + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket != -1) { + FD_SET(clients[i].socket, &readset); + if (clients[i].socket > max_fd) { + max_fd = clients[i].socket; + } + } + } + + // Устанавливаем таймаут для периодической проверки флага stop + struct timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + + int activity = select(max_fd + 1, &readset, NULL, NULL, &timeout); + + if (activity < 0 && errno != EINTR) { + syslog(LOG_ERR, "Select error: %s", strerror(errno)); + continue; + } + + if (activity == 0) { + // Таймаут, просто продолжаем + continue; + } + + // Проверяем, есть ли новое подключение + if (FD_ISSET(listener, &readset)) { + int new_socket = accept(listener, NULL, NULL); + if (new_socket >= 0) { + // Устанавливаем неблокирующий режим для клиентского сокета + if (fcntl(new_socket, F_SETFL, O_NONBLOCK) < 0) { + syslog(LOG_WARNING, "Unable to set non-blocking mode for client"); + } + + int index = add_client(new_socket); + if (index == -1) { + syslog(LOG_WARNING, "Maximum clients reached, rejecting connection"); + const char *msg = "Server full, please try again later\n"; + send(new_socket, msg, strlen(msg), 0); + close(new_socket); + } else { + const char *greeting = "Connected to chat server. Please enter your " + "name:\n"; + send(new_socket, greeting, strlen(greeting), 0); + } + } + } + + // Проверяем данные от клиентов + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket == -1) + continue; + + if (FD_ISSET(clients[i].socket, &readset)) { + char buffer[BUFFER_SIZE]; + int bytes_read = recv(clients[i].socket, buffer, BUFFER_SIZE - 1, 0); + + if (bytes_read <= 0) { + // Клиент отключился + remove_client(i); + } else { + buffer[bytes_read] = '\0'; + handle_message(i, buffer); + } + } + } + } + + // Закрываем все соединения + close(listener); + for (int i = 0; i < MAX_CLIENTS; i++) { + if (clients[i].socket != -1) { + close(clients[i].socket); + } + } + + syslog(LOG_INFO, "Chat server stopped"); + closelog(); + return 0; +} diff --git a/lab5/server.h b/lab5/server.h new file mode 100644 index 0000000..0ed59cf --- /dev/null +++ b/lab5/server.h @@ -0,0 +1,8 @@ +#ifndef SERVER_H +#define SERVER_H + +int chat_server(int port); + +#endif + +