Как определить время ожидания WinSock TCP с помощью BindIoCompletionCallback

Я создаю TCP-сервер Visual C++ WinSock с использованием BindIoCompletionCallback, он отлично работает при получении и отправке данных, но не могу найти хороший способ определения времени ожидания: SetSockOpt/SO_RCVTIMEO/SO_SNDTIMEO не влияет на неблокирующие сокеты, если одноранговый узел не является отправляя какие-либо данные, CompletionRoutine вообще не вызывается.

Я думаю об использовании RegisterWaitForSingleObject с полем hEvent в OVERLAPPED, это может сработать, но тогда CompletionRoutine вообще не нужен, я все еще использую IOCP? есть ли проблемы с производительностью, если я использую только RegisterWaitForSingleObject и не использую BindIoCompletionCallback?

Обновление: Пример кода:

Моя первая попытка:

    bool CServer::Startup() {
        SOCKET ServerSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
        WSAEVENT ServerEvent = WSACreateEvent();
        WSAEventSelect(ServerSocket, ServerEvent, FD_ACCEPT);
        ......
        bind(ServerSocket......);
        listen(ServerSocket......);
        _beginthread(ListeningThread, 128 * 1024, (void*) this);
        ......
        ......
    }

    void __cdecl CServer::ListeningThread( void* param ) // static
    {
        CServer* server = (CServer*) param;
        while (true) {
            if (WSAWaitForMultipleEvents(1, &server->ServerEvent, FALSE, 100, FALSE) == WSA_WAIT_EVENT_0) {
                WSANETWORKEVENTS events = {};
                if (WSAEnumNetworkEvents(server->ServerSocket, server->ServerEvent, &events) != SOCKET_ERROR) {
                    if ((events.lNetworkEvents & FD_ACCEPT) && (events.iErrorCode[FD_ACCEPT_BIT] == 0)) {
                        SOCKET socket = accept(server->ServerSocket, NULL, NULL);
                        if (socket != SOCKET_ERROR) {
                            BindIoCompletionCallback((HANDLE) socket, CompletionRoutine, 0);
                            ......
                        }
                    }
                }
            }
        }
    }

    VOID CALLBACK CServer::CompletionRoutine( __in DWORD dwErrorCode, __in DWORD dwNumberOfBytesTransfered, __in LPOVERLAPPED lpOverlapped ) // static
    {
        ......
        BOOL res = GetOverlappedResult(......, TRUE);
        ......
    }

    class CIoOperation {
    public:
        OVERLAPPED Overlapped;
        ......
        ......
    };

    bool CServer::Receive(SOCKET socket, PBYTE buffer, DWORD length, void* context)
    {
        if (connection != NULL) {
            CIoOperation* io = new CIoOperation();
            WSABUF buf = {length, (PCHAR) buffer}; 
            DWORD flags = 0;
            if ((WSARecv(Socket, &buf, 1, NULL, &flags, &io->Overlapped, NULL) != 0) && (GetLastError() != WSA_IO_PENDING)) {
                delete io;
                return false;
            } else return true;
        }
        return false;
    }

Как я уже сказал, он работает нормально, если клиент фактически отправляет мне данные, "получение" не блокируется, вызов CompletionRoutine вызван, данные получены, но вот один прием, если клиент не отправляет мне данные, как можно Я сдаюсь после перерыва?

Так как SetSockOpt / SO_RCVTIMEO / SO_SNDTIMEO здесь не поможет, я думаю, что я должен использовать поле hEvent в структуре OVERLAPPED, которая будет сигнализироваться после завершения ввода-вывода, но WaitForSingleObject / WSAWaitForMultipleEvents, которая будет блокировать вызов Receive, и я хочу всегда возвращайтесь немедленно, поэтому я использовал RegisterWaitForSingleObject и WAITORTIMERCALLBACK. это сработало, обратный вызов был вызван после истечения времени ожидания, или IO завершился, но теперь у меня есть два обратных вызова для любой отдельной операции IO, CompletionRoutine и WaitOrTimerCallback:

если IO завершен, они будут вызваны одновременно, если IO не завершен, будет вызван WaitOrTimerCallback, затем я вызываю CancelIoEx, это вызвало вызов CompletionRoutine с некоторой ошибкой ABORTED, но здесь условие гонки, возможно IO будет завершено прямо перед тем, как я отменю это, тогда... бла, все в целом довольно сложно.

