Сокеты, использующие GetQueuedCompletionStatus и ERROR_MORE_DATA

Я пытаюсь использовать GetQueuedCompletionStatus с winsocks, но, похоже, не понимаю. Порядок действий следующий:

void foo() {
    ...
    SOCKET sck = WSASocket(AF_INET, SOCK_DGRAM, 0, 
        NULL, 0, WSA_FLAG_OVERLAPPED);
    ....
    bind(sck,(struct sockaddr *)&addr,sizeof(struct sockaddr_in));
    HANDLE hPort = CreateIoCompletionPort((HANDLE)sck, NULL, 0, 0 );
    OVERLAPPED pOverlapped = {0,};
    WSARecvFrom(sck,NULL,0,NULL,NULL,(struct sockaddr *)&laddr,&lsize,&pOverlapped,0);
    BOOL bReturn = GetQueuedCompletionStatus(
            hPort,
            &rbytes,
            (LPDWORD)&lpContext,
            &pOutOverlapped,
            INFINITE);
    ...
}

Затем я отправляю некоторые сетевые данные на связанный порт из внешнего инструмента. GetQueuedCompletionStatus возвращает FALSE, а GetLastError () возвращает ERROR_MORE_DATA, что звучит правильно, поскольку я не предоставил буфер в WSARecvFrom.

Вопрос в том, как я могу предоставить буфер для фактического получения данных из неудачной операции ввода-вывода?

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

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

Итак, как я могу правильно обработать ERROR_MORE_DATA, не теряя данные с первой операции?


person Andre    schedule 07.08.2015    source источник
comment
Почему вы не предоставляете WSABUF buffer / bufferArray с вызовом? Зачем провоцировать ненадежный протокол дейтаграмм сомнительными аргументами?   -  person Martin James    schedule 07.08.2015
comment
Я пытаюсь сделать так, чтобы Windows сообщала мне, когда есть данные, прежде чем их читать.   -  person Andre    schedule 07.08.2015
comment
Функции чтения ввода-вывода не работают. Вы должны предоставить действующий буфер для заполнения функции, независимо от того, является ли чтение синхронным или асинхронным. Чтобы узнать, когда данные доступны в сокете, не читая их, вам нужно использовать select(), WSAAsyncSelect() или WSAEventSelect(), чтобы определить, когда сокет доступен для чтения (есть ожидающие данные), ЗАТЕМ вы можете читать из него, используя буфер. В вашем примере вы пытаетесь использовать IOCP, который выполняет фактическое чтение в фоновом режиме, а затем уведомляет вас, когда чтение завершено, поэтому вам нужен настоящий буфер для чтения.   -  person Remy Lebeau    schedule 07.08.2015


Ответы (1)


Вы должны предоставить буфер для WSARecvFrom(), как и при любой операции чтения, независимо от того, используете ли вы IOCP или нет. Вы должны убедиться, что буфер остается действительным в памяти до завершения операции IOCP. IOCP заполняет предоставленный вами буфер, а затем уведомляет порт завершения о завершении.

UDP не может передавать более 65535 байтов в одной дейтаграмме, поэтому вы можете использовать это в качестве максимального размера буфера.

В вашем примере ваш код написан для синхронного запуска (что полностью исключает цель использования IOCP), поэтому вы можете использовать локальный буфер:

