Закрытие сокетов, которые не завершили AcceptEx - если и как?

Я понимаю, что если я выполню вызов AcceptEx через указатель функции, как это рекомендуется в документации, то, если я укажу размер буфера получателя, вызов не завершится, пока не будут отправлены некоторые данные:

if (!lpfnAcceptEx(sockListen,
    sockAccept,
    PerIoData->Buffer,
    DATA_BUFSIZE - ((sizeof(SOCKADDR_IN) + 16) * 2), /* receive buffer size */
    sizeof(SOCKADDR_IN) + 16,
    sizeof(SOCKADDR_IN) + 16,
    &dwBytes,
    &(PerIoData->Overlapped)
    ))
    {
        DWORD dwLastError = GetLastError();
        // Handle error

    }

Из MSDN

Если буфер приема предоставлен, перекрывающаяся операция не будет завершена до тех пор, пока соединение не будет принято и данные не будут прочитаны. Используйте функцию getsockopt с параметром SO_CONNECT_TIME, чтобы проверить, было ли принято соединение.

Если сокет не подключен, getockopt возвращает 0xFFFFFFFF. Приложения, которые проверяют, завершена ли перекрывающаяся операция, в сочетании с параметром SO_CONNECT_TIME могут определить, что соединение принято, но данные не получены.

Такие соединения рекомендуется прерывать, закрывая принятый сокет, что приводит к завершению вызова функции AcceptEx с ошибкой.

Теперь это, кажется, означает, что я должен принудительно закрыть сокет. Однако в моей книге «Сетевое программирование для Microsoft Windows — второе издание» приводятся аналогичные факты, но далее говорится

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

Значит, мне нельзя закрывать его сейчас?? Я смущен.

Два вопроса:

1) Если сокет не полностью завершил AcceptEx, я возвращаю 0xFFFFFFFF из getsockopt. Это делает его кандидатом на принудительное закрытие. Но откуда мне знать, как долго он находится в таком состоянии? Я не могу добавить свою собственную логику синхронизации, потому что я не знаю, когда был сделан прием, потому что моя процедура порта завершения не завершена!

2) Когда я выясню, нужно ли мне закрыть сокет, как мне это сделать? Достаточно ли closesocket()?


person Wad    schedule 15.02.2018    source источник


Ответы (2)


1) Если сокет не полностью завершил AcceptEx, я получаю обратно 0xFFFFFFFF из getsockopt. Это делает его кандидатом на принудительное закрытие.

нет. это ошибка. если вы получаете 0xFFFFFFFF, это означает, что клиент не подключается к сокету. он все еще ждет подключения. нам нужно остановить эту операцию, только если мы решим вообще прекратить прослушивание порта. в противном случае нам не нужно закрывать этот сокет или отменять этот ввод-вывод

Но откуда мне знать, как долго он находится в таком состоянии? Я не могу добавить свою собственную логику синхронизации, потому что я не знаю, когда был сделан прием, потому что моя процедура порта завершения не завершена!

но getsockopt с SO_CONNECT_TIME и возвращает количество секунд, в течение которых сокет был подключен:

поэтому, если этот номер 0xFFFFFFFF - AcceptEx, все еще ждите соединения и не должны быть закрыты/отменены. в противном случае (мы получили другое значение) - это количество секунд, в течение которых клиент уже подключен. посмотрите пример кода

поэтому вы можете периодически проверять сокеты - если вы получили N (!=-1) секунд от getsockopt( s, SOL_SOCKET, SO_CONNECT_TIME, (char *)&seconds, (PINT)&bytes) - это означает, что клиент уже N секунд подключен к вашему сокету, но еще не отправляет никаких данных. именно это (когда N становится слишком большим) делает его кандидатом на принудительное закрытие. но не -1 (0xFFFFFFFF) значение.

Так что я не должен закрыть его сейчас?? Я смущен.

вы неправильно понимаете. между двумя частями текста нет противоречия:

... не будет очищен, когда неподключенный дескриптор клиента будет закрыт...

обратите внимание, что здесь говорится о закрывающем дескрипторе, используемом AcceptEx, когда он все еще находится в состоянии неподключен.

