Отправка TCP пакетов на сервер

150
26 декабря 2021, 08:00

У меня есть сервер написанный на C++, основанный на "неблокирующих сокетах с использованием select" - Пример данный реализации есть в интернете, я использовал эту конструкцию для своего сервера. Моя задача получать данные от клиента для последующего использования:

  1. Запись в БД;
  2. Проверки с участием полученных данных;

Проблема является в том,что второй отправленный пакет не читается.Я заметил, что после получения первого пакета (login),то pass записывает символ: "@" и этим, он завершает работу с сокетом и продолжает делать последующие действия. Я думал, что проблема в нуль терминаторе, но,увы, это не являлось проблемой. Буду благодарен за совет/помощь.

bool MyClass::socket_connect()
{
    struct sockaddr_in addr;
    int bytes_read,listener,opt = 0;
    // opt = отвечает за отключение и включение алгоритма: 0 - отключение, 1 - включение
    listener = socket(AF_INET, SOCK_STREAM, 0);
    if(listener < 0)
    {
        perror("socket");
        exit(1);
    }
    setsockopt(listener, IPPROTO_TCP, TCP_NODELAY, (const char *)&opt, sizeof(opt));
    fcntl(listener, F_SETFL, O_NONBLOCK);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    if(bind(listener, (struct sockaddr *)&addr, sizeof(addr)) < 0)
    {
        perror("bind");
        exit(2);
    }
    listen(listener, 2);
    set<int> clients;
    clients.clear();
    while(1)
    {
        // Заполняем множество сокетов
        fd_set readset;
        FD_ZERO(&readset);
        FD_SET(listener, &readset);
        for(set<int>::iterator it = clients.begin(); it != clients.end(); it++)
            FD_SET(*it, &readset);
        // Задаём таймаут
        timeval timeout;
        timeout.tv_sec = 15;
        timeout.tv_usec = 0;
        // Ждём события в одном из сокетов
        int mx = max(listener, *max_element(clients.begin(), clients.end()));
        if(select(mx+1, &readset, NULL, NULL, &timeout) <= 0)
        {
            perror("select");
            exit(3);
        }
        // Определяем тип события и выполняем соответствующие действия
        if(FD_ISSET(listener, &readset))
        {
            // Поступил новый запрос на соединение, используем accept
            int sock = accept(listener, NULL, NULL);
            if(sock < 0)
            {
                perror("accept");
                exit(3);
            }
            fcntl(sock, F_SETFL, O_NONBLOCK);
            clients.insert(sock);
        }
        for(set<int>::iterator it = clients.begin(); it != clients.end(); it++)
        {
            if(FD_ISSET(*it, &readset))
            {
                // Поступили данные от клиента, читаем их
                bytes_read = recv(*it, log, 10, 0);
                if(log <= 0)
                {
                    // Соединение разорвано, удаляем сокет из множества
                    close(*it);
                    clients.erase(*it);
                    return 0;
                }
                bytes_read = recv(*it, pass, 16, 0);
                if(pass <= 0)
                {
                    // Соединение разорвано, удаляем сокет из множества
                    close(*it);
                    clients.erase(*it);
                    return 0;
                }
                return true;
                // Отправляем данные обратно клиенту
                //char buf[] = "Data received!";
                //send(*it, buf, bytes_read, 0);
            }
        }
    }
return false;
}
Answer 1

Складывается впечатление, что в вашем сервере получение данных от клиентов организовано неправильно с точки зрения того, как устроен TCP.

При использовании TCP на уровне протокола отсутствует такое понятие как "сообщение". Данные передаются потоком, и прибывать они могут частями.

Например, если клиент отправил 10 байт на сервер, то в зависимости от разных факторов, когда на сервере происходит чтение этих данных:

recv(*it, log, 10, 0); // пример из вашего кода

нет гарантии того, что все 10 байт будут доступны для чтения сразу же. Возможно, что при первом вызове recv программа получит только 3 байта, а остальные 7 будут доступны позже. Например:

Клиент ---------------------------------------------> Сервер
Отправляет строку "электростанция"
(отправлено 14 байт)
Клиент                                                Сервер
                          вызывает recv(), получает "электр"
                          (получено 6 байт)
Клиент                                                Сервер
                        вызывает recv(), получает "останция"
                          (получено 8 байт)

Из-за этой особенности работы TCP необходимо правильно организовать обмен сообщениями.

Во-первых, как уже заметил @gbg, необходимо определиться с понятием "сообщение" в вашем протоколе. Как серверу понять, что сообщение было передано целиком и его можно интерпретировать? Можно использовать разные подходы:

  • Передавать сначала длину сообщения, потом само сообщение.

Для этого можно условиться, что сообщение состоит из двух полей, первое имеет фиксированную длину (скажем, 2 байта), и используется для указания длины передаваемой информации, а второе поле - сама информация, имеет длину, указанную в первом поле:

|--------|--------|---  * * * *   ---|
       длина           сообщение
      2 байта           x байт (как указано в первом поле)

