Прием и отправка IOCP

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

Чтобы проверить свой код, я указал браузеру на свой сервер и отправил простой http-запрос. Результаты сбивают с толку.

Например, в какой-то момент GetQueuedCompletionStatus возвращает значение, и WSARecv сообщает, что он прочитал количество байтов отправленного мной HTTP-ответа, хотя этот ответ должен (и должен) оказаться на клиенте, а recvbuffer даже не заполнен этими байтами.

Также я не понимаю, когда освобождать мои буферы после того, как другой браузер закрывает соединение, поскольку GetQueuedCompletionStatus продолжает возвращаться несколько раз после моего вызова closesocket.

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

Чтобы выявить любые неправильные представления, которые у меня могут быть о IOCP, я написал некоторый псевдокод, чтобы передать то, что, по моему мнению, делает мой код:

main {
    create server socket
    create io completion port
    while true {
        accept client socket
        create completion port for client socket
        create recv buffer and send buffer for client
        call WSARecv once with 0 bytes for whatever reason
    }
}

worker thread {
    while true {
        wait until GetQueuedCompletionStatus returns
        do something if that failed, not quite sure what (free buffers?)
        if no bytes were transferred, close socket
        try to recv data
        try to send data
    }
}

Фактический код:

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

#define BUFFER_SIZE 1024

typedef struct {
    WSAOVERLAPPED overlapped;
    SOCKET socket;
    WSABUF sendbuf;
    WSABUF recvbuf;
    char sendbuffer[BUFFER_SIZE];
    char recvbuffer[BUFFER_SIZE];
} client;

DWORD WINAPI worker_thread(HANDLE iocp){
    DWORD flags = 0, n = 0;
    ULONG unused;
    client *c;

    while (1){
        int ret = GetQueuedCompletionStatus(iocp, &n, &unused, (LPOVERLAPPED*)&c, INFINITE);
        printf("%3d triggered\n", c->socket);

        if (ret == FALSE){
            printf("%3d GetQueuedCompletionStatus error %i\n", c->socket, WSAGetLastError());
            continue;
        }

        if (c->socket == INVALID_SOCKET){
            printf("error: socket already closed\n");
            continue;
        }

        if (n == 0) {
            printf("%3d disconnected\n", c->socket);
            closesocket(c->socket);
            c->socket = INVALID_SOCKET;
            continue;
        }

        /* how do I know if there is data to read or data to write? */

        WSARecv(c->socket, &(c->recvbuf), 1, &n, &flags, &(c->overlapped), NULL);
        printf("%3d WSARecv %ld bytes\n", c->socket, n);

        WSASend(c->socket, &(c->sendbuf), 1, &n, flags, &(c->overlapped), NULL);
        printf("%3d WSASend %ld bytes\n", c->socket, n);

        /* TODO handle partial sends */
        c->sendbuf.len = 0;
    }

    return 0;
}

SOCKET make_server(int port){
    int yes = 1;
    struct sockaddr_in addr;
    SOCKET sock;

    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(port);

    sock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)yes, sizeof(yes));

    bind(sock, (struct sockaddr*)&addr, sizeof(addr));

    listen(sock, SOMAXCONN);

    return sock;
}

int main(){
    const char *text =
        "HTTP/1.0 200 OK\r\n"
        "Content-Length: 13\r\n"
        "Content-Type: text/html\r\n"
        "Connection: Close\r\n"
        "\r\n"
        "Hello, World!";

    SOCKET server_socket = make_server(8080);

    HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

    CreateThread(NULL, 0, worker_thread, iocp, 0, NULL);

    while (1){
        DWORD flags = 0, n = 0;
        client *c;
        struct sockaddr_in addr;
        int addrlen = sizeof(addr);

        SOCKET client_socket = WSAAccept(server_socket, (struct sockaddr*)&addr, &addrlen, NULL, 0);

        printf("%3d connected\n", client_socket);

        CreateIoCompletionPort((HANDLE)client_socket, iocp, 0, 0);

        c = (client*)calloc(1, sizeof(*c));

        c->socket = client_socket;
        c->sendbuf.len = strlen(text);
        c->recvbuf.len = BUFFER_SIZE;
        c->sendbuf.buf = c->sendbuffer;
        c->recvbuf.buf = c->recvbuffer;
        strcpy(c->sendbuf.buf, text);

        /* for some reason I have to receive 0 bytes once */
        WSARecv(c->socket, &(c->recvbuf), 1, &n, &flags, &(c->overlapped), NULL);
    }
}

