Решение тестового за день

от автора

Предисловие

Как-то раз откликнулся на вакансию С++ разработчика с хорошей вилкой от сорока до сто восьмидесяти тысяч в своем регионе. До этого не имел опыта коммерческой разработки и мне в ответ прислали тестовое задание. Там было три задачи из которых надо было решить две. Как всегда моя любимая рубрика – решение тестовых задач. Сегодня разберем одну из них.

1.      Задача

Добрый день.

Решение желательно разместить в репозитории Github и прислать ссылки. Но можно прислать исходные коды.

Тестовая задача 1. Язык С++. Среда разработки Visual Studio 2019.

Задача. Написать 2 консольных приложения Client.exe и Server.exe под Windows, обменивающихся файлами через UPD сокет с подтверждением по TCP.

1.Сервер. Server.exe

Первый параметр – IP, второй номер порта, третий – каталог для хранения файлов.

Сервер начинает прослушивать по IP адресу порт и ждет подключения клиента по TCP.

Пример:

Server.exe 127.0.0.1 5555 temp

1.      Клиент. Client.exe

Старт с 5 параметрами.

1 и 2 параметры – IP адрес и порт для подключения к серверу. Третий – порт для отправки UDP пакетов. Четвертый – путь к файлу. Пятый – таймаут на подтверждение UDP пакетов в миллисекундах.

Пример

Client.exe 127.0.0.1 5555 6000 test.txt 500

3.Взаимодействие сервера и клиента.

При старте сервер открывает TCP сокет с IP адресом и портом из стартовых параметров(1 и 2 параметры), берет его на прослушивание и ожидает подключений от клиента.

Клиент при старте пытается подключиться к серверу по TCP на IP адрес и порт из стартовых параметров (1 и 2 параметры ) пока не будет установлено соединение. После установления соединения клиент загружает в память файл (3 параметр, размер файла не более 10 Мб.) и отправляет по TCP соединению имя файл и порт UDP на сервер.

Далее клиент начинает отправлять файл блоками (Каждый блок данных в udp пакетах должен содержать свой id)  udp датаграммами и получать по TCP подтверждения о приеме сервером.

В случае если в течении таймаута (5 параметр) не было получено подтверждение на пакет, то пакет по udp отправляется повторно. В случае когда все пакеты подтверждены клиент уведомляет сервер TCP об окончании передачи файла,

закрывает TCP соединение, завершает работу.

Сервер после установления подключения клиента по ТСP, получения имя файла и порта udp открывает udp сокет и начинает принимать udp пакеты исходящие с IP адреса клиента и порта переданного клиентом.

На каждый полученный udp пакет сервер отправляет подтверждение через tcp сокет на клиент. Полученные пакеты хранятся в памяти. После получения от клиента уведомления об окончании передачи (все пакеты подтверждены на клиенте) сервер сохраняет файл в каталог (3 параметр).

Формат посылок по TCP (имя файла, порт и подтверждения пакетов) — на ваше усмотрение

формат и нумерация блоков для udp пакетов — на ваше усмотрение

2.      Решение

Что касается задачи – то она хорошо расписана – без всяких там «белых пятен». Давайте сразу перейдем к коду решения.

Код клиента