Такие соединения (подключены, но данные не получены) рекомендуется завершать, закрывая принятый сокет.

поэтому здесь говорят о закрытии уже подключенного сокета.

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


однако из моего варианта использование буфера приема в AcceptEx не очень хорошая идея. лучше явный вызов WSARecv после подключения клиента. да, это дополнительный вызов ядра. но с другой стороны, если вы используете буфер приема в AcceptEx - вам нужно периодически вызывать getsockopt (и это вызов ядра!) на каждом сокете прослушивания. поэтому вместо одного вызова сокета, где AcceptEx завершено, вам нужно будет сделать N вызовов getsockopt каждый T период времени. когда AcceptEx завершится сразу после подключения клиента - вы сами можете сэкономить время подключения и периодически проверять это время самостоятельно. но для этого вам не нужно обращаться к ядру, и это будет намного быстрее. время, когда вы можете сказать через GetTickCount64

2) Когда я выясню, нужно ли мне закрыть сокет, как мне это сделать? Достаточно ли closesocket()?

да closesocket() нужно и достаточно

person RbMm    schedule 17.02.2018
comment
Да, вы прекрасно заметили количество вызовов ядра. Я хотел понять это с академической точки зрения, теперь, когда я это понял, я могу рассмотреть ваш вариант. Ясно, что это лучше из двух. Тогда спасибо! - person Wad; 17.02.2018

ОК, я обнаружил это сам после изучения кода, опубликованного Леном Холгейтом по адресу эта ссылка.

По сути, нам нужно хранить все объекты SOCKET (которые мы создаем для передачи указателю функции AcceptEx, полученному, как показано выше), чтобы перебирать их. Programming for Microsoft Windows — Second Edition говорит нам, что подходящее время для итерации ожидающих соединений — это когда у нас больше соединений, желающих быть принятыми, чем AcceptEx невыполненных вызовов. Мы можем определить, так ли это, следующим образом:

WSAEVENT NewEvent = CreateEvent(0, FALSE, TRUE, 0); // Auto reset event
WSAEventSelect(sockListen, NewEvent, FD_ACCEPT);

if (::WaitForSingleObject(NewEvent, INFINITE) == WAIT_OBJECT_0)
     // Need to post an AcceptEx

Обратите внимание на использование события автоматического сброса, а не ручного, созданного WSACreateEvent(). Теперь, после публикации AcceptEx, мы можем перебирать наши ожидающие сокеты, проверяя продолжительность подключения для каждого из них:

// Get the time for which this socket has been connected
::getsockopt(sock, SOL_SOCKET, SO_CONNECT_TIME, (char *)&nSeconds, &nBytes);

//
// If we decide the socket has been open for long enough, set SO_LINGER then close it
//
LINGER lingerStruct; // *
lingerStruct.l_onoff = 1;   // Leave socket open...
lingerStruct.l_linger = 0;  //...for 0 seconds after closesocket() is called

::setsockopt(sock, SOL_SOCKET, SO_LINGER, (char *)&lingerStruct, sizeof(lingerStruct));
closesocket(sock);

(*) См. здесь, почему это необходимо.

Последнее, что нужно сделать, это удалить SOCKET из любого хранилища, в котором мы его храним, когда вызов AcceptEx завершится.

person Wad    schedule 16.02.2018
comment
для чего вам нужны WSAEventSelect и объекты событий? это плохой выбор. не нужны никакие события и выбор. использовать только iocp. - person RbMm; 17.02.2018
comment
Потому что как еще я узнаю, что у меня больше клиентов, желающих подключиться, чем было совершено AcceptEx() вызовов? - person Wad; 17.02.2018
comment
вы можете определить для себя min и max сокеты прослушивания. и начните слушать с (min+max)/2 AcceptEx вызовов. когда AcceptEx закончено - вы уменьшаете количество прослушиваний. когда он стал <min на каждом новом AcceptEx завершенном, вы создаете новый сокет и вызываете AcceptEx на нем. когда клиент отключается - вы ищете количество прослушиваний - если оно <=max - вы повторно используете этот сокет и снова вызываете AcceptEx на нем. если >max - закрываешь сокет. это обычная стратегия пула - person RbMm; 19.02.2018