Как правильно закрыть сокет (с IOCP) после отправки?

У меня проблема с Winsock2, использующим IOCP (перекрывающийся режим ввода-вывода), когда мне нужно закрыть соединение после отправки запрошенных данных.

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

Я также обнаружил, что есть несколько методов отключения, таких как WSA_SendDisconnect(), которые я сейчас использую.

Сейчас текущая реализация выглядит примерно так:

  1. Отправка данных

  2. Установка флага, указывающего на намерение закрыть соединение после отправки

  3. Когда происходит событие IOCP относительно события отправки, если флаг установлен и bytecount > 0, я снова отправляю пустой буфер, просто чтобы убедиться, что все данные были отправлены

  4. Когда происходит событие IOCP относительно события отправки, если флаг установлен и bytecount == 0, я сбрасываю флаг и вызываю WSA_SendDisconnect() с пустым буфером, чтобы отправить отключить сообщение

  5. Когда происходит событие IOCP относительно события отправки без установленного флага и bytecount == 0, я вызываю closesocket() и уничтожаю контекст.

С помощью этой процедуры я мог сделать почти уверенным, что сокет отключится только после того, как все данные будут отправлены, на самом деле это работает очень хорошо, но когда я тестирую программу в некоторых очень редких случаях 10-15 раз из 1000000 тестов что-то не так . (Было бы очень сложно определить точную проблему, программа работает как веб-сервер, и я тестирую ее с помощью apachebench и siege, после теста я получаю сводку, где я вижу иногда 10-15 неудачных запросов. .)

Я был почти уверен, что ошибка связана с тем, что сокет не может отправить весь пакет до закрытия, поэтому я поставил небольшую задержку перед socketclose(), и с тех пор я получаю ноль неудачных запросов, но, конечно, это не решение, а просто способ отфильтровать то, что может вызвать проблему.

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

Что было бы лучшим решением для закрытия сокета только после того, как весь буфер отправки был фактически записан в сеть?

Ps: Пробовал играть с NoDelay и настройкой LingerState, не помогло.


person beatcoder    schedule 27.03.2019    source источник
comment
XY проблема. Сокеты TCP полностью очищаются от данных перед отправкой FIN. Ваш вопрос основан на ложной предпосылке. И отправка пустого буфера по TCP ровно ничего не делает.   -  person user207421    schedule 27.03.2019
comment
Кажется, что использование Winsock с вызовом IOCP socketclose() сразу после записи буфера отправки закрывает сокет перед очисткой. Я очень хорошо знаю, что отправка пустого пакета не только через TCP, но и через любой уровень Ethernet ничего не делает. Я просто хочу убедиться, что последовательность пакетов заканчивается пакетом длиной 0 байт, который немедленно возвращает событие успешной записи, не оказывая никакого влияния на фактический трафик, чтобы я мог закрыть сокет. Это не более чем маркер в моей программе.   -  person beatcoder    schedule 27.03.2019
comment
На самом деле отправка пустой дейтаграммы UDP работает. В любом случае все, что вам нужно сделать, это не закрываться, пока вы не получите завершение ввода-вывода для последней записи. Это не должно быть трудно организовать.   -  person user207421    schedule 27.03.2019
comment
Проблема в том, что событие завершения ввода-вывода не происходит, когда данные были отправлены (то есть буфер был очищен), но когда запись данных в буфер сокета прошла успешно. IOCP инициирует событие завершения перед отправкой данных. Даже в документации говорится, что событие завершения происходит после успешного чтения или записи буфера. Я не нашел никакого практического решения, чтобы определить, были ли данные удалены из буфера.   -  person beatcoder    schedule 27.03.2019
comment
вызовите DisconnectEx с TF_REUSE_SOCKET. когда этот запрос завершен, вы можете закрыть сокет   -  person RbMm    schedule 27.03.2019
comment
@RbMm - Спасибо, это кажется полезным, я постараюсь вскоре опубликовать результаты.   -  person beatcoder    schedule 27.03.2019
comment
конечно, вам нужно позвонить closesocket после завершения DisconnectEx. передайте ему свой iocp, он быстрее вернет ERROR_IO_PENDING, и когда io завершится после этого, можно закрыть или повторно использовать сокет   -  person RbMm    schedule 27.03.2019
comment
@beatcoder Я не нашел никакого практического решения, чтобы определить, были ли данные удалены из буфера, потому что такого сигнала не существует. Если вам это действительно нужно, вам придется напрямую контролировать сетевое оборудование, например, с помощью libpcap, чтобы увидеть, когда данные поступают на оборудование.   -  person Remy Lebeau    schedule 27.03.2019
comment
Если вы закроете после завершения ввода-вывода, он должен работать. TCP имеет данные и поставит в очередь FIN после них. Какие у вас есть доказательства того, что этого не происходит?   -  person user207421    schedule 28.03.2019
comment
@MarquisofLorne Я знаю, что должен, но это не так. Как оказалось, завершение ввода-вывода вызывает событие при успешной записи буфера, а не после успешной отправки пакета. Между окончанием записи буфера TX и между его отправкой на сетевой уровень есть время, между этими двумя действиями IOCP запускает событие завершения. Чтобы убедиться, что буфер будет сброшен в сеть, единственное, что можно сделать, это закрыть сокет. Это больше не приведет к повышению уровня, но вы убедитесь, что буфер будет отправлен, а затем будет отправлен пакет FIN. Я ответил на этот вопрос, прочитайте мое решение.   -  person beatcoder    schedule 24.08.2020


Ответы (1)


Хорошо, я могу с уверенностью сказать, что эта проблема решена после большого количества тестов, это решение отлично работает на моем веб-сервере в течение нескольких месяцев без каких-либо проблем при любых обстоятельствах. Ответы были полезными, но окончательное решение следующее:

        try
        {
            socket.Shutdown(SocketShutdown.Both);
        }
        catch (SocketException ex)
        {
            // handle exception
        }
        finally
        {
            socket?.Close();
        }

Хитрость заключается в том, чтобы инициировать отключение с помощью SocketShutdown.Both и, наконец, закрыть сокет, если он все еще существует. Иногда я замечал (я действительно не знаю почему), метод выключения автоматически удалял сокет, поэтому socket.Close() вызывал исключение NullReferenceException, но иногда нет. Я работал над этой проблемой с помощью ? после ссылки на объект сокета, поэтому, если он равен нулю, метод Close() просто не будет вызываться.

Он отлично работает в Windows, а также в Linux (debian) в ядре dotnet.

Почему работает только это? Не знаю, но при этом у меня не было неудачных запросов, потери пакетов или неправильно закрытых соединений. Просто работает.

person beatcoder    schedule 23.08.2020