Как определить время ожидания WinSock TCP с помощью BindIoCompletionCallback - PullRequest
4 голосов
/ 31 декабря 2011

Я создаю 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 ]

0 голосов
/ 11 января 2012

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

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

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

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

0 голосов
/ 04 января 2012

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

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

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

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

Подробнее:

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

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

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

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

...