Таким образом, сервер (или клиент), начиная читать сообщение знает, что сначала необходимо прочитать два байта и интерпретировать их как длину сообщения. После этого приступить к чтению и продолжать чтение, пока это количество байт не будет получено. Тогда можно считать, что сообщение получено целиком, цикл начинается снова - чтение 2 байт, и т.д.

В примере со строкой "электростанция" это можно было бы представить так:

Поток данных: [14][электростанция][6][молоко]
              "пакет" №1          "пакет" №2
  • Другой способ: Использование служебных символов:

Этот подход отличается тем, что в протоколе определяются служебные символы, которые используются для управления передачей данных. Например, байт \n может быть использован как обозначение конца сообщения. Конечно, при этом нужна гарантия того, что байт \n не может быть частью сообщения полезной нагрузки. Например, если ожидается, что в полезной нагрузке будут присутствовать только латинские буквы a-z и A-Z, то это приемлемое решение.

В примере со строкой "электростанция" это можно было бы представить так:

электростанция[\n]молоко[\n]
"пакет" №1        "пакет" №2

Если же ожидается, что в полезной нагрузке могут быть байты с любыми значениями (двоичный протокол), то можно использовать экранирование символов, чтобы эта схема работала, либо можно выбрать первый способ (с передачей длины сообщения), описанный выше.

То есть вам нужно определить свой протокол обмена данными, чтобы чётко обозначить, что такое "сообщение" с точки зрения приложения.

Далее, на стороне сервера для каждого клиента должен существовать отдельный буфер для временного хранения полученных данных. Так как информация может прибывать частями, то нужно организовать временное хранение этой информации для каждого клиента. Не использование временного буфера или использование одного глобального буфера не позволит серверу функционировать верно:

Клиент 1 -------------------------------------------> Сервер
Отправляет строку "электростанция\n"
(отправлено 15 байт)

Клиент 2 -------------------------------------------> Сервер
Отправляет строку "молоко\n"
(отправлено 7 байт)
                                                      Сервер
                          вызывает recv(), получает "электр"
                          от клиента 1, записывает в буфер
                          Содержимое буфера "электр"
                                                      Сервер
                   (переходит к чтению данных от клиента №2)
                          вызывает recv(), получает "молок"
                          от клиента 2, добавляет в буфер
                          Содержимое буфера "электрмолок"

Поэтому каждый клиент должен иметь своё собственное временное хранилище для поступающих данных. Кажется, что в вашем коде log и pass, куда вы записываете полученные данные, являются общими для всех клиентов.

Можно создать структуру, в которой хранить сокет и буфер (а может, и другие данные):

struct Client
{
    int fd;
    std::string input_buffer;
};

Либо использовать, скажем, std::map:

std::map<int, std::string> clients;

Алгоритм работы при этом может быть такой:

  1. Прочитать данные, поступившие от клиента.
  2. Если временный буфер клиента не пуст, добавить полученные данные к буферу.
  3. Проверить содержимое буфера и определить присутствует ли там хотя бы одно полное сообщение (в соответствии с установленным понятием "сообщение").
  4. Передать сообщение на обработку, удалить его из буфера.
  5. Повторять шаг 3 до тех пор, пока буфер не окажется пустым, либо оставшаяся в нём информация не представляет из себя полного сообщения.
Answer 2

Вот эта логика неверна:

bytes_read = recv(*it, log, 10, 0);
if(log <= 0)
{
      // Соединение разорвано, удаляем сокет из множества
      close(*it);
      clients.erase(*it);
      return 0;
}

Оставлю за скобками, что log у вас - глобальная сущность (что говорит об ужасном дизайне - убирайте глобальные сущности).

Вы обрабатываете системный вызов не в том порядке. Сначала вам нужно обработать код возврата.

  • Если там <=0 - что-то стряслось.

  • Иначе, вам пришли какие-то байты. Но это могут быть не все байты, что вам отправили - так уж работает TCP. Вам нужно убедиться, что вы прочитали все сообщение, которое вам нужно.

По смыслу, вам должны прислать тут пару строк. Опуская тот факт, что пароли голяком по сети не рассылают, предложу вам слать сначала байт с длиной вашей строки, а потом - байты самой строки, либо сделать длину буфера для строки фиксированной.

Тогда после чтения из сокета можно проанализировать полученное и принять решение, нужно ли читать еще и в каком количестве.

Читайте Снейдера, а не примерчики из интернетиков.

READ ALSO
Сортировка с сохранением позиции

Сортировка с сохранением позиции

Есть ли стандартная функция которая позволяет отсортировать массив (вектор или список) с сохранением индексов или как сделать такую сортировку...

90
Кодировка Хаффмана на с++

Кодировка Хаффмана на с++

Пытался сделать программу, которая кодировала бы заданные слова методом Хаффмана, но она работает не идеальноЕсли вводить всего 1 символ...

79
C6386: Переполнение буфера при записи

C6386: Переполнение буфера при записи

Реализую свой класс для работы с матрицами, но в одном из методов класса при анализе кода Visual Studio сообщает о переполнении буфера

217
Задача по функциям C++ [закрыт]

Задача по функциям C++ [закрыт]

Хотите улучшить этот вопрос? Обновите вопрос так, чтобы он вписывался в тематику Stack Overflow на русском

226