Читать и записывать в тот же сокет (TCP), используя select

Мы пишем клиент и сервер, чтобы сделать (что я думал) довольно простые сетевые коммуникации. Клиенты Mulitple подключаются к серверу, который затем должен отправить данные обратно всем остальным клиентам.

Сервер просто сидит в блокирующем цикле select, ожидающем трафик, и когда он приходит, отправляет данные другим клиентам. Кажется, это работает нормально.

Проблема заключается в клиенте. В ответ на чтение он иногда захочет написать запись.

Однако я обнаружил, что если я использую:

 rv = select(fdmax + 1, &master_list, NULL, NULL, NULL);

Мой код будет блокироваться до тех пор, пока не будут прочитаны новые данные. Но иногда (асинхронно, из другого потока) у меня будут новые данные для записи в потоке сетевой связи. Итак, я хочу, чтобы мой выбор периодически просыпался и позволял мне проверять, есть ли данные для записи, например:

if (select(....) != -1)
{
  if (FD_SET(sockfd, &master_list))
     // handle data or disconnect
  else
     // look for data to write and write() / send() those.
}

Я попробовал настроить режим опроса (или смехотворно короткие таймауты) с помощью

// master list contains the sockfd from the getaddrinfo/socket/connect seq
struct timeval t;
memset(&t, 0, sizeof t);
rv = select(fdmax + 1, &master_list, NULL, NULL, &t);

но обнаружили, что тогда клиент никогда не получает никаких входящих данных.

Я также попытался установить, что сокет fd не блокируется, например:

fcntl(sockfd, F_SETFL, O_NONBLOCK);

но это не решает проблему:

  • Если мой клиент select() не имеет struct timeval, чтение данных работает, но оно никогда не разблокируется, чтобы позволить мне искать доступные для записи данные.
  • Если у моего клиента select() есть timeval, чтобы его опросить, то он никогда не сигнализирует, что есть данные, которые нужно прочитать, и мое приложение замирает, думая, что нет сетевого подключения (несмотря на то, что все остальные вызовы функций преуспели)

Любые указатели на то, что я могу делать неправильно? Невозможно выполнить чтение-запись в одном сокете (я не могу поверить, что это правда).

(EDIT: правильный ответ и то, что я помню на сервере, но не на клиенте, должен иметь второй fd_set и копировать master_list перед каждым вызовом select():

// declare and FD_ZERO read_fds:
// put sockfd in master_list

while (1)
{
   read_fds = master_list;
   select(...);

   if (FD_ISSET(read_fds))
     ....
   else
     // sleep or otherwise don't hog cpu resources
}

)

Ответ 1

Все выглядит отлично, за исключением строки, в которой вы делаете if (FD_SET(sockfd, &master_list)). У меня очень похожая структура кода, и я использовал FD_ISSET. Вы должны проверить, установлен ли список, а не устанавливать его снова. Кроме этого, я больше ничего не вижу.

Изменить. Кроме того, для таймаута у меня есть следующее:

timeval listening_timeout;
listening_timeout.tv_sec = timeout_in_seconds;
listening_timeout.tv_usec = 0;

возможно, есть проблема, если вы установите его на 0 (как вы, кажется, делаете?)

Edit2. Я вспомнил, что столкнулся с какой-то странной проблемой, когда я не очищал считываемый набор после того, как выбрал выход, и до того, как я снова ввел его. Мне пришлось сделать что-то вроде:

FD_ZERO(&sockfd);
FD_SET(sockfd, &rd);

прежде чем я ввел select. Однако я не помню, почему.

Ответ 2

Кажется, я вспоминаю трюк о создании и совместном использовании файла чтения/записи filedescriptor между сетевым потоком и основным потоком, который добавляется к дескрипторам в вызове select. Этот fd имеет один байт, написанный им основным потоком, когда ему есть что отправить. Запись пробуждает сетевой поток из выбранного вызова, а сетевой поток затем захватывает данные из общего буфера и записывает его в сеть, а затем возвращается в режим ожидания.

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

Ответ 3

Я не вижу ничего плохого в вашем коде, поэтому он должен работать. Если вы не можете заставить его работать, одним из способов обойти его было бы создать канал, который будет использоваться вашим потоком чтения, и поток, который подготавливает вещи для записи, и добавьте конец чтения канала в ваш набор select. Затем, когда другой поток подготовил данные для записи, он просто отправляет что-то на трубку, поток чтения просыпается от select, и затем он может писать. В зависимости от того, как часто есть данные для чтения или записи, это также может быть более эффективным.

Ответ 4

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