Как recv() понимает, что все данные получены в случаи TCP?

170
12 февраля 2019, 16:30

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

Однако если посмотреть на реальный код, это не так - recv() на сервере читает ровно столько байт, сколько отправлено с клиента с помощью send()

Каким образом recv() понимает, что все данные получены, и управление нужно вернуть в вызывающий код?

Полный и минимальный пример на голых сокетах:

Сервер:

#include <iostream>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <memory>
#include <arpa/inet.h>
const int BUFFER_SIZE = 1024;
const int PORT = 12345;
int main() {
    //create server socket
    int socketFd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (socketFd < 0) {
        return -1;
    }
    int opt_val = 1;
    setsockopt(socketFd, SOL_SOCKET, SO_REUSEADDR, &opt_val, sizeof opt_val);

    //bind to address
    sockaddr_in socketAddress;
    socketAddress.sin_family = AF_INET;
    socketAddress.sin_port = htons(PORT);
    socketAddress.sin_addr.s_addr = htons(INADDR_ANY);
    int rc = ::bind(socketFd,
                    reinterpret_cast<sockaddr*>(&socketAddress),
                    sizeof(socketAddress));
    if (rc < 0) {
        return -2;
    }

    //listen
    rc = ::listen(socketFd, SOMAXCONN);
    if (rc < 0) {
        return -3;
    }
    //accept new connection
    sockaddr_in socketAdress;
    unsigned int sizeOfSocketAdress = sizeof(socketAdress);
    int clientSocket = ::accept(socketFd, (struct sockaddr *)&socketAdress, &sizeOfSocketAdress);
    if (clientSocket < 0) {
        return -4;
    }
    //receive
    char buffer[BUFFER_SIZE];
    int receivedBytes = ::recv(clientSocket, buffer, BUFFER_SIZE, MSG_NOSIGNAL);
    std::cout << "Received " << receivedBytes << " bytes : "  << buffer << std::endl; // Прочитано 5 байт "hello", хотя буфер не заполнен и соединение не прервано
    return 0;
}

Клиент:

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
    struct sockaddr_in sa;
    int res;
    int socketFd;
    socketFd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (socketFd == -1) {
      perror("cannot create socket");
      exit(EXIT_FAILURE);
    }
    memset(&sa, 0, sizeof sa);
    sa.sin_family = AF_INET;
    sa.sin_port = htons(12345);
    res = inet_pton(AF_INET, "127.0.0.1", &sa.sin_addr);
    if (connect(socketFd, (struct sockaddr *)&sa, sizeof sa) == -1) {
      perror("connect failed");
      close(socketFd);
      exit(EXIT_FAILURE);
    }
    auto buf = "hello";
    auto len = 5;
    int sentBytes = ::send(socketFd, buf, len, 0);
    std::cout << "sent " << sentBytes << "bytes: " << buf << std::endl;
    std::string tmp;
    std::getline(std::cin, tmp); //приостановка выполнения, соединение все еще не закрыто
    return EXIT_SUCCESS;
}
Answer 1

Нет, recv читает количество байт не больше указанного размера буфера, совершенно не волнуясь по поводу получения всех данных. В лучшем случае с клиента приходит пакет с флагом PSH, намекающий, что имеет смысл отдать данные читателю сейчас.

Answer 2

Каким образом recv() понимает, что все данные получены, и управление нужно вернуть в вызывающий код?

Никаким. Это забота приложения. С точки зрения приложения, TCP канал - это обычный файл. Как приложение узнаёт, что некая порция данных принята полностью? Существует только два способа:

  1. Есть определённый символ - разделитель записей. Для большинства текстовых файлов, этот символ - '\n'
  2. В начале порции данных пишется длина блока. Как конкретно это делается - зависит от конкретного приложения.

Оба эти пункта 100% применимы к ТСР соединениям. Например, в HTTP протоколе применяется вариант пункта 1: каждое сообщение НАЧИНАЕТСЯ специальной строкой заголовка и ЗАВЕРШАЕТСЯ пустой строкой. Данные передаются несколько иным способом, но идея та же.

Answer 3

Поведение TCP непредсказуемо :) нет никакой 100% гарантии что 'пакет пользовательских данных' доставлен частично или полностью. Это вам решать в своей программе. Имеет смысл иногда проводить доп проверки на наличие данных, покажу на примере функции вычитывающей все данные для асинхронного сокета:

// функция работает как 'умный' TCP.socket.data.flush
void tcp_recv_empty(int sock, ssize_t sz)
{
    ssize_t       rsz;
    unsigned char rbuf[65536]; // 1500 более бережно :)
#   if !defined(MSG_WAITALL)
#   define MSG_WAITALL 0x40
#   endif
    if (!sz)
    {
        if ((ioctl(sock, FIONREAD, &sz) != 0) || (!sz)) { return; }
    }
    while (sz > 0)
    {
        errno = 0;
        switch ((rsz = recv(sock, rbuf, sizeof(rbuf), MSG_WAITALL)))
        {
            case (ssize_t)-1:
            {
#               if defined(EAGAIN)
                if ((errno == EAGAIN) || (errno == EINTR))
#               elif defined(EWOULDBLOCK)
                if ((errno == EWOULDBLOCK) || (errno == EINTR))
#               endif
                {
                    continue;
                }
                return;
            }
            case 0:
            {
                return;
            }
            default:
            {
                sz -= rsz;
            }
        }
    }
}

Всегда надо полагаться на код возврата recv*() и состояние получаемое например такой функцией:

int net_socket_iserror(int sock)
{
    int se;
    socklen_t sl = sizeof(int);
#   if defined(SO_ERROR)
    if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &se, &sl) < 0)
    {
        return errno;
    }
    return se;
#   else
    return 0;
#   endif
}
READ ALSO
как вызвать метод одного класса из другого класса

как вызвать метод одного класса из другого класса

Необходимо вызвать метод объекта ob2 из метода exe объекта ob1, используя ссылку, находящуюся в векторе объекта ob1

262
Что может быть быстрее, чем math sqrt?

Что может быть быстрее, чем math sqrt?

Передо мной стоит задача: Мне необходимо максимально быстро найти количество целых квадратных корней из нескольких триллионов целых чисел,...

218
Анонимный объект

Анонимный объект

Что такое анонимный объект в С++?

215