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