Обработка нескольких вызовов recv () и всех возможных сценариев - PullRequest
5 голосов
/ 02 декабря 2010

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

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

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

Ответы [ 4 ]

7 голосов
/ 03 декабря 2010

Метод, на который ссылается Beej и который упоминает 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);
    }
}

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

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

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

5 голосов
/ 02 декабря 2010

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

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

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

Циклические буферы хорошо работают втакие ситуации.

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

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

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

2 голосов
/ 02 декабря 2010

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

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

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

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

1 голос
/ 27 августа 2013

В основном, есть два предположения, если вы используете множественное соединение, тогда лучший способ обработать множественное соединение (будь то сокет прослушивания, 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;
    }

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

...