Пример вывода:

/* Browser makes two tcp connections on socket 124 and 128. */
124 connected
128 connected

/* GetQueuedCompletionStatus returned for socket 124. */
124 triggered

/* We received the browser's http request. */
124 WSARecv 375 bytes

/* Send http response to browser. */
124 WSASend 96 bytes

/* GetQueuedCompletionStatus returned again. */
124 triggered

/* This is wrong, we should not receive our response to the browser. */
/* Also we didn't even receive data here. */
/* recvbuffer still contains the http request. */
124 WSARecv 96 bytes

/* this is ok */
124 WSASend 0 bytes
124 triggered
124 disconnected

/* Why does GetQueuedCompletionStatus still return? the socket is closed! */
/* Also how can I tell when I can safely free the buffers */
/* if GetQueuedCompletionStatus keeps returning? */
 -1 triggered
 -1 GetQueuedCompletionStatus error 1236
 -1 triggered
 -1 GetQueuedCompletionStatus error 1236

/* same again for second http request */
128 triggered
128 WSARecv 375 bytes
128 WSASend 96 bytes
128 triggered
128 WSARecv 96 bytes
128 WSASend 0 bytes
128 triggered
128 disconnected
 -1 triggered
 -1 GetQueuedCompletionStatus error 1236
 -1 triggered
 -1 GetQueuedCompletionStatus error 1236
128 connected
128 triggered
128 WSARecv 375 bytes
128 WSASend 96 bytes
128 triggered
128 WSARecv 96 bytes
128 WSASend 0 bytes
128 triggered
128 disconnected
 -1 triggered
 -1 GetQueuedCompletionStatus error 1236
 -1 triggered
 -1 GetQueuedCompletionStatus error 1236
128 connected
128 triggered
128 WSARecv 289 bytes
128 WSASend 96 bytes
128 triggered
128 WSARecv 96 bytes
128 WSASend 0 bytes
128 triggered
128 disconnected
 -1 triggered
 -1 GetQueuedCompletionStatus error 1236
 -1 triggered
 -1 GetQueuedCompletionStatus error 1236

person John Doe    schedule 20.08.2014    source источник
comment
У меня есть пример кода IOCP, который вы можете посмотреть, если хотите, см. здесь: serverframework.com/products---the-free-framework.html   -  person Len Holgate    schedule 21.08.2014
comment
@ Optimus1, извините, если ссылка не оказалась вам полезной.   -  person Len Holgate    schedule 17.05.2021


Ответы (3)


Вместо этого ваш рабочий процесс псевдокода должен выглядеть примерно так:

main {
    create server socket
    create io completion port
    create worker thread
    while not done {
        accept client socket
        associate client socket with completion port
        create recv, send, and work buffers for client
        call WSARecv with >0 bytes to start filling recv buffer
        if failed {
            close client socket and free associated buffers
        }
    }
    terminate worker thread
    close client sockets
    close server socket
}

worker thread {
    while not terminated {
        call GetQueuedCompletionStatus
        if failed {
            if failed because of IO error {
                close socket and free associated buffers
            }
            else if not timeout {
                handle error as needed
            }
        }
        else if no bytes were transferred {
            close socket and free associated buffers
        }
        else if IO was WSARecv {
            move data from recv buffer to end of work buffer
            while work buffer has a complete message {
                remove message from front of work buffer, process as needed
                if output to send {
                    if send buffer not empty {
                        append output to end of send buffer, will send later
                    }
                    else {
                        move output to send buffer
                        call WSASend
                        if failed {
                            close socket and free associated buffers
                        }
                    }
                }
            }
            call WSARecv with >0 bytes to start filling recv buffer
            if failed {
                close socket and free associated buffers
            }
        }
        else if IO was WSASend {
            remove reported number of bytes from front of send buffer
            if send buffer not empty {
                call WSASend
                if failed {
                    close socket and free associated buffers
                }
            }
        }
    }
}

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

