Почему поведение selects отличается при попытке чтения и записи сокетов?

Допустим, у нас есть файловый дескриптор клиента, принятый функцией accept().

client_socket = accept(_socket, (sockaddr *)&client_addr, &len)

Теперь мы устанавливаем этот файловый дескриптор в fd_set для чтения и записи:

fd_set readfds;
fd_set writefds;

//zero them
FD_ZERO(readfds);
FD_ZERO(writefds);

//set the client_socket
FD_SET(client_socket, &readfds);
FD_SET(client_socket, &writefds);

теперь мы используем select the, чтобы проверить, доступен ли сокет для чтения или записи:

select(FD_SETSIZE, &readfs, &writefds, NULL, NULL)

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

if (FD_ISSET(client_socket, &readfds) {
    read(client_socket, &buf, 4096);
}
//assume that buf is big enough and that read returns less than 4096

В следующем цикле мы сбрасываем fd_sets, как и раньше. Теперь select позволит нам написать наш ответ клиенту:

if (FD_ISSET(client_socket, &readfds) {
    write(client_socket, &buf, len(buf));
}

До сих пор все работало нормально, но теперь происходит странное поведение. Давайте предположим, что наш клиент сказал нам поддерживать соединение активным, в этом случае мы бы установили fd_set так же, как и раньше, вот так:

//zero them
FD_ZERO(readfds);
FD_ZERO(writefds);

//set the client_socket
FD_SET(client_socket, &readfds);
FD_SET(client_socket, &writefds);
// reading not allowed

При использовании select now это позволит нам снова писать, но не позволит читать из client_socket. НО, если мы изменим настройку writefds на ноль, это позволит нам читать, хотя мы ничего не меняли в readfds.

//zero them
FD_ZERO(readfds);
FD_ZERO(writefds);

//set the client_socket
FD_SET(client_socket, &readfds);
//FD_SET(client_socket, &writefds); -> don't set the file-descriptors for write
// now reading is allowed

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

Опечатка? Теперь select позволит нам написать наш ответ клиенту: if (FD_ISSET(client_socket, &readfds)?

273K 16.03.2022 16:16
select не гарантирует, что он устанавливает сокет в обоих наборах readfds и writefds.
273K 16.03.2022 16:25

извините, я имел в виду FD_ISSET(client_socktes, writefds)

jsiller 16.03.2022 16:38
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
37
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Итак, вопрос в том, как select() узнает, когда вернуться? Ответ в том, что вы должны сказать ему, что должно вызвать его возврат, вызывая FD_SET() соответствующими способами.

Обычно вы хотите, чтобы select() возвращался, когда данные готовы к чтению в любом из ваших сокетов (чтобы вы могли прочитать вновь поступившие данные), поэтому обычно вам следует вызывать FD_SET(mySock, &readFD) на всех ваших сокетах.

FD_SET(mySock, &writeFD) немного более нюансированный. Он говорит select() вернуться, когда сокет имеет доступное буферное пространство для записи выходных байтов. Однако во многих случаях вы хотите, чтобы неselect() возвращался, когда в сокете есть доступное буферное пространство, просто потому, что у вас в настоящее время нет данных, которые вы все равно хотите записать в сокет. В этом сценарии, если вы всегда вызываете FD_SET(mySocket, &writeFD), то select() будет немедленно возвращаться, даже если у вас нет задачи, которую вы хотите выполнить, и это приведет к тому, что ваша программа будет использовать много циклов ЦП без какой-либо пользы.

Таким образом, единственный раз, когда вы должны вызывать FD_SET(mySocket, &writeFD), это если вы знаете, что хотите записать некоторые данные в этот сокет как можно скорее.

В вашей программе, скорее всего, происходит то, что FD_SET(mySocket, &writeFD) вызывает select() немедленный возврат (поскольку mySocket в настоящее время имеет доступное буферное пространство для записи), а затем ваша программа (ошибочно) предполагает, что, поскольку select() вернулся, сокет готов -для чтения, только чтобы узнать, что это не так. В случае, когда вы закомментировали FD_SET(mySocket, &writeFD), OTOH, select() не возвращается до тех пор, пока сокет не будет готов для чтения, и поэтому вы получаете ожидаемое поведение при вызове FD_ISSET(mySocket, &readFD).

«Так что единственный раз, когда вы должны вызывать FD_SET(mySocket, &writeFD), это если вы знаете, что хотите записать некоторые данные в этот сокет как можно скорее.» — точнее, вы должны устанавливать writeFD для сокета только тогда, когда предыдущее write()/send() не удалось с ошибкой EWOULDBLOCK/EAGAIN. Затем вы подождите, пока сокет станет доступным для записи (освободите место в буфере), прежде чем повторить попытку неудачной отправки снова.

Remy Lebeau 16.03.2022 19:54

@RemyLebeau, это один из подходов, но это не единственный способ сделать это. Например, иногда мои исходящие данные хранятся в виде хэш-карты объектов C++, которые я сериализую по запросу (т.е. только тогда, когда я знаю, что есть буферное пространство, доступное для получения хотя бы некоторых сериализованных байтов). В этом случае я FD_SET(mySocket, &writeFD) всякий раз, когда у меня есть больше сериализованных байтов для отправки или, моя таблица объектов C++ не пуста, и когда сокет выбирает готовый к записи, я сериализую содержимое таблицы в байтовый буфер и отправляю как многое из этого, как сокет примет.

Jeremy Friesner 16.03.2022 20:59

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