diff --git a/CMakeLists.txt b/CMakeLists.txt index 8768e6e..e8f7be2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,5 +19,6 @@ define_lab(lab4) define_lab(lab5) define_lab(lab6) define_lab(lab7) +define_lab(lab8) define_lab(lab9) define_lab(lab10) \ No newline at end of file diff --git a/lab8/CMakeLists.txt b/lab8/CMakeLists.txt new file mode 100644 index 0000000..b48a2bb --- /dev/null +++ b/lab8/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.16) +set(CMAKE_C_STANDARD 11) + +# Lab name +set(LAB_NAME "lab8") + +# Lab tasks +list(APPEND SOURCE_FILES + server.c + client.c + ) +list(APPEND NON_COMPILABLE_SRC +# .execme + ) + +### Here goes the template + +project("${LAB_NAME}" C) + +add_custom_target("${LAB_NAME}") + +foreach (file IN LISTS SOURCE_FILES) + add_executable("${LAB_NAME}_${file}_run" "${file}") + add_dependencies("${LAB_NAME}" "${LAB_NAME}_${file}_run") +endforeach () + +foreach (file IN LISTS NON_COMPILABLE_SRC) + add_custom_command( + TARGET "${LAB_NAME}" POST_BUILD + DEPENDS "${file}" + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/${file}" + "${CMAKE_CURRENT_BINARY_DIR}/${file}" + ) +endforeach () \ No newline at end of file diff --git a/lab8/README.md b/lab8/README.md new file mode 100644 index 0000000..6a30cf3 --- /dev/null +++ b/lab8/README.md @@ -0,0 +1,38 @@ +# Лабораторная работа №8 + +> Разработка сервера с использованием цикла ожидания событий + +На сервере создаётся и биндится сокет, слушается клиент. Далее в бесконечном цикле с +использованием `select()` обрабатываются запросы (с возможностью подключения нескольких +клиентов) + +Для каждого запроса обрабатывается запрос в plain-text-формате, возвращается ответ в том же формате. + +Для асинхронной работы использованы неблокирующие сокеты. +Их отличие в том, что во время выполнения операции программа не блокирует своё исполнение. + +Используя функцию `fcntl()`, сокеты переводятся в неблокирующий режим. +Вызов любой функции с таким сокетом будет возвращать управление немедленно. + +> Добавить обработку сигналов + +Для обработки сигналов используем системный вызов `signal()`. + +Существуют и другие подходы для создания сервера с циклом ожидания событий. Например, с использованием `fork()`, +как это делается, например, в Apache2, где для обработки клиентов заранее делается массив форков (т.к. форк -- +дорогостоящая операция). +Этот способ подразумевает создание дочернего процесса для обслуживания каждого нового клиента. +При этом родительский процесс занимается только прослушиванием порта и приёмом соединений. + +Но у такого подхода есть минусы. + +1. Если клиентов очень много, создание нового процесса для обслуживания каждого из них может оказаться слишком + дорогостоящей операцией (решается пулом пре-форков). +2. Такой способ неявно подразумевает, что все клиенты обслуживаются независимо друг от друга. + +Для решения этих проблем мы можем использовать `select()`. +Его плюсы заключаются в разрешении вышеописанных проблем. + +Минусы заключаются в том, что программы получаются более сложными по сравнению с первым способом, +а также программа вынуждена отслеживать дескрипторы всех клиентов и работать с ними параллельно +и каждый раз нужно пробегаться в цикле по дескрипторам всех сокетов и проверять, произошло ли какое-либо событие. diff --git a/lab8/client.c b/lab8/client.c new file mode 100644 index 0000000..ae16b90 --- /dev/null +++ b/lab8/client.c @@ -0,0 +1,93 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define USAGE_STRING "Usage: %s [address [port]]\n" \ + "`address' should be valid IPv4 address\n" \ + "`port' should be in range [1, 65535]\n" +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define STR_EQ(a, b) (!strcmp((a),(b))) +#define WARNING(fmt, ...) fprintf(stderr,fmt,##__VA_ARGS__) +#define FATAL(fmt, ...) do {\ + WARNING(fmt,##__VA_ARGS__);\ + exit(1);\ +} while (0) + +int main(int argc, char *argv[]) { + struct in_addr address = {.s_addr = inet_addr("127.0.0.1")}; + int port = PORT; + + switch (argc) { + case 3: + port = strtol(argv[2], NULL, 10); + if (port <= 0 || port >= 0xFFFF) { + FATAL("Could not parse port\n" USAGE_STRING, argv[0]); + } + case 2: + if (!inet_aton(argv[1], &address)) { + FATAL("Could not parse IPv4 address\n" USAGE_STRING, argv[0]); + } + case 1: + break; + default: + FATAL(USAGE_STRING, argv[0]); + } + + int sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd == -1) { + perror("socket()"); + FATAL("Error while creating socket\n"); + } + struct sockaddr_in srv_addr = { + .sin_family = AF_INET, + .sin_addr = address, + .sin_port = htons(port) + }; + + int err = connect(sockfd, (const struct sockaddr *) &srv_addr, sizeof(srv_addr)); + if (err == -1) { + perror("connect()"); + FATAL("Connection error.\n"); + } + + char buff[4096]; + for (;;) { + memset(buff, 0, 4096); + printf("time - print current server time\n" + " - print contents of the file\n" + "quit - quit client\n" + "Enter command: "); + int n = 0; + while ((buff[n++] = getchar()) != '\n'); + buff[n - 1] = 0; + + // Some commands should be parsed on client side + if (STR_EQ(buff, "quit")) { + break; + } + + if (send(sockfd, buff, n, 0) == -1) { + perror("send()"); + FATAL("Could not send data.\n"); + } + + memset(buff, 0, 4096); + if (recv(sockfd, buff, 4096, 0) == -1) { + perror("recv()"); + FATAL("Receiving failed.\n"); + } + printf("%s\n", buff); + } + + close(sockfd); + return 0; +} \ No newline at end of file diff --git a/lab8/config.h b/lab8/config.h new file mode 100644 index 0000000..5ae4d1c --- /dev/null +++ b/lab8/config.h @@ -0,0 +1,21 @@ +#pragma once + +/** + * Server port. Also used in client to specify connection port. + */ +#define PORT 8841 + +/** + * Maximum number of connections pending. + */ +#define PENDING_CONNS_MAX 32 + +/** + * Timeout value in seconds. + */ +#define TIMEOUT_SECS 120 + +/** + * Buffer size for client request. + */ +#define BUF_SIZE 4096 \ No newline at end of file diff --git a/lab8/server.c b/lab8/server.c new file mode 100644 index 0000000..59ebf36 --- /dev/null +++ b/lab8/server.c @@ -0,0 +1,143 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define STR_EQ(a, b) (!strcmp((a),(b))) +#define WARNING(fmt, ...) fprintf(stderr,fmt,##__VA_ARGS__) +#define FATAL(fmt, ...) do {\ + WARNING(fmt,##__VA_ARGS__);\ + exit(1);\ +} while (0) + +char *handle_cmd(char *buff) { + char *ret = NULL; + if (!*buff) { + ret = strdup("Empty request\n"); + } + else if (isalpha(buff[0])) { + if (STR_EQ(buff, "time")) { + // Respond with current time + time_t t = time(NULL); + ret = strdup(ctime(&t)); + } else { + ret = strdup("Unknown command\n"); + } + } else { + // Get files + int fd = open(buff, O_RDONLY); + if (fd == -1) { + perror("open()"); + WARNING("Can't open file %s\n", buff); + return strdup("ERROR: server failed to obtain file.\n"); + } + + struct stat f_stat; + if(fstat(fd, &f_stat) == -1) { + perror("fstat()"); + WARNING("Can't stat file %s\n", buff); + return strdup("ERROR: server failed to obtain file.\n"); + } + ret = malloc(f_stat.st_size); + read(fd, ret, f_stat.st_size); + close(fd); + } + return ret; +} + +void sig_hnd(int signum) { + switch (signum) { + case SIGUSR1: + WARNING("SIGUSR1 handled\n"); + break; + case SIGUSR2: + WARNING("SIGUSR2 handled\n"); + break; + } +} + +int main() { + int sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd == -1) { + perror("socket()"); + FATAL("Error while creating socket\n"); + } + fcntl(sockfd, F_SETFL, O_NONBLOCK); + + struct sockaddr_in srv_addr = {.sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_ANY), + .sin_port = htons(PORT)}, + clt_addr; + + int err = bind(sockfd, (const struct sockaddr *) &srv_addr, sizeof(srv_addr)); + if (err == -1) { + perror("bind()"); + FATAL("Error binding socket to address\n"); + } + err = listen(sockfd, PENDING_CONNS_MAX); + if (err == -1) { + perror("listen()"); + FATAL("Error putting socket to passive mode\n"); + } + printf("Listening on port %d...\n", PORT); + + signal(SIGUSR1, sig_hnd); + signal(SIGUSR2, sig_hnd); + + char buff[4096] = {0}; + fd_set readset, allset; + FD_ZERO(&allset); + FD_SET(sockfd, &allset); + struct timeval timeout = {.tv_sec = TIMEOUT_SECS, .tv_usec = 0}; + socklen_t socklen = sizeof(clt_addr); + for (;;) { + readset = allset; + + if (select(FD_SETSIZE, &readset, NULL, NULL, &timeout) == -1) { + perror("select()"); + FATAL("Server error\n"); + } + for (int i = 0; i < FD_SETSIZE; ++i) { + if (FD_ISSET(i, &readset)) { + if (i == sockfd) { + int sock = accept(sockfd, (struct sockaddr *) &clt_addr, &socklen); + if (sock == -1) { + perror("accept()"); + FATAL("Acception failed.\n"); + } + printf("Client accepted...\n"); + fcntl(sock, F_SETFL, O_NONBLOCK); + FD_SET(sock, &allset); + } else { + int bytes_read = recv(i, buff, sizeof(buff), 0); + if (bytes_read <= 0) { + close(i); + FD_CLR(i, &allset); + printf("Client disconnected...\n"); + continue; + } + + char *read_buff = handle_cmd(buff); + + if (send(i, read_buff, strlen(read_buff), 0) == -1) { + perror("send()"); + FATAL("Error sending data.\n"); + } + free(read_buff); + memset(buff, 0, 4096); + } + } + } + } + return 0; +} \ No newline at end of file