Как правильно использовать буфер при обмене данными через recv / send в UNIX? - PullRequest
1 голос
/ 03 ноября 2019

Я делаю простой чат, который должен отправить текстовое сообщение, сделанное одним участником всем остальным участникам. Формат сообщения, которое должен получить каждый, - «[IP]: привет!». Также сервер должен уведомлять всех, когда кто-то подключается или отключается: «[IP] подключился» и «[IP] отключен» соответственно.

Вот фрагмент кода, который реализует эту функцию сервера. Вы можете начать поиск, так как строка, содержащая комментарий «ПРОБЛЕМА ЗДЕСЬ»:

while (true)
{

// select() for reading

static constexpr int bufferSize = 1024;
static char buffer[bufferSize];

// this is for getting IP-address of sender
static sockaddr_in sockAddr;
static socklen_t sockAddrSize;

if (FD_ISSET(masterSocket, &set)) // masterSocket is a socket that establishes connections
{
    sockAddrSize = sizeof(sockAddr);
    int slaveSocket = accept(masketSocket, &sockAddr, &sockAddrSize); // slaveSocket is a client socket

    // setting a slaveSocket non-blocking

    sprintf(buffer, "[%d.%d.%d.%d] has connected\n", 
        (sockAddr.sin_addr.s_addr & 0x000000FF), 
        (sockAddr.sin_addr.s_addr & 0x0000FF00) >> 8, 
        (sockAddr.sin_addr.s_addr & 0x00FF0000) >> 16, 
        (sockAddr.sin_addr.s_addr & 0xFF000000) >> 24);

    for (const auto &socket : slaveSockets)
        send(socket, buffer, strlen(buffer), MSG_NOSIGNAL);

    slaveSockets.insert(slaveSocket);
}

for (const auto &socket : slaveSockets)
{
    if (FD_ISSET(socket, &set))
        continue;

    static int recvSize = recv(socket, buffer, bufferSize, MSG_NOSIGNAL);

    if (recvSize == 0 && errno != EAGAIN)
    {
        sockAddrSize = sizeof(sockAddr);
        getsockname(socket, (sockaddr *) &sockAddr, &sockAddrSize);
        sprintf(buffer, "[%d.%d.%d.%d] has disconnected\n", 
                    (sockAddr.sin_addr.s_addr & 0x000000FF), 
                    (sockAddr.sin_addr.s_addr & 0x0000FF00) >> 8, 
                    (sockAddr.sin_addr.s_addr & 0x00FF0000) >> 16, 
                    (sockAddr.sin_addr.s_addr & 0xFF000000) >> 24);

        shutdown(socket, SHUT_RDWR);
        close(socket);
        slaveSockets.erase(socket);

        for (const auto &socket : slaveSockets)
            send(socket, buffer, strlen(buffer), MSG_NOSIGNAL);
    }
    else if (recvSize > 0) // THE PROBLEM IS HERE
    {
        static char reply[bufferSize];
        sockAddrSize = sizeof(&sockAddr);
        getsocklen(socket, (sockaddr *) &sockAddr, &sockAddrSize);
        sprintf(reply, "[%d.%d.%d.%d]: %s\n",
            (sockAddr.sin_addr.s_addr & 0x000000FF),
            (sockAddr.sin_addr.s_addr & 0x0000FF00) >> 8,
            (sockAddr.sin_addr.s_addr & 0x00FF0000) >> 16,
            (sockAddr.sin_addr.s_addr & 0xFF000000) >> 24,
            buffer);

        int senderSocket = socket;
        for (const auto &socket : slaveSockets)
        {
            if (socket == senderSocket)
                continue;

            send(socket, reply, strlen(reply), MSG_NOSIGNAL); // even tried the "strlen(reply) + 1"
        }
    }
}

}

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

Клиент A подключился.

Клиент B подключился. Клиент A получил «[127.0.0.1] подключился».

Клиент A отправил «привет». Клиент B получил "[127.0.0.1]: привет \ n0.1] подключился \ n".

Клиент B отправил "что случилось?". Клиент A получил «[127.0.0.1]: что случилось? \ Nподключен \ n».

Клиент A отключен. Клиент B получил «[127.0.0.1] отключен».

Как вы можете видеть, информация о подключении / отключении всегда выводится правильно, но чат работает неправильно: в его конце содержатся части информации о подключении / отключении.

Я искренне верю, что правильно использую буферы и не могу понять, что я делаю неправильно.

Ответы [ 2 ]

2 голосов
/ 03 ноября 2019

buffer не является строкой C с нулевым символом в конце после возврата recv. Это логично - что если вы передадите двоичные данные? Тогда вы захотите recv точно (длина сообщения) байтов и не добавлять никаких нулевых байтов.

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

Вы все еще можете полагаться на отправителядобавление нулевого байта, но затем вы должны передать bufferSize-1 в качестве длины буфера recv, а после вызова recv установить reply[bufferSize-1]=0. Но, возможно, это все-таки не самая лучшая вещь: одна из множества других опций - передать «длину сообщения» в виде 32-разрядного незапятнанного целого числа, проверить максимальную длину (скажем, ни одно сообщение не превышает 1024 символа, иесли есть, ничего не получите и просто закройте сокет), и recv точно передайте байты "длины сообщения" в буфер. Вам все еще нужно будет добавить завершающий нулевой байт, если вы собираетесь использовать буфер как строку в стиле C.

Редактировать: ВАЖНО! Если вы используете TCP (SOCK_STREAM), всегда используйте длину сообщения: сообщение может (и когда-нибудь будет) прочитано recv во фрагментах. Вы обязательно должны объединить их в целое сообщение самостоятельно.

1 голос
/ 03 ноября 2019

Просто чтобы добавить к ответу smitsyn, вы должны использовать recvSize (это меньше 0 в ветке с проблемой - так как вы получили что-то от одного из клиентов), чтобы установить '\0 'внутри буфера.

Ваши статические переменные были инициализированы до 0 (по умолчанию), поэтому ваш буфер содержал' \ 0 'сразу после самого длинного сообщения, которое вы получили (так какостальные были перезаписаны) и вам повезло, что sprintf нашел его и не стал искать его где-то еще в памяти вашей программы (даже за пределами).

Что-то в этом духе должно заставить его работать (для простогоcase):

else if (recvSize > 0) // THE PROBLEM IS HERE
    {
     ...

     buffer[recvSize] = '\0'; // of course make sure it fits! could use a std::min(recvSize, bufferSize - 1)
     sprintf(reply, "[%d.%d.%d.%d]: %s\n",
            (sockAddr.sin_addr.s_addr & 0x000000FF),
            (sockAddr.sin_addr.s_addr & 0x0000FF00) >> 8,
            (sockAddr.sin_addr.s_addr & 0x00FF0000) >> 16,
            (sockAddr.sin_addr.s_addr & 0xFF000000) >> 24,
            buffer); // now this print should work as expected 

Редактировать: потому что я не могу оставлять комментарии: (

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...