diff --git a/Server/README.md b/Server/README.md new file mode 100644 index 0000000..863e84c --- /dev/null +++ b/Server/README.md @@ -0,0 +1,55 @@ +# tcpip +TCP/IP client/server (support many clients) + +______________________________________________________ +Сервер +______________________________________________________ + +Стандартні дії та їх логіку по створенню клієнта я описала +в рефераті і позначила їх межі коментарями у коді. Тут я опишу +деякі додаткові рішення специфічні для конкретно цього +завдання і можливо трохи більш продвинуті. + +Оскільки кожен клієнт має мати свій ідентифікатор зареєстрований +на сервері, то я створюю масив clients_id де і-тому місці +стоїть ідентифікатор того клієнта, якого обслуговує і-тий +файловий дескриптор. По ходу програми саме за допомогою +цього масива реалізовується зв'зка ідентифікатора клаєнта +та номером файлового дескриптора через який помжна надсилати/ +приймати повідомлення і таким чином я уникаю двомірних масивів +для цієї задачі. + +Всі наступні дії детально описані в коментарях у коді. + +________________________________________________________ +Клієнт +________________________________________________________ + +Стандартні дії та їх логіку по створенню клієнта я описав +в рефераті і позначив їх межі коментарями у коді. Тут я опишу +деякі додаткові рішення специфічні для конкретно цього +завдання і можливо трохи більш продвинуті. + +Функції writeToServer та readFromServer досить тривіальні: +одна постійно чекає ввід в консоль від користувача, а інша +постійно перевіряє чи сервер нічого не відправив та якщо +відправив -- виводить на екран. + +Для початку варто зрозуміти, що наш клієнт має приймати +повідомлення від сервера постійно, а не лише, так би мовити, +"парами" (сам надіслав дані, а тоді чекає відповіді від сервера). +Я реалізувала незалежність процесів прийняття і відправки +даних у клієнті за допомогою виділення двох додаткових потоків: +один з яких забезпечує перманентну роботу функції writeToServer(), +а інший роботу readFromServer(). + +Створення нових процесів відбувається досить просто за допомогою +ф-ції pthread_create() (хедерфайл pthread.h). + +Для того, щоб гарненько вийти з програми і просто потренуватись +перехоплювати сигнали я створила змінну flag (додав до неї кваліфікатор +volatile -- він зберігає змінну від оптимізації процесором, адже +процесор може перемістити її в кеш, що додасть деякі складнощі +у доступі до неї з різних процесів, а процесів у нас вже більше одного). +Отже під час натискання ctrl+c флаг змінює своє значення на 1 -- +нескінченний цикл завершується. diff --git a/Server/bin/cli_test_c b/Server/bin/cli_test_c new file mode 100644 index 0000000..9007277 Binary files /dev/null and b/Server/bin/cli_test_c differ diff --git a/Server/bin/cli_test_cpp b/Server/bin/cli_test_cpp new file mode 100644 index 0000000..baf17fb Binary files /dev/null and b/Server/bin/cli_test_cpp differ diff --git a/Server/bin/server_test_cpp b/Server/bin/server_test_cpp new file mode 100644 index 0000000..f57dffe Binary files /dev/null and b/Server/bin/server_test_cpp differ diff --git a/Server/bin/srv_test_c b/Server/bin/srv_test_c new file mode 100644 index 0000000..5588630 Binary files /dev/null and b/Server/bin/srv_test_c differ diff --git a/Server/client_1.png b/Server/client_1.png new file mode 100644 index 0000000..782d803 Binary files /dev/null and b/Server/client_1.png differ diff --git a/Server/client_2.png b/Server/client_2.png new file mode 100644 index 0000000..63a5016 Binary files /dev/null and b/Server/client_2.png differ diff --git a/Server/server.png b/Server/server.png new file mode 100644 index 0000000..5e0130b Binary files /dev/null and b/Server/server.png differ diff --git a/Server/src/Server_C/cli.c b/Server/src/Server_C/cli.c new file mode 100644 index 0000000..51049ac --- /dev/null +++ b/Server/src/Server_C/cli.c @@ -0,0 +1,140 @@ +/** @file cli.c +* @brief implementation of methods for client part. +* +* +* +* +* @author Kekalo Kateryna +*/ + + + + +#include "cli.h" + +int sock; +char * cli_id; +char client_name[20]; + +volatile sig_atomic_t flag = 0; + +void catch_ctrl_c_and_exit(int sig){ + flag = 1; +} + + +void * writeToServer(void *arg ){ + + InputOutput * args = arg; + + int nbytes; + char buf[BUFLEN]; + + while(1){ + fprintf(args->outstream, "Enter message starting with 'user_name | '"); + fscanf(args->instream, "%s", buf ); + buf[strlen(buf)-1] = 0; + + nbytes = write(sock, buf, strlen(buf)+1); + if (nbytes < 0) break; + if (strstr(buf, "stop")) break; + } + catch_ctrl_c_and_exit(2); + +} + + +void * readFromServer (void *arg) { + + InputOutput * args = arg; + int nbytes; + char buf[BUFLEN]; + + while(1){ + nbytes = read(sock, buf, BUFLEN); + if ( nbytes < 0 ) { + perror("Read error\n"); + break; + }else if (nbytes == 0) { + fprintf(args->outstream, "\nServer no message\n"); + break; + } + else { + fprintf(args->outstream, "\n[server] %s\n", buf); + } + } +} + +void cli(FILE * instream, FILE * outstream){ + signal(SIGINT, catch_ctrl_c_and_exit); + + fprintf(stdout, "Enter your nickname: "); + fscanf(instream, "%s", client_name); + + + int err; + struct sockaddr_in server_addr; + struct hostent *hostinfo; + hostinfo = gethostbyname(SERVER_NAME); + if ( NULL == hostinfo ) { + fprintf (stderr, "Unknown host %s. \n", SERVER_NAME); + exit (EXIT_FAILURE); + } + + + // Створення сокета + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(SERVER_PORT); + server_addr.sin_addr = *(struct in_addr*) hostinfo->h_addr; + + sock = socket(AF_INET, SOCK_STREAM, 0); + if ( sock < 0 ) { + perror ("Client: socket was not created "); + exit (EXIT_FAILURE); + } + + + // Під'єднання до сервера + err = connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); + if ( err < 0 ) { + perror("Client: connect failure"); + exit(EXIT_FAILURE); + } + fprintf(outstream, "Connection is ready\n"); + + // Передача ідентифікатора клієнта + write(sock, client_name, strlen(client_name)+1); + + //заповнення структури для різних можливостей вводу виводу + InputOutput *args = malloc(sizeof *args); + args->instream = instream; + args->outstream = outstream; + + //створення нових потоків + pthread_t write_to_server; + + if (pthread_create(&write_to_server, NULL, &writeToServer, args) != 0) { + perror("Create pthread error!\n"); + exit(EXIT_FAILURE); + } + + pthread_t read_from_server; + + if (pthread_create(&read_from_server, NULL, &readFromServer, args) != 0) { + perror("Create pthread error!\n"); + exit(EXIT_FAILURE); + } + + while(1){ + if (flag) break; + } + fprintf(outstream, "The end\n"); + + close(sock); + exit (EXIT_SUCCESS); + +} + + + + diff --git a/Server/src/Server_C/cli.h b/Server/src/Server_C/cli.h new file mode 100644 index 0000000..a40ce83 --- /dev/null +++ b/Server/src/Server_C/cli.h @@ -0,0 +1,71 @@ +/** @file cli.h +* @brief declaration of methods and headers for client part. +* +* +* +* +* @author Kekalo Kateryna +*/ + + + + +#ifndef CLI_H +#define CLI_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +#define SERVER_PORT 5555 +#define SERVER_NAME "127.0.0.1" +#define BUFLEN 512 + +///type for arguments to pass in thread creating function +typedef struct { + FILE * instream; + FILE * outstream; +}InputOutput; + + +void cli(FILE * instream, FILE * outstream); +/** @brief main initializing function of tcp client +* +* @param stream for input username and messages +* @param stream for output messages about runtime actions +*/ + + +void * writeToServer (void *arg); +/** @brief This function get user input and send messages to server. +* +* running in separate thread +* @param pointer to InputOutput structure +*/ + + + + +void * readFromServer (void *arg); +/** @brief This function read messages from server and output them. +* +* running in separate thread +* @param pointer to InputOutput structure +*/ + + +void catch_ctrl_c_and_exit(int sig); +/** @brief handler for signal +*/ + +#endif diff --git a/Server/src/Server_C/cli_test.c b/Server/src/Server_C/cli_test.c new file mode 100644 index 0000000..157c35e --- /dev/null +++ b/Server/src/Server_C/cli_test.c @@ -0,0 +1,7 @@ +#include "cli.h" + + +int main(){ + cli(stdin, stdout); + return 0; +} diff --git a/Server/src/Server_C/srv.c b/Server/src/Server_C/srv.c new file mode 100644 index 0000000..d404c0b --- /dev/null +++ b/Server/src/Server_C/srv.c @@ -0,0 +1,218 @@ +/** @file srv.c +* @brief implementation of methods for server part. +* +* +* +* @author Kekalo Kateryna +*/ + + + +#include "srv.h" + + +void resend(int fd, char * cli_id){ + FILE * inp; + inp = fopen(TEMPFILENAME, "r"); // відкриваємо тимчасовий файл куди записувались повідомлення користувачам, яких не було в мережі + + if (inp==NULL) perror("\nFile was not opened! \n"); + char msg[BUFLEN]; + while (fgets(msg, sizeof(msg), inp) != NULL){ //читаємо кожен рядок + char * r = strstr(msg, " | "); //перевіряємо чи це непустий рядок і чи в ньому є частина з ідентифікатором користувача + if ( r ){ + char name[20]; + memset(name, 0, sizeof(name)); + char message[BUFLEN]; + memset(message, 0, sizeof(message)); + char delim; + + sscanf(msg, "%s %c %s", name, &delim, message); //розділяємо рядок на ідентифікатор та повідомлення + if ( strstr(name, cli_id) ){ // перевіряємо чи це повідомлення саме цьому користувачу + writeToClient(fd, message); // надсилаємо повідомлення + break; + } + } + } + fclose(inp); //закриваємо тимчасовий файл +} + + +int find_client(char * buf, char clients[][BUFLEN], int n_clients){ + char * name; + char * message; + char delim; + + if ( strstr(buf, " | ")!=NULL ){ + sscanf(buf, "%s %c %s", name, &delim, message); //розділяємо рядок на ідентифікатор та повідомлення + for(int i = 2; i < n_clients; i++){ // ітеруємось по всім ідентифікаторам + char * s = strstr(name, clients[i]); // шукаємо того, кому повідомлення + if ( s ){ + writeToClient(i, message); + return 1; + } + } + } + return 0; +} + + + +int readFromClient (int fd, char *buf) { + + int nbytes; + + nbytes = read(fd, buf, BUFLEN); + if ( nbytes < 0 ) { + perror("Read error\n"); + return -1; + }else if (nbytes == 0) { + printf("Client no message\n"); + return -1; + } + else { + printf("Server got a message %s\n", buf); + return 0; + } +} + + +void writeToClient (int fd, char *buf) +{ + int nbytes; + + nbytes = write(fd, buf, strlen(buf) + 1); + printf("Write back: %s\nnbytes=%d\n", buf, nbytes); + + if ( nbytes < 0 ){ + perror("Server: write failure"); + } +} + + + + + + +int server(){ + + int len_cli_id = N_CLIENTS + 4; + char clients_id[len_cli_id][BUFLEN]; // +3 (file descriptors for standart I/O) + 1 (file desctiptor for .tmp.txt) + // масив де і-ий елемент це ідентифікатор клієнта, який обслуговується на і-тому файловому дескрипторі + + for (int i = 0; i < len_cli_id; i++) strcpy(clients_id[i], " "); + + int i, err, opt=1; + int sock, new_sock; + struct sockaddr_in addr; + struct sockaddr_in client; + char buf[BUFLEN]; + char inp[BUFLEN]; + socklen_t size; + FILE * fout; + fout = fopen(TEMPFILENAME, "w"); // відкриття файлу куди записуватимуться повідомлення користувачам яких немає в мережі + + + + //створення сокету + sock = socket (PF_INET, SOCK_STREAM, 0); + if ( sock < 0 ) { + perror("Server: cannot create socket"); + exit (EXIT_FAILURE); + } + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)); + + // зв'язування сокету + addr.sin_family = PF_INET; + addr.sin_port = htons(SERVER_PORT); + addr.sin_addr.s_addr = htonl(INADDR_ANY); // прив'язка до будь-якої локальної адреси + err = bind(sock, (struct sockaddr*)&addr, sizeof(addr)); + if ( err<0 ) { + perror("Server: cannot bind socket\n"); + exit(EXIT_FAILURE); + } + + + + // чекаємо на приєднання + err = listen(sock, 3); + if ( err<0 ){ + perror ("Server: listen queue failure"); + exit(EXIT_FAILURE); + } + + + // структура для poll() + struct pollfd act_set[N_CLIENTS]; + act_set[0].fd = sock; //додаємо перший слухаючий сокет + act_set[0].events = POLLIN; //встановлюємо яку подію ми будемо моніторити + act_set[0].revents = 0; //встановлюємо статус в початкове положення + int num_set = 1; + + + + + while(1) { + int ret = poll(act_set, num_set, -1); //запуск методу poll() для паралельного обслуговування багатьох клієнтів + if ( ret<0 ){ + perror("Server: poll failure"); + exit(EXIT_FAILURE); + } + + if ( ret>0 ){ + for (i=0; i +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +#define SERVER_PORT 5555 +#define BUFLEN 512 +#define N_CLIENTS 200 +#define TEMPFILENAME ".tmp.txt" //ім'я тимчасового файлу куди записуватимуться повідомлення для неприєднаних клієнтів + + +int server(); +/** @brief main initializing function of tcp server +*/ + + + + +void writeToClient (int fd, char *buf); +int readFromClient (int fd, char *buf); + +void resend(int fd, char * cli_id); +/** @brief function which check whether there are not messages for +* client with cli_id username and send them +* +* @param number of file descriptor +* @param username of client +*/ + + +int find_client(char * buf, char clients[][BUFLEN], int n_clients); +/** @brief find user to which msg in buf was sent +* +* @param message +* @param list of usernames +* @param length of list +*/ + + + +#endif diff --git a/Server/src/Server_C/srv_test.c b/Server/src/Server_C/srv_test.c new file mode 100644 index 0000000..394f1f0 --- /dev/null +++ b/Server/src/Server_C/srv_test.c @@ -0,0 +1,11 @@ +#include "srv.c" + + + +int main(){ + + server(); + + return 0; +} + diff --git a/Server/src/Server_CPP/cli.cpp b/Server/src/Server_CPP/cli.cpp new file mode 100644 index 0000000..fb212ca --- /dev/null +++ b/Server/src/Server_CPP/cli.cpp @@ -0,0 +1,141 @@ +/** @file cli.c +* @brief implementation of methods for client part. +* +* +* +* +* @author Kekalo Katerynas +*/ + + + + +#include "cli.h" + +int sock; +char * cli_id; +char client_name[20]; + +volatile sig_atomic_t flag = 0; + +void catch_ctrl_c_and_exit(int sig){ + flag = 1; +} + + +void * writeToServer(void *arg){ + + int nbytes; + char buf[BUFLEN]; + + while(1){ + fprintf(stdout, "Enter message starting with 'user_name|'"); + while (fgets(buf, BUFLEN, stdin)==nullptr){ + printf("Empty string. Try again!\n\t > "); + } + buf[strlen(buf)-1] = 0; + + nbytes = write(sock, buf, strlen(buf)+1); + if (nbytes < 0) break; + if (strstr(buf, "stop")) break; + } + catch_ctrl_c_and_exit(2); + +} + + +void * readFromServer (void *arg) { + + int nbytes; + char buf[BUFLEN]; + + while(1){ + nbytes = read(sock, buf, BUFLEN); + if ( nbytes < 0 ) { + perror("Read error\n"); + break; + }else if (nbytes == 0) { + fprintf(stderr, "\nServer no message\n"); + break; + } + else { + printf("\n[server] %s\n", buf); + } + } +} + +svoid client(){ + signal(SIGINT, catch_ctrl_c_and_exit); + + fprintf(stdout, "Enter your nickname: "); + scanf("%s", client_name); + + + int err; + struct sockaddr_in server_addr; + struct hostent *hostinfo; + hostinfo = gethostbyname(SERVER_NAME); + if ( NULL == hostinfo ) { + fprintf (stderr, "Unknown host %s. \n", SERVER_NAME); + exit (EXIT_FAILURE); + } + + // Створення сокета + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(SERVER_PORT); + server_addr.sin_addr = *(struct in_addr*) hostinfo->h_addr; + + sock = socket(AF_INET, SOCK_STREAM, 0); + if ( sock < 0 ) { + perror ("Client: socket was not created "); + exit (EXIT_FAILURE); + } + + + sock = socket(PF_INET, SOCK_STREAM, 0); + if ( sock<0 ){ + perror("Client: socket was not created"); + exit (EXIT_FAILURE); + } + + // Під'єднання до сервера + err = connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)); + if ( err < 0 ) { + perror("Client: connect failure"); + exit(EXIT_FAILURE); + } + fprintf(stdout, "Connection is ready\n"); + + // Передача ідентифікатора клієнта + write(sock, client_name, strlen(client_name)+1); + + + //створення нових потоків + pthread_t write_to_server; + + if (pthread_create(&write_to_server, NULL, &writeToServer, NULL) != 0) { + perror("Create pthread error!\n"); + exit(EXIT_FAILURE); + } + + pthread_t read_from_server; + + if (pthread_create(&read_from_server, NULL, &readFromServer, NULL) != 0) { + perror("Create pthread error!\n"); + exit(EXIT_FAILURE); + } + + while(1){ + if (flag) break; + } + fprintf(stdout, "The end\n"); + + close(sock); + exit (EXIT_SUCCESS); + + +} + + + + diff --git a/Server/src/Server_CPP/cli.h b/Server/src/Server_CPP/cli.h new file mode 100644 index 0000000..9a3ddc0 --- /dev/null +++ b/Server/src/Server_CPP/cli.h @@ -0,0 +1,54 @@ +/** @file cli.h +* @brief declaration of methods and headers for client part. +* +* +* +* +* @author Kekalo Kateryna +*/ + + +#ifndef CLI_H +#define CLI_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +#define SERVER_PORT 5555 +#define SERVER_NAME "127.0.0.1" +#define BUFLEN 512 + +void client(); +/** @brief main initializing function of tcp client +* +*/ + + +void * writeToServer (void *arg); +/** @brief This function get user input and send messages to server. +* +* running in separate thread +*/ + +void * readFromServer (void *arg); +/** @brief This function read messages from server and output them. +* +* running in separate thread +*/ + + + +void catch_ctrl_c_and_exit(int sig); + +#endif diff --git a/Server/src/Server_CPP/cli_test.cpp b/Server/src/Server_CPP/cli_test.cpp new file mode 100644 index 0000000..ad3e09a --- /dev/null +++ b/Server/src/Server_CPP/cli_test.cpp @@ -0,0 +1,6 @@ +#include "cli.cpp" + +int main(){ + client(); +} + diff --git a/Server/src/Server_CPP/server_test.cpp b/Server/src/Server_CPP/server_test.cpp new file mode 100644 index 0000000..0e2708b --- /dev/null +++ b/Server/src/Server_CPP/server_test.cpp @@ -0,0 +1,7 @@ +#include "srv.cpp" + + +int main(){ + server(); + return 0; +} diff --git a/Server/src/Server_CPP/srv.cpp b/Server/src/Server_CPP/srv.cpp new file mode 100644 index 0000000..b6131a7 --- /dev/null +++ b/Server/src/Server_CPP/srv.cpp @@ -0,0 +1,228 @@ +/** @file srv.c +* @brief implementation of methods for server part. +* +* +* +* +* @author Kekalo Kateryna +*/ + + + +#include "srv.h" + + + + +int server(){ + + int len_cli_id = N_CLIENTS + 4; + char clients_id[len_cli_id][BUFLEN]; // +3 (file descriptors for standart I/O) + 1 (file desctiptor for .tmp.txt) +// масив де і-ий елемент це ідентифікатор клієнта, який обслуговується на і-тому файловому дескрипторі + + for (int i = 0; i < len_cli_id; i++) strcpy(clients_id[i], " "); + + int i, err, opt=1; + int sock, new_sock; + struct sockaddr_in addr; + struct sockaddr_in client; + char buf[BUFLEN]; + char inp[BUFLEN]; + socklen_t size; + FILE * fout; + fout = fopen(TEMPFILENAME, "w"); // відкриття файлу куди записуватимуться повідомлення користувачам яких немає в мережі + + //створення сокету + sock = socket (PF_INET, SOCK_STREAM, 0); + if ( sock < 0 ) { + perror("Server: cannot create socket"); + exit (EXIT_FAILURE); + } + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)); + + // зв'язування сокету + addr.sin_family = PF_INET; + addr.sin_port = htons(SERVER_PORT); + addr.sin_addr.s_addr = htonl(INADDR_ANY); // прив'язка до будь-якої локальної адреси + err = bind(sock, (struct sockaddr*)&addr, sizeof(addr)); + if ( err<0 ) { + perror("Server: cannot bind socket\n"); + exit(EXIT_FAILURE); + } + + + // чекаємо на приєднання + err = listen(sock, 3); + if ( err<0 ){ + perror ("Server: listen queue failure"); + exit(EXIT_FAILURE); + } + + + pollfd act_set[N_CLIENTS]; + act_set[0].fd = sock; //додаємо перший сокет(слухаючий) + act_set[0].events = POLLIN; //встановлюємо яку подію ми будемо моніторити + act_set[0].revents = 0; //встановлюємо статус в початкове положення + int num_set = 1; + + + + + while(1) { + int ret = poll(act_set, num_set, -1); //запуск методу poll() для паралельного обслуговування багатьох клієнтів + if ( ret<0 ){ + perror("Server: poll failure"); + exit(EXIT_FAILURE); + } + + if ( ret>0 ){ + for (i=0; i +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +#define SERVER_PORT 5555 +#define BUFLEN 512 +#define N_CLIENTS 200 +#define TEMPFILENAME ".tmp.txt" + + +int server(); +/** @brief main initializing function of tcp server +*/ + + +void writeToClient (int fd, char *buf); +int readFromClient (int fd, char *buf); +void resend(int fd, char * cli_id); +/** @brief function which check whether there are not messages for +* client with cli_id username and send them +* +* @param number of file descriptor +* @param username of client +*/ + +int find_client(char * buf, char clients[][BUFLEN], int n_clients); +/** @brief find user to which msg in buf was sent +* +* @param message +* @param list of usernames +* @param length of list +*/ + + +#endif diff --git "a/Server/\320\237\321\200\320\276\321\224\320\272\321\202 \342\204\22626" "b/Server/\320\237\321\200\320\276\321\224\320\272\321\202 \342\204\22626" new file mode 100644 index 0000000..3d299bd --- /dev/null +++ "b/Server/\320\237\321\200\320\276\321\224\320\272\321\202 \342\204\22626" @@ -0,0 +1,7 @@ +26. Server +Реалізувати клієнт-серверну систему, що дозволяє клаєнтам посилати +повідомлення на сервер (поток байтів). Повідомлення також +містять ідентифікатори інших клієнтів, і сервер якщо бачить підєднавшегося +відповідного клієнта пересилає йому ці повідомлення. Проект передбачає +текстовий опис роботи програми (документація) та реферат по створенню +клієнт/серверу на Сі/Сі++ . diff --git "a/Server/\320\240\320\265\321\204\320\265\321\200\320\260\321\202 \320\272\321\203\321\200\321\201\320\276\320\262\320\260.pdf" "b/Server/\320\240\320\265\321\204\320\265\321\200\320\260\321\202 \320\272\321\203\321\200\321\201\320\276\320\262\320\260.pdf" new file mode 100644 index 0000000..fb1219a Binary files /dev/null and "b/Server/\320\240\320\265\321\204\320\265\321\200\320\260\321\202 \320\272\321\203\321\200\321\201\320\276\320\262\320\260.pdf" differ