#include <iostream> #include "Connection.h"//Класс в который обернуты сокеты #include <fstream> #include <vector> #include <string> int main(int argc,char*argv[]) {     if (argc == 1)     {         std::cout << "no flags argc!" << std::endl;         std::cout << "Usage: Client.exe <ip> <portTCP> <portUDP> <Filename> <Timeout>" << std::endl;         exit(1);     }     Connection* con = new Connection(argv[5]);//создаем соединение и передаем в него таймаут     con->InitClient(argv[1], atoi(argv[2]),atoi(argv[3]));//подключаемся к серверу(передаем ip адрес,tcp порт и udp порт     std::vector<std::string> vecline;     std::string line;     std::ifstream in(argv[4]); // открываем файл для чтения     if (in.is_open())     {         int ch=0;         while((ch=in.get())!=EOF)         {             line += ch;             if (line.size() == 242)             {                 vecline.push_back(line);                 line = "";             }         }         if(line!="")             vecline.push_back(line);     }     in.close();     // закрываем файл     std::string path = argv[4];     std::string filename = path.substr(path.rfind('\\') + 1);//выделяем имя файла из полного пути к файлу     con->Send(argv[3]);//отправляем на сервер udp порт через tcp     con->Send(filename);//передаем на сервер имя файла     con->Send(std::to_string(vecline.size()));//передаем на сервер кол-во строк     std::cout << "Starting transmission" << std::endl;//начинаем передачу     std::string id = "";     for (int i = 0; i < vecline.size(); i++)     {         id = "Begin";         if (i / 10.0 < 1)             id += "00000";         else if((i/100.0<1) && (i/10.0>=1))             id += "0000";         else if ((i/1000.0<1) && (i / 100.0 >= 1))             id += "000";         else if ((i / 10000.0 < 1) && (i / 1000.0 >= 1))             id += "00";         else if ((i / 100000.0 < 1) && (i / 10000.0 >= 1))             id += "00";         else if ((i / 1000000.0 < 1) && (i / 100000.0 >= 1))             id += "00";         else             id += "0";         id += std::to_string(i) + "End";         id += vecline[i]; //обертываем udp пакет служебными данными id формата Begin000001EndData         con->SendUDP(id);//отправляем udp пакет на сервер         //Sleep(1000);         con->Receive();//получаем подтверждение от сервера через tcp         std::cout << "i" << i << std::endl;         if ((i!=0) && (i != atoi(con->GetBuffer())))//в случае если пакеты             i--;//потерялись по дороге то уменьшаем i и по циклу состоится         id = "";//повторная передача     }     con->ClientClose();     con->~Connection();     std::cout << "Done!\n"; }

Клиент считывает файл(текстового формата) посимвольно — пока не достигнет длины 242 символа. 14 символов зарезервировано для udp заголовка . В сумме будет 256=buflen.

Код сервера

#include <iostream> #include "Connection.h" #include <fstream> #include <string> int main(int argc, char* argv[]) {     system("Del \"C:\\temp\\test.txt\"");//удаляем наш тестовый файл если он есть     if (argc == 1)     {         std::cout << "no flags argc!" << std::endl;         std::cout << "Usage: Server.exe <ip> <port> <Folder>" << std::endl;         exit(1);     }     std::string timeout_ = "500";//задаем таймаут     Connection* con = new Connection(timeout_.c_str());     con->InitServer(argv[1], atoi(argv[2]));//стартует сервер с TCP сокетом     con->ReceiveServer();//получаем данные с портом udp от клиента     int portudp = atoi(con->GetBuffer());     con->InitServerUDP(argv[1], portudp); //стартует сервер с UDP сокетом     con->ReceiveServer();//Получаем имя файла     std::string FileName = con->GetBuffer();     if (FileName.size() > FILENAME_MAX)         std::cout << "File name is so long" << std::endl;     con->ReceiveServer();//получаем кол-во строк в файле     int j = atoi(con->GetBuffer());     int i = 0,jj;     std::string id = "";     std::cout << "Starting transmission" << std::endl;//передача     while (i < j)     {         jj = 0;         con->ReceiveServerUDP();         while (con->GetVec()[i][jj++] != 'n')             ;         while (con->GetVec()[i][jj] != 'E')             id += con->GetVec()[i][jj++];//парсим строку         con->SendServer(id);//выделяем id пакета и отправляем его клиенту         std::cout << "i="<<i << std::endl;         if (atoi(id.c_str()) == i) );//сравниваем id пакета и номер строки         {             i++;               }         id = "";     }     std::cout << "Transmission is end" << std::endl;     std::ofstream out;          // поток для записи     std::string path = argv[3];     system("mkdir \"C:\\temp\\\"");//создаем директорию     path += '\\' + FileName;     if (path.size() > FILENAME_MAX)         std::cout << "File name is so long" << std::endl;     std::cout << "Creating file" << std::endl;     out.open(path); // открываем файл для записи     if (out.is_open())     {         std::string strfile = "";         int jj;         for (int i = 0; i < con->GetVec().size(); i++)         {             strfile = "";             jj = 0;             while (con->GetVec()[i][jj++] != 'd')                 ;             for (jj; jj < con->GetVec()[i].size(); jj++)                 strfile += con->GetVec()[i][jj];//Вырезаем заголовки udp             out <<strfile;// и пишем в файл         }         out.close();     }     con->ServerClose();     con->~Connection();     std::cout << "Done!\n"; }

