Как правильно доставлять данные по сети
Допустим я три раза на сервере выполняю вызов send
send(sock,"I",sizeof("I"),0);
send(sock,"Y",sizeof("Y"),0);
send(sock,"X",sizeof("X"),0);
На стороне клиента же три раза выполняю вызов recv
recv(sock,buf,sizeof(buf),0);
recv(sock,buf,sizeof(buf),0);
recv(sock,buf,sizeof(buf),0);
Иногда происходит так, что три вызова send выполняются быстрее , чем данные приходят к клиенту, таким образом первый вызов recv считывает все отправленные тремя вызовами send данные, а второй recv замирает в ожидании, приходится делать "подстраховки":
Клиент принимает данные первого вызова send, после чего отправляет положительный ответ серверу, и следующий вызов send на сервере выполняется только после принятия положительного ответа от клиента, это крайне колхозно, поэтому если вы знаете нормальный способ - напишите ответ
Ответы (1 шт):
С программной точки зрения TCP представляет двухсторонний поток данных, и эти данные могут доставляться произвольными кусками за раз, как меньшими так и большими. Делить этот поток на сообщения нужно средствами своего протокола, который работает поверх TCP.
Есть несколько основных способов:
Отправлять и читать сообщения фиксированной длины.
#define MSG_SZ 64 // ... char buf[MSG_SZ]; strncpy("X", buf, MSG_SZ-1); buf[MSG_SZ-1]=0; send(sock, buf, MSG_SZ, 0); strncpy("Y", buf, MSG_SZ-1); buf[MSG_SZ-1]=0; send(sock, buf, MSG_SZ, 0);Приём:
char buf[MSG_SZ]; recv(sock, buf, MSG_SZ, MSG_WAITALL); recv(sock, buf, MSG_SZ, MSG_WAITALL);Использовать разделяющий символ/строку.
#define MSG_SEP "\n<NEXT>\n" #define MSG_SEP_LEN (sizeof(MSG_SEP) - 1) // ... send(sock, "X", sizeof("X")-1, 0); send(sock, MSG_SEP, MSG_SEP_LEN, 0); send(sock, "Y", sizeof("Y")-1, 0); send(sock, MSG_SEP, MSG_SEP_LEN, 0);Приём:
char *recvMsg(int sock) { static char buf[BUF_SZ]; // буфер статический, так что будет сохраняться между вызовами static size_t recv_n = 0; //< колличество данных в буфере char *sep_pos; //< позиция разделителя while(1) { ssize_t recv_rv = 0; sep_pos = memmem(buf, recv_n, MSG_SEP, MSG_SEP_LEN) // сначала ищем разделитель т.к. предыдущий вызов мог прочитать несколько сообщений. // memmem — нестандартная функция для поиска подстроки в памяти аналогичная strstr if (sep_pos) { break; } // Получение дополнительных данных ssize_t recv_rv = recv(sock, buf + recv_n, BUF_SZ - recv_n, 0); if (recv_rv<0) { if (errno == EINTR) { continue; // Системный вызов был прерван сигналом. // Эту ситуацию желательно обрабатывать на unix-подобных ОС } else { error_or_die(); // обработка других ошибок, например разрыва соединения return 0; } } else if (recv_rv == 0) { // соединение закрыто и данных в сокете больше нет, но стоп-строка не найдена. error_or_die(); return 0; } else { // прочитано несколько символов. Добавим их длину. recv_n+=recv_rv; if (recv_n==BUF_SZ) { // Буфер переполнен. error_or_die(); return 0; } } } assert (sep_pos); // Обрезка одного сообщения size_t rv_len = sep_pos-buf; char *rv = malloc(rv_len+1); assert(rv); memncpy(buf, rv, rv_len); rv[rv_len] = 0; // Сдвиг оставшихся в буфере данных в начало recv_n -=( rv_len + MSG_SEP_LEN); memncpy(sep_pos + MSG_SEP_LEN, buf, recv_n); return rv; } // ... char *buf1 = recvMsg(sock); /* ... */ free (buf1); char *buf2 = recvMsg(sock); /* ... */ free (buf2);Отправлять размер сообщения перед ним/в его заголовке.
const char *msg="X"; uint32_t sz = strlen(msg); uint32_t nsz = htonl(nsz); // перевод размера сообщения в сетевой порядок байт. // Вообще говоря, это не обязательно, но необходимо удостоверится, // что обе машины ожидают данные с одинаковым порядком байт совпадает. send(sock, &nsz, 4, 0); send(sock, msg, sz, 0); msg="Y"; sz = strlen(msg); nsz = htonl(nsz) send(sock, &nsz, 4, 0); send(sock, msg, sz, 0);Приём:
uint32_t sz; recv(sock, &sz, 4, MSG_WAITALL); sz = ntohl(sz); recv(sock, buf, sz, MSG_WAITALL); recv(sock, &sz, 4, MSG_WAITALL); sz = ntohl(sz); recv(sock, buf, sz, MSG_WAITALL);Отправлять в одном соединении только одно сообщение.
sock = socket(AF_INET, SOCK_STREAM, 0) connect(sock, (struct sockaddr *) &addr, sizeof(addr); send(sock, "X", sizeof("X"), 0); shutdown(sock, SHUT_WR); /* получение ответа */ close(sock) sock = socket(AF_INET, SOCK_STREAM, 0); connect(sock, (struct sockaddr *) &addr, sizeof(addr); send(sock, "Y", sizeof("Y"), 0); shutdown(sock, SHUT_WR); /* получение ответа */ close(sock);Приём:
sock = accept(srv_sock, 0, 0); size_t recv_n = 0; ssize_t recv_rv; while (1) { recv_rv = recv (sock, buf+recv_n, BUF_SZ - recv_n, 0); if (recv_rv<0) { error_or_die(); /* обработка ошибок */ } else if (recv_rv == 0) { break; } else { // recv_rv > 0 recv_n += recv_rv } } /* формирование ответа */ close(sock); /* приём следующего соединения аналогичен */
Все подходы могут использоваться в различных комбинациях и у каждого подхода есть плюсы и минусы. Например, (1) подходит только для очень простых протоколов, то по факту он же используется в примере (3) для «заголовка»; В HTTP для заголовка используется способ (2), а для содержимого (3) или, в старых версиях, (4).
Также следует помнить о том, что за один раз может придти не только больше, но и меньше данных, поэтому в примерах выше используется MSG_WAITALL или полноценный цикл чтения с буферизацией, как в (2) и (4). С точки зрения накладных расходов на локальную систему обычно аналогичный цикл чтения в пользовательском пространстве предпочтителен и для остальных вариантов. А для случае сервера, обслуживающего множество клиентов он будет зачастую обязателен.
Дисклеймер: Весь код написан умозрительно для иллюстрации принципов, не отлажет и может содержать ошибки.