Обработка нескольких вызовов recv() и всех возможных сценариев

Я довольно новичок в C и пишу TCP-сервер, и мне было интересно, как обрабатывать recv() от клиента, который будет отправлять команды, на которые сервер будет отвечать. Ради этого вопроса, давайте просто скажем, что заголовок - это 1-й байт, идентификатор команды - 2-й байт, а длина полезной нагрузки - 3-й байт, за которым следует полезная нагрузка (если есть).

Каков наилучший способ для recv() этих данных? Я думал о том, чтобы вызвать recv() для чтения первых 3 байтов в буфер, проверить, чтобы убедиться, что идентификаторы заголовка и команды действительны, затем проверить длину полезной нагрузки и снова вызвать recv() с длиной полезной нагрузки в качестве длины и добавить ее к задняя часть вышеупомянутого буфера. Читая сетевую статью Биджа (в частности, этот раздел здесь: http://beej.us/guide/bgnet/output/html/singlepage/bgnet.html), он советует использовать "массив, достаточно большой для двоих [max длина] пакетов "для обработки ситуаций, таких как получение следующего пакета.

Каков наилучший способ обработки этих типов recv()? Основной вопрос, но я хотел бы реализовать его эффективно, обрабатывая все возможные случаи. Заранее спасибо.

4 ответа

Решение

Метод, на который ссылается Бидж и который упоминает AlastairG, работает примерно так:

Для каждого одновременного соединения вы поддерживаете буфер для чтения, но еще не обработанных данных. (Это буфер, который Beej предлагает изменить в два раза по максимальной длине пакета). Очевидно, что буфер начинается пустым:

unsigned char recv_buffer[BUF_SIZE];
size_t recv_len = 0;

Всякий раз, когда ваш сокет доступен для чтения, считывайте оставшееся пространство в буфере, затем сразу же пытайтесь обработать то, что у вас есть:

result = recv(sock, recv_buffer + recv_len, BUF_SIZE - recv_len, 0);

if (result > 0) {
    recv_len += result;
    process_buffer(recv_buffer, &recv_len);
}

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

void process_buffer(unsigned char *buffer, size_t *len)
{
    while (*len >= 3) {
        /* We have at least 3 bytes, so we have the payload length */

        unsigned payload_len = buffer[2];

        if (*len < 3 + payload_len) {
            /* Too short - haven't recieved whole payload yet */
            break;
        }

        /* OK - execute command */
        do_command(buffer[0], buffer[1], payload_len, &buffer[3]);

        /* Now shuffle the remaining data in the buffer back to the start */
        *len -= 3 + payload_len;
        if (*len > 0)
            memmove(buffer, buffer + 3 + payload_len, *len);
    }
}

(The do_command() функция проверит правильность заголовка и байта команды).

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

Когда вы обрабатываете несколько одновременных клиентов, у вас просто есть один recv_buffer а также recv_len для каждого клиента и вставьте их в структуру для каждого клиента (которая, вероятно, содержит и другие вещи - например, сокет клиента, возможно, адрес источника, текущее состояние и т. д.).

Хороший вопрос Как идеально вы хотите пойти? Для решения "все пение все танцы" используйте асинхронные сокеты, считывайте все данные, которые можете, когда можете, и всякий раз, когда вы получаете новые данные, вызывайте некоторую функцию обработки данных в буфере.

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

Сделайте что-нибудь похожее на запись. То есть функция обработки команд записывает в буфер. Если в буфере есть данные, то при проверке сокетов (выберите или опрашивайте) проверяйте возможность записи и записывайте столько, сколько можете, не забывая удалять только те байты, которые фактически записаны из буфера.

Круговые буферы хорошо работают в таких ситуациях.

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

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

По сути, я согласен с тем, что вы говорите, говорит Бидж, но не читайте мелкие кусочки за раз. Читайте большие куски за раз. Создание такого сокет-сервера, обучение и проектирование по мере моего развития на основе небольшого опыта работы с сокетами и справочных страниц - это был один из самых забавных проектов, над которыми я когда-либо работал, и очень познавательный.

Решение, которое Alastair описывает, является лучшим с точки зрения производительности. К вашему сведению - асинхронное программирование также известно как программирование на основе событий. Другими словами, вы ожидаете поступления данных в сокет, считываете их в буфер, обрабатываете что / когда можете и повторяете. Ваше приложение может делать другие вещи между чтением данных и их обработкой.

Еще пара ссылок, которые я нашел полезными, делая что-то очень похожее:

Вторая - отличная библиотека, которая поможет реализовать все это.

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

В основном, есть два предположения, если вы используете множественное соединение, тогда лучший способ обработать множественное соединение (будь то сокет прослушивания, readfd или writefd) с помощью select/poll/epoll. Вы можете использовать любой из них в зависимости от ваших требований.

В вашем втором запросе о том, как обрабатывать несколько recv(), можно использовать эту практику: всякий раз, когда поступают данные, просто заглядывайте заголовок (он должен иметь фиксированную длину и формат, как вы описали).

    buff_header = (char*) malloc(HEADER_LENGTH);
    count =  recv(sock_fd, buff_header, HEADER_LENGTH, MSG_PEEK);
    /*MSG_PEEK if you want to use the header later other wise you can set it to zero
      and read the buffer from queue and the logic for the code written below would
      be changed accordingly*/

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

    msg_length=payload_length+HEADER_LENGTH;
    buffer =(char*) malloc(msg_length);
    while(msg_length)
    {
        count = recv(sock_fd, buffer, msg_length, 0);
        buffer+=count;
        msg_length-=count;
    }

таким образом, вам не нужно брать какой-либо массив, имеющий некоторую фиксированную длину, и вы можете легко реализовать свою логику.

Другие вопросы по тегам