Схема передачи данных:

В самом же классе Connection  в принципе описаны системные сокеты, единственное что хочется упомянуть – это вот данные фрагменты:

Таймауты:

struct timeval timeout;  timeout.tv_sec = 0; if(timeout_!="")     timeout.tv_usec = atoi(timeout_); else     timeout.tv_usec = 500;

И части кода которые вызывается после всех инициализаций(непосредственно перед приемом или передачей – один раз):

if (setsockopt(newsockfd, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof timeout) < 0)             error("setsockopt failed\n");

Для серверного tcp сокета передачи

if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof timeout) < 0)             error("setsockopt failed");

Для клиентского tcp сокета приема

P.S. Считаю что данный пост будет не полным, если я не добавлю класс Connection с привычными для глаза системными сетевыми вызовами функций.

Заголовочный файл Connection.h

#pragma once #include <iostream> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <WinSock.h> #include <fcntl.h> #pragma comment (lib,"WS2_32.lib") #pragma warning(disable : 4996) #include <vector> class Connection { public:     Connection(const char* timeout);     int InitServer(const char* address, int port);     int ServerClose();     int Send(std::string line);     int Receive();     int SendUDP(std::string line);     int ReceiveUDP();     int InitClient(const char* address, int port,int udpport);     int ClientClose();     void bzero(char* buf, int l);     void error(const char* msg);     int SendServer(std::string line);     int ReceiveServer();     int ReceiveServerUDP();     int SendServerUDP();     char* GetBuffer();     std::vector<std::string> GetVec();     int Block(bool block);     int BlockServer(bool block);     int InitServerUDP(const char* address, int port);     virtual ~Connection(); protected: private:     int sockfd, newsockfd,udpsocket;     int buflen;     char* buffer;     struct sockaddr_in serv_addr, cli_addr,serv_addr_udp,cli_addr_udp;     int clilen;     int servlen;     int n;     std::string strFile,strCli;     std::vector<std::string> vecline,veccli;     struct timeval timeout; }; 

Соответственно у сервера три сокета: sockfd слушает tcp порт, newsockfd для передачи tcp пакетов клиенту и udpsocket — для приема udp.

У клиента sockfd — для приема tcp пакетов и udpsocket — для передачи udp пакетов.

Сам класс Connection.cpp

