Я читал, что это должно быть безопасно от разных потоков одновременно, но моя программа имеет странное поведение, и я не знаю, что не так.
У меня есть параллельные потоки, взаимодействующие с клиентским сокетом
Поскольку я все еще отправляю, клиент уже получил данные и закрыл сокет.
В то же время я делаю select и recv для этого сокета, который возвращает 0 (поскольку он закрыт), поэтому я закрываю этот сокет. Однако отправка еще не вернулась … и так как я вызываю close на этом сокете, отправка вызова завершается неудачно с EBADF.
Я знаю, что клиент получил данные правильно, так как я выводил их после закрытия сокета, и это правильно. Однако, с моей стороны, мой отправляющий вызов все еще возвращает ошибку (EBADF), поэтому я хочу исправить ее, чтобы она не потерпела неудачу.
Это не всегда происходит. Это случается, может быть, в 40% случаев. Я нигде не сплю. Я должен иметь паузы между посылками или recvs или что-нибудь?
Вот некоторый код:
Посылка:
while(true)
{
// keep sending until send returns 0
n = send(_sfd, bytesPtr, sentSize, 0);
if (n == 0)
{
break;
}
else if(n<0)
{
cerr << "ERROR: send returned an error "<<errno<< endl; // this case is triggered
return n;
}
sentSize -= n;
bytesPtr += n;
}
Прием:
while(true)
{
memset(bufferPointer,0,sizeLeft);
n = recv(_sfd,bufferPointer,sizeLeft, 0);
if (debug) cerr << "Receiving..."<<sizeLeft<<endl;
if(n == 0)
{
cerr << "Connection closed"<<endl; // this case is triggered
return n;
}
else if (n < 0)
{
cerr << "ERROR reading from socket"<<endl;
return n;
}
bufferPointer += n;
sizeLeft -= n;
if(sizeLeft <= 0) break;
}
На клиенте я использую тот же код получения, затем вызываю close () для сокета.
Затем, со своей стороны, я получаю 0 при получении вызова, а также вызову close () на сокете
Тогда моя отправка не удалась. Это все еще не закончено ?! Но мой клиент уже получил данные!
Должен признаться, я удивлен, что вы видите эту проблему так же часто, как и вы, но это всегда возможно, когда вы имеете дело с потоками. Когда вы звоните send()
в конечном итоге вы войдете в ядро, чтобы добавить данные в буфер сокетов, и поэтому весьма вероятно, что произойдет переключение контекста, возможно, к другому процессу в системе. Между тем ядро, вероятно, буферизовало и передало пакет довольно быстро. Я предполагаю, что вы тестируете в локальной сети, поэтому другой конец получает данные, закрывает соединение и очень быстро отправляет соответствующий FIN обратно. Все это может произойти, когда на отправляющей машине все еще выполняются другие потоки или процессы, потому что задержка в локальной сети Ethernet очень мала.
Теперь приходит FIN — ваш приемный поток в последнее время мало что сделал, так как он ждал ввода. Поэтому многие системы планирования немного повысят свой приоритет, и есть большая вероятность, что он будет запущен следующим (вы не указываете, какую ОС вы используете, но это может произойти, по крайней мере, в Linux, например). Этот поток закрывает сокет из-за его нулевого чтения. В какой-то момент вскоре после этого отправляющий поток будет повторно пробужден, но, по-видимому, ядро замечает, что сокет закрыт, прежде чем он вернется из заблокированного send()
и возвращается EBADF
,
Теперь это всего лишь предположение относительно точной причины — помимо прочего, это сильно зависит от вашей платформы. Но вы можете видеть, как это могло произойти.
Самым простым решением, вероятно, является использование poll()
в потоке отправки, но подождите, пока сокет не станет готовым к записи, а не готовым к чтению. Очевидно, что вам также нужно дождаться отправки буферизованных данных — то, как вы это сделаете, зависит от того, какой поток буферизует данные. poll()
вызов позволит вам определить, когда соединение было закрыто, пометив его POLLHUP
, который вы можете обнаружить, прежде чем пытаться send()
,
Как правило, вы не должны закрывать сокет, пока не убедитесь, что буфер отправки полностью очищен — вы можете быть уверены в этом только после send()
вызов вернулся и указывает, что все оставшиеся данные вышли. Я занимался этим в прошлом, проверяя буфер отправки, когда получаю нулевое чтение, и если он не пустой, я устанавливаю флаг «закрытия». В вашем случае отправляющий поток будет использовать это как подсказку для закрытия, когда все будет очищено. Это важно, потому что, если удаленный конец делает половину с shutdown()
тогда вы получите нулевое чтение, даже если оно все еще читает. Однако вам может быть наплевать на половину закрытия, и тогда ваша стратегия в порядке.
Наконец, я бы лично избежал хлопот, связанных с отправкой и получением потоков, и имел бы только один поток, который выполняет и то, и другое — это более или менее важно select()
а также poll()
, чтобы позволить одному потоку выполнения иметь дело с одним или несколькими файловыми дескрипторами, не беспокоясь о выполнении операции, которая блокирует и блокирует другие соединения.
Нашел проблему. Это с моей петлей. Обратите внимание, что это бесконечный цикл. Когда у меня больше не осталось отправки, мой sentSize равен 0, но я все равно буду зацикливаться, чтобы попытаться отправить больше. В это время другой поток уже закрыл этот поток, и поэтому мой вызов отправки для 0 байтов возвращается с ошибкой.
Я исправил это, изменив цикл, чтобы остановить цикл, когда sentSize равен 0, и это исправило проблему!