
Предисловие
Как-то раз откликнулся на вакансию С++ разработчика с хорошей вилкой от сорока до сто восьмидесяти тысяч в своем регионе. До этого не имел опыта коммерческой разработки и мне в ответ прислали тестовое задание. Там было три задачи из которых надо было решить две. Как всегда моя любимая рубрика – решение тестовых задач. Сегодня разберем одну из них.
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/
Добавить комментарий