Как стать автором
Обновить

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

Время на прочтение11 мин
Количество просмотров13K

Предисловие

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

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 общий для обоих проектов и находится в проекте с сервером.

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

Теги:
Хабы:
Всего голосов 19: ↑11 и ↓8+3
Комментарии55

Публикации

Истории

Работа

Программист C++
133 вакансии
QT разработчик
8 вакансий

Ближайшие события