Допустим, у нас есть файловый дескриптор клиента, принятый функцией 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
не гарантирует, что он устанавливает сокет в обоих наборах readfds и writefds.
извините, я имел в виду FD_ISSET(client_socktes, writefds)
Цель 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
. Затем вы подождите, пока сокет станет доступным для записи (освободите место в буфере), прежде чем повторить попытку неудачной отправки снова.
@RemyLebeau, это один из подходов, но это не единственный способ сделать это. Например, иногда мои исходящие данные хранятся в виде хэш-карты объектов C++, которые я сериализую по запросу (т.е. только тогда, когда я знаю, что есть буферное пространство, доступное для получения хотя бы некоторых сериализованных байтов). В этом случае я FD_SET(mySocket, &writeFD)
всякий раз, когда у меня есть больше сериализованных байтов для отправки или, моя таблица объектов C++ не пуста, и когда сокет выбирает готовый к записи, я сериализую содержимое таблицы в байтовый буфер и отправляю как многое из этого, как сокет примет.
Опечатка? Теперь select позволит нам написать наш ответ клиенту:
if (FD_ISSET(client_socket, &readfds)
?