person Remy Lebeau    schedule 20.08.2014
comment
ИМХО, ваша обработка сбоев слишком упрощена. Неудачное чтение из-за закрытия клиента не требует отключения стороны отправки, если только вы этого не хотите... Также я предполагаю, что "свободные связанные буферы" относятся только к буферам, используемым операцией, которая завершилась неудачно... - person Len Holgate; 21.08.2014
comment
Это был простой рабочий процесс для простого примера проекта. В реальном сложном проекте обработка ошибок может быть столь же сложной. Например, если входящее чтение завершилось неудачно, а исходящая отправка уже находится в процессе выполнения, вы можете немедленно прекратить чтение и освободить буфер приема, но отложить закрытие сокета и освобождение буфера отправки до тех пор, пока активная отправка не завершится/не завершится первой. - person Remy Lebeau; 21.08.2014
comment
Справедливо, я просто хотел, чтобы это было отмечено, поскольку в противном случае упрощенная обработка ошибок в простых примерах имеет тенденцию заканчиваться реальной вещью в производственном коде... - person Len Holgate; 21.08.2014

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

НО общие принципы аналогичны, так что это хорошее упражнение, чтобы увидеть, сколько вы можете извлечь из него (но что бы вы ни делали, не используйте его без серьезных изменений).
Лучшие разделы кода находятся в IOCPDlg.cpp и IOCPDlg.h.

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

С завершенными статусами в вашем рабочем потоке вы должны сделать следующее:

  • Проверьте, нет ли ошибки, используя код возврата GetQueuedCompletionStatus и GetLastError():

  • Обработайте ошибку, но отфильтруйте тайм-ауты (тайм-ауты чтения сами по себе не являются ошибками):
    if (!ReturnValue && GetErrorValue != ERROR_SEM_TIMEOUT ) { /-- i free up the socket and associated buffers here --/ continue; }

  • Процесс ввода-вывода. Я использую следующее, чтобы различать ввод-вывод (я добавлю код для него внизу):
    /-- get the base address of the struct holding lpOverlapped --/ pOverlapPlus = CONTAINING_RECORD(lpOverlapped, OVERLAPPEDPLUS, ol);

  • Если только что обработанный ввод-вывод относится к типу IORead, поставьте в очередь еще одно чтение.

  • Отправка не должна выполняться в этом потоке, другой поток/функция должен добавлять запросы отправки ввода-вывода в IOCP.

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


Код для OVERLAPPEDPLUS можно найти в статье CodeProject.

enum IOType 
{
    IOInitialize,
    IORead,
    IOWrite,
    IOIdle
};

class OVERLAPPEDPLUS 
{
public:
    OVERLAPPED ol;
    IOType ioType;

    OVERLAPPEDPLUS(IOType _ioType)
    {
        ZeroMemory(this, sizeof(OVERLAPPEDPLUS));
        ioType = _ioType;
    }
};
person Serdalis    schedule 20.08.2014
comment
Я также узнаю о iocp из кода, который вы упомянули. Не могли бы вы рассказать что-нибудь еще об ошибках и плохом поведении в этом коде? Спасибо заранее. - person maciekm; 06.09.2014
comment
Два основных, которые я помню, это то, что WAIT_TIMEOUT должно быть ERROR_SEM_TIMEOUT, отсутствие семафора записи и записи, которые будут запускаться при 0 оставшихся байтах, что можно исправить, проверив, есть ли в буфере 0 байтов, оставшихся до постановки в очередь окончательного запроса на отправку. . Помимо этих трех основных вещей, были некоторые второстепенные вещи (я не могу вспомнить), связанные с MFC, что должно быть в порядке, поскольку любые новые проекты все равно не должны использовать MFC. - person Serdalis; 08.09.2014

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

в вашем рабочем потоке:

switch (state_var)
{
  case IO_READ:
    //process wsarecv
    //process data
    break;
  case IO_SEND:
    //process wsasend
    //send more data
    //if there are no more to send
    //state_var = IO_READ;
    //call wsarecv
    break;
  //process other io command you define
}
person Florentino Tuason    schedule 15.10.2015