Читать и записывать в тот же сокет (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 потока должны иметь возможность работать с одним и тем же сокетом за раз, поэтому ваш основной поток должен иметь возможность писать клиенту, а другой - в режиме ожидания ожидания входящих данных. Это, конечно, предполагает, что оба потока имеют доступ к списку клиентов.