void foo() {
    ...
    SOCKET sck = WSASocket(AF_INET, SOCK_DGRAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (sck == INVALID_SOCKET)
    {
        // error, do something...
        return;
    }
    ....
    bind(sck,(struct sockaddr *)&addr, sizeof(struct sockaddr_in));
    HANDLE hPort = CreateIoCompletionPort((HANDLE)sck, NULL, 0, 0 );
    if (!hPort)
    {
        // error, do something...
        return;
    }

    WSAOVERLAPPED Overlapped = {0};
    Overlapped.hEvent = WSACreateEvent();

    BYTE buffer[0xFFFF];
    DWORD dwBytesRecvd = 0;
    DWORD dwFlags = 0;
    sockaddr_in fromaddr = {0};
    int fromaddrlen = sizeof(fromaddr);

    WSABUF buf;
    buf.len = sizeof(buffer);
    buf.buf = buffer;

    int iRet = WSARecvFrom(sck, &buf, 1, &dwBytesRecvd, &dwFlags, (sockaddr*)&fromaddr, &fromaddrlen, &Overlapped, NULL);
    if (iRet == SOCKET_ERROR)
    {
        if (WSAGetLastError() != WSA_IO_PENDING)
        {
           // error, do something...
           return;
        }

        DWORD rBytes;
        ULONG_PTR key;
        LPOVERLAPPED pOverlapped = NULL;

        if (!GetQueuedCompletionStatus(hPort, &rbytes, &key, &pOverlapped, INFINITE))
        {
            if (pOverlapped)
            {
                // WSARecvFrom() failed...
            }
            else
            {
                // GetQueuedCompletionStatus() failed...
            }

            // do something...
            return;
        }
    }

    // I/O complete, use buffer, dwBytesRecvd, dwFlags, and fromaddr as needed...
}

Однако это противоречит цели IOCP. Если вы действительно хотите быть синхронным, вы можете просто использовать recvfrom() и позволить ему блокировать вызывающий поток до тех пор, пока не будут получены данные. IOCP работает лучше всего, когда у вас есть пул потоков, обслуживающих порт завершения. Позвоните WSARecvFrom() и дайте ему поработать в фоновом режиме, не ждите. Пусть отдельный поток вызывает GetQueuedCompletionPort() и обрабатывает данные при их получении, например:

struct MyOverlapped
{
    WSAOVERLAPPED overlapped;
    BYTE buffer[0xFFFF];
    DWORD buflen;
    DWORD flags;
    sockaddr_storage fromaddr;
    int fromaddrLen;
};

HANDLE hPort = NULL;

void foo() {
    ...
    SOCKET sck = WSASocket(AF_INET, SOCK_DGRAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (sck == INVALID_SOCKET)
    {
        // error, do something...
        return;
    }
    ....
    bind(sck,(struct sockaddr *)&addr, sizeof(struct sockaddr_in));
    hPort = CreateIoCompletionPort((HANDLE)sck, NULL, 0, 0 );
    if (!hPort)
    {
        // error, do something...
        return;
    }

    MyOverlapped *ov = new MyOverlapped;
    ZeroMemory(ov, sizeof(*ov));
    ov->overlapped.hEvent = WSACreateEvent();
    ov->fromaddrlen = sizeof(ov->fromaddr);

    WSABUF buf;
    buf.len = sizeof(ov->buffer);
    buf.buf = ov->buffer;

    int iRet = WSARecvFrom(sck, &buf, 1, &ov->buflen, &ov->flags, (sockaddr*)&ov->fromaddr, &ov->fromaddrlen, (WSAOVERLAPPED*)ov, NULL);
    if (iRet == SOCKET_ERROR)
    {
        if (WSAGetLastError() != WSA_IO_PENDING)
        {
           // error, do something...
           return;
        }

        // WSARecvFrom() is now operating in the background,
        // the IOCP port will be signaled when finished...
    }
    else
    {
        // data is already available,
        // the IOCP port will be signaled immediately...
    }

    ...
}

...

// in another thread...

{
    ...

    DWORD rbytes;
    ULONG_PTR key;
    MyOverlapped *ov = NULL;

    if (!GetQueuedCompletionStatus(hPort, &rbytes, &key, (LPOVERLAPPED*)&ov, INFINITE))
    {
        if (ov)
        {
            // WSARecvFrom() failed...
            // free ov, or reuse it for another operation...
        }
        else
        {
            // GetQueuedCompletionStatus() failed...
        }
    }
    else
    {
        // use ov as needed...
        // free ov, or reuse it for another operation...
    }

    ...
}
person Remy Lebeau    schedule 07.08.2015
comment
Итак, вы говорите, что ERROR_MORE_DATA в IOCP не имеет значения (по крайней мере, в случае сокетов). В том случае, если входной буфер был слишком мал для операции, данные просто отбрасываются? Разве нет абсолютно никакой возможности повторить операцию с новым буфером? Можете ли вы указать мне на любую документацию Microsoft, описывающую это поведение? - person Andre; 07.08.2015
comment
ERROR_MORE_DATA означает, что предоставленный вами буфер меньше полученных данных. Это правда, потому что вы вообще не предоставили никакого буфера (и в UDP существует концепция датаграмм нулевой длины, поэтому буфер нулевой длины допустим, поэтому WSARecvFrom() не дает сбоев заранее). Для UDP датаграмма отбрасывается, если буфер слишком мал. Невозможно восстановить его или повторить попытку чтения той же дейтаграммы с большим буфером. Вы должны сделать это правильно с первого раза, иначе потеряете навсегда. - person Remy Lebeau; 07.08.2015
comment
Поскольку максимальный размер буфера UDP составляет всего 65535 байт на дейтаграмму, что очень мало для выделения, выделите максимальный размер буфера для каждого чтения UDP. Завершение сообщит вам, сколько байтов было фактически получено. - person Remy Lebeau; 07.08.2015
comment
Я немного покопался и вы правы, данные UDP будут отброшены и их нельзя будет восстановить, облом. Что касается распределения, если бы у меня было 1000 сокетов, ожидающих данных, мне нужно было бы выделить 1000 буферов по 64 КБ, так что это не очень элегантное решение. Приму твой ответ, спасибо. - person Andre; 07.08.2015
comment
@Andre: вам нужен отдельный буфер для каждой операции чтения / записи. Когда одна операция завершена, вы можете повторно использовать этот буфер для другой операции, если это необходимо. Но да, если у вас есть 1000 сокетов, которые читают одновременно, вам понадобится 1000 отдельных буферов для чтения. Если вы не хотите фрагментировать системную память, подумайте об использовании выделенной кучи или пула памяти для ваших буферов. В противном случае используйте _1 _ / _ 2_, чтобы определять, когда данные ожидают, и выделять + читать только по мере необходимости. - person Remy Lebeau; 07.08.2015
comment
@Andre: разве вам не нужен только 1 сокет для любого количества клиентов UDP для одного номера порта? Только если вы открыли открытый сокет привязкой к 1000 UDP-портам - person orcy; 28.02.2017
comment
UDP-пакеты @RemyLebeau не теряются, если буфер слишком мал. См. Блог . grijjy.com/2018/08/29/ Модель ввода-вывода Windows. Должен быть указан флаг MSG_PEEK. - person a man in love; 24.05.2019