Затем я понял, что мне вообще не нужны BindIoCompletionCallback и CompletionRoutine, и я делаю все из WaitOrTimerCallback, это может сработать, но вот интересный вопрос, в первую очередь я хотел построить Winsock-сервер на основе IOCP и подумал, что BindIoCompletionCallback Самый простой способ сделать это, используя пул потоков, предоставляемый самой Windows, теперь я в конечном итоге с сервером без кода IOCP вообще? это все еще IOCP? или я должен забыть BindIoCompletionCallback и построить свою собственную реализацию пула потоков IOCP? Зачем?

2 ответа

Что я сделал, так это заставил уведомления о таймауте / завершении вводить критическую секцию в объекте сокета. После этого победитель может установить переменную состояния сокета и выполнить свое действие, каким бы оно ни было. Если сначала завершается ввод-вывод, буферный массив ввода-вывода обрабатывается обычным образом, и любое время ожидания направляется на перезапуск конечным автоматом. Точно так же, если время ожидания наступает первым, ввод / вывод получает CancelIOEx'd и любое последующее уведомление о завершении в очереди сбрасывается механизмом состояний. Из-за этих возможных "поздних" уведомлений я помещаю освобожденные сокеты в очередь тайм-аута и перерабатываю их в пул объектов сокетов только через пять минут, аналогично тому, как сам стек TCP помещает свои сокеты в "TIME_WAIT".

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

На сервере использовалось всего несколько таймаутов, поэтому я использовал очереди, исправленные во время компиляции. Было бы довольно легко добавить новые очереди или изменить время ожидания, отправив соответствующие "командные" сообщения в очередь ввода потока, смешанную с новыми сокетами, но я не зашел так далеко.

По истечении времени ожидания поток вызвал в объекте событие, которое, в случае сокета, вошло бы в конечный автомат, защищенный CS объекта сокета (это был класс TimeoutObject, из которого, среди прочего, произошел сокет).

Больше:

Я жду семафор, который контролирует очередь ввода потока времени ожидания. Если он сигнализируется, я получаю новый объект TimeoutObject из входной очереди и добавляю его в конец любой ожидаемой очереди ожидания. Если время ожидания семафора истекло, я проверяю элементы в заголовках очередей FIFO времени ожидания и пересчитываю их оставшийся интервал, вычитая текущее время из времени ожидания. Если интервал равен 0 или отрицателен, вызывается событие тайм-аута. Перебирая очереди и их заголовки, я сохраняю локальный минимальный оставшийся интервал до следующего таймаута. Поскольку все элементы заголовка во всех очередях имеют ненулевой оставшийся интервал, я возвращаюсь к ожиданию семафора очереди, используя минимальный оставшийся интервал, который я накопил.

Вызов события возвращает перечисление. Это перечисление указывает потоку тайм-аута, как обрабатывать объект, событие которого только что было запущено. Один из вариантов - перезапустить тайм-аут, пересчитав тайм-аут и в конце поместив объект обратно в свою очередь тайм-аута.

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

Это и потому, что IIRC имеет ограничение в 64 дескриптора, как WaitForMultipleObjects(). Мой сервер имел более 23000 клиентов, тайм-аут. Я обнаружил, что один поток тайм-аута и несколько очередей FIFO являются более гибкими - для него может быть тайм-аут любого старого объекта до тех пор, пока он произошел от TimeoutObject - никаких дополнительных вызовов / дескрипторов ОС не требуется.

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

Рекомендуемый способ проверить устаревшие соединения - позвонить getsockopt с SO_CONNECT_TIME вариант. Возвращает количество секунд, в течение которых сокет был подключен. Я знаю, что это операция опроса, но если вы хорошо разбираетесь в том, как и когда вы запрашиваете это значение, на самом деле это довольно хороший механизм для управления соединениями. Ниже я объясню, как это делается.

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

Поток принятия контролирует мое отставание сокета через WSAEventSelect и FD_ACCEPT параметр. Это означает, что поток принятия выполняется только тогда, когда Windows определяет, что существуют входящие подключения, требующие принятия. В это время я перечисляю мои принятые сокеты и запрос SO_CONNECT_TIME снова для каждой розетки. Я вычитаю из этого значения временную метку последнего завершения ввода-вывода соединения, и, если разница превышает указанное пороговое значение, мой код считает соединение истекшим.

Другие вопросы по тегам