#include "Connection.h" void Connection::bzero(char* buf, int l) {     memset(buf, 0, l); } void Connection::error(const char* msg) {     int err = WSAGetLastError();     perror(msg);     std::cout<<err<<std::endl;     WSACleanup();     std::cin.ignore();     exit(1); } Connection::Connection(const char*timeout_) {     buflen = 256;     buffer = new char[buflen];     strFile = "";     strCli = "";     timeout.tv_sec = 0;     if(timeout_!="")         timeout.tv_usec = atoi(timeout_);     else         timeout.tv_usec = 500; } int Connection::InitServer(const char* address,int port_) {     WSADATA ws = { 0 };     if (WSAStartup(MAKEWORD(2, 2), &ws) == 0)     {         sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);         if (sockfd < 0)             error("ERROR opening socket");         bzero((char*)&cli_addr, sizeof(cli_addr));         memset(serv_addr.sin_zero, 0, sizeof(serv_addr.sin_zero));         serv_addr.sin_family = AF_INET;         serv_addr.sin_port = htons(port_);         std::string str = "";         int i = 0;         while (address[i] != '.')             str += address[i++];         i++;         //std::cout << str<<std::endl;         serv_addr.sin_addr.S_un.S_un_b.s_b1 = atoi(str.c_str());         str = "";         while (address[i] != '.')             str += address[i++];         i++;         serv_addr.sin_addr.S_un.S_un_b.s_b2 = atoi(str.c_str());         //std::cout << str << std::endl;         str = "";         while (address[i] != '.')             str += address[i++];         i++;         serv_addr.sin_addr.S_un.S_un_b.s_b3 = atoi(str.c_str());         //std::cout << str << std::endl;         str = "";         while (i!=strlen(address))             str += address[i++];        // std::cout << str << std::endl;         serv_addr.sin_addr.S_un.S_un_b.s_b4 = atoi(str.c_str());          if (bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)             error("ERROR on binding");         if (listen(sockfd, SOMAXCONN) < 0)             error("ERROR on listen");         std::cout << "Waiting client" << std::endl;         clilen = sizeof(cli_addr);               newsockfd = accept(sockfd, (struct sockaddr*)&cli_addr, &clilen);         std::cout << "Client connected" << std::endl;         if (newsockfd < 0)             error("ERROR on accept");         //if (setsockopt(newsockfd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof timeout) < 0)           //  error("setsockopt failed\n");          if (setsockopt(newsockfd, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof timeout) < 0)             error("setsockopt failed\n");     }     else         WSAGetLastError();     return 0; }  int Connection::InitServerUDP(const char* address,int port) {     udpsocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);     if (udpsocket < 0)         error("ERROR opening socket");     serv_addr_udp.sin_family = AF_INET;     serv_addr_udp.sin_port = htons(port);     std::string str = "";     int i = 0;     while (address[i] != '.')         str += address[i++];     i++;     serv_addr_udp.sin_addr.S_un.S_un_b.s_b1 = atoi(str.c_str());//задаем октеты адреса     str = "";     while (address[i] != '.')         str += address[i++];     i++;     serv_addr_udp.sin_addr.S_un.S_un_b.s_b2 = atoi(str.c_str());//задаем октеты адреса     str = "";     while (address[i] != '.')         str += address[i++];     i++;     serv_addr_udp.sin_addr.S_un.S_un_b.s_b3 = atoi(str.c_str());//задаем октеты адреса     str = "";     while (i != strlen(address))         str += address[i++];     serv_addr_udp.sin_addr.S_un.S_un_b.s_b4 = atoi(str.c_str());//задаем октеты адреса     if (bind(udpsocket, (struct sockaddr*)&serv_addr_udp, sizeof(serv_addr_udp)) < 0)         error("ERROR on binding");     std::cout << "Udp Ready" << std::endl;     return 0; } char* Connection::GetBuffer() {     return buffer; } std::vector<std::string> Connection::GetVec() {     return vecline; } int Connection::ServerClose() {     shutdown(newsockfd, 0);     shutdown(sockfd, 0);     shutdown(udpsocket, 0);     closesocket(newsockfd);     closesocket(sockfd);     closesocket(udpsocket);     WSACleanup();     return 0; } int Connection::SendServer(std::string line) {     memset(buffer, 0, buflen);     n = send(newsockfd, line.c_str(), buflen, 0);     //std::cout << line << std::endl;     if (n < 0)         error("ERROR on send");     return n; } int Connection::Send(std::string line) {     n = send(sockfd, line.c_str(), buflen, 0);     if (n < 0)         error("ERROR on send");     return n; } int Connection::SendUDP(std::string line) {     n = sendto(udpsocket, line.c_str(), buflen, 0, (struct sockaddr*)&serv_addr_udp, sizeof serv_addr_udp);     if (n < 0)         error("ERROR on send udp");     //std::cout << "send" << std::endl;     return n; } int Connection::ReceiveServer() {     memset(buffer, 0, buflen);     n = recv(newsockfd, buffer, buflen, 0);     if (n < 0)         error("ERROR on read");     return n; } int Connection::ReceiveServerUDP() {     memset(buffer, 0, buflen);     int slen = sizeof(sockaddr_in);     n = recvfrom(udpsocket, buffer, buflen, 0, (struct sockaddr*)&cli_addr_udp, &slen);     //std::cout << "n=" << n << std::endl;     if (n < 0)         error("ERROR on read udp");     for (int i = 0; i < strlen(buffer); i++)         strFile += buffer[i];     if (vecline.size()!=0 && strFile == vecline[vecline.size() - 1])         return n;//проверяем не повторилась ли передача udp пакета     vecline.push_back(strFile);     //std::cout << strFile <<" buf len"<< strlen(buffer)<< std::endl;     strFile = "";     return n; } int Connection::Receive() {     memset(buffer, 0, buflen);     n = recv(sockfd, buffer, buflen, 0);     //std::cout << buffer << std::endl;     if (n < 0)         error("ERROR on read");     for (int i = 0; i < strlen(buffer); i++)         strCli += buffer[i];     if (veccli.size() !=0 && strCli == veccli[veccli.size() - 1])         return n;//проверяем не повторилась ли передача tcp пакета     veccli.push_back(strCli);     strCli = "";     return n; } int Connection::Block(bool block) {     u_long argp = block ? 1 : 0;     ioctlsocket(sockfd, FIONBIO, &argp);     return n; } int Connection::BlockServer(bool block) {     u_long argp = block ? 1 : 0;     ioctlsocket(newsockfd, FIONBIO, &argp);     return n; } Connection::~Connection() {     delete[] buffer; } int Connection::InitClient(const char* address_, int port_,int udpport) {     WSADATA ws = { 0 };     if (WSAStartup(MAKEWORD(2, 2), &ws) == 0)     {         sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);         if (sockfd < 0)             error("ERROR opening socket");         bzero((char*)&serv_addr, sizeof(serv_addr));         bzero((char*)&cli_addr, sizeof(cli_addr));         serv_addr.sin_family = AF_INET;         serv_addr.sin_addr.s_addr = inet_addr(address_);         serv_addr.sin_port = htons(port_);         servlen = sizeof(serv_addr);         n = connect(sockfd, (struct sockaddr*)&serv_addr, servlen);         if (n < 0)             error("ERROR on connect");         if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof timeout) < 0)             error("setsockopt failed");         udpsocket = socket(AF_INET, SOCK_DGRAM, 0);         if (udpsocket < 0)             error("ERROR opening udp socket");         memset((char*)&serv_addr_udp, 0, sizeof(serv_addr_udp));         serv_addr_udp.sin_family = AF_INET;         serv_addr_udp.sin_port = htons(udpport);         serv_addr_udp.sin_addr.S_un.S_addr = inet_addr(address_);         //if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof timeout) < 0)           //  error("setsockopt failed");     }     else         WSAGetLastError();     return 0; } int Connection::ClientClose() {     shutdown(udpsocket, 0);     shutdown(sockfd, 0);     closesocket(udpsocket);     closesocket(sockfd);     WSACleanup();     return 0; }   

Заключение и выводы

Вот такая задача – могу сказать, что научился применять передачу данных в такой вот связке и таймаутам, конечно. Единственный минус – кодировка русских букв в файле после приема меняется и не читаемая.

Полный код смотреть здесь:

https://gitlab.com/Gremlin_Rage/cpp/-/tree/master/Client

https://gitlab.com/Gremlin_Rage/cpp/-/tree/master/Server

Класс Connection общий для обоих проектов и находится в проекте с сервером.

По задаче в ней не указано, что делать с таймаутом на сервере – по мне его бы тоже передать на сервер вначале не помешало.


ссылка на оригинал статьи https://habr.com/ru/post/685870/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *