Странное поведение IOCP при общении с браузерами - PullRequest
0 голосов
/ 20 декабря 2018

Я пишу IOCP-сервер для потоковой передачи видео с настольного клиента в браузер.Обе стороны используют протокол WebSocket для унификации архитектуры сервера (и потому что у браузеров нет другого способа выполнить полнодуплексный обмен).

Рабочий поток начинается следующим образом:

unsigned int __stdcall WorkerThread(void * param){
    int ThreadId = (int)param;
    OVERLAPPED *overlapped = nullptr;
    IO_Context *ctx = nullptr;
    Client *client = nullptr;
    DWORD transfered = 0;
    BOOL QCS = 0;

    while(WAIT_OBJECT_0 != WaitForSingleObject(EventShutdown, 0)){
        QCS = GetQueuedCompletionStatus(hIOCP, &transfered, (PULONG_PTR)&client, &overlapped, INFINITE);

        if(!client){
            if( Debug ) printf("No client\n");
            break;
        }
        ctx = (IO_Context *)overlapped;
        if(!QCS || (QCS && !transfered)){
            printf("Error %d\n", WSAGetLastError());
            DeleteClient(client);
            continue;
        }

        switch(auto opcode = client->ProcessCurrentEvent(ctx, transfered)){
            // Client owed to receive some data
            case OPCODE_RECV_DEBT:{ 
                if((SOCKET_ERROR == client->Recv()) && (WSA_IO_PENDING != WSAGetLastError())) DeleteClient(client);
                break;
            }
            // Client received all data or the beginning of new message
            case OPCODE_RECV_DONE:{ 
                std::string message;
                client->GetInput(message);
                // Analizing the first byte of WebSocket frame
                switch( opcode = message[0] & 0xFF ){ 
                    // HTTP_HANDSHAKE is 'G' - from GET HTTP...
                    case HTTP_HANDSHAKE:{
                        message = websocket::handshake(message);
                        while(!client->SetSend(message)) Sleep(1); // Set outgoing data
                        if((SOCKET_ERROR == client->Send()) && (WSA_IO_PENDING != WSAGetLastError())) DeleteClient(client);
                        break;
                    }
                    // Browser sent a closing frame (0x88) - performing clean WebSocket closure
                    case FIN_CLOSE:{
                        websocket::frame frame;
                        frame.parse(message);
                        frame.masked = false;
                        if( frame.pl_len == 0 ){
                            unsigned short reason = 1000;
                            frame.payload.resize(sizeof(reason));
                            frame.payload[0] = (reason >> 8) & 0xFF;
                            frame.payload[1] =  reason       & 0xFF;
                        }
                        frame.pack(message);
                        while(!client->SetSend(message)) Sleep(1);
                        if((SOCKET_ERROR == client->Send()) && (WSA_IO_PENDING != WSAGetLastError())) DeleteClient(client);
                        shutdown(client->Socket(), SD_SEND);
                        break;
                    }

Структура контекста ввода-вывода:

struct IO_Context{
    OVERLAPPED overlapped;
    WSABUF data;
    char buffer[IO_BUFFER_LENGTH];
    unsigned char opcode;
    unsigned long long debt;
    std::string message;
    IO_Context(){
        debt = 0;
        opcode = 0;
        data.buf = buffer;
        data.len = IO_BUFFER_LENGTH;
        overlapped.Offset = overlapped.OffsetHigh = 0;
        overlapped.Internal = overlapped.InternalHigh = 0;
        overlapped.Pointer = nullptr;
        overlapped.hEvent = nullptr;
    }
    ~IO_Context(){ while(!HasOverlappedIoCompleted(&overlapped)) Sleep(1); }
};

Функция отправки клиента:

int Client::Send(){
    int var_buf = O.message.size();
    // "O" is IO_Context for Output
    O.data.len = (var_buf>IO_BUFFER_LENGTH)?IO_BUFFER_LENGTH:var_buf;
    var_buf = O.data.len;
    while(var_buf > 0) O.data.buf[var_buf] = O.message[--var_buf];
    O.message.erase(0, O.data.len);
    return WSASend(connection, &O.data, 1, nullptr, 0, &O.overlapped, nullptr);
}

Когда клиент рабочего стола отключается (для этого используется только closesocket (), без shutdown ()), GetQueuedCompletionStatus возвращаетTRUE и устанавливает значение 0 - в этом случае WSAGetLastError () возвращает 64 (указанное сетевое имя больше не доступно), и это имеет смысл - клиент отключен (строка с if(!QCS || (QCS && !transfered))).Но когда браузер отключается, коды ошибок меня смущают ... Это могут быть 0, 997 (ожидающая операция), 87 (недопустимый параметр) ... и никаких кодов, связанных с окончанием соединения.

ПочемуIOCP выбрать это событие?Как он может выбрать ожидающую операцию?Почему ошибка равна 0 при передаче 0 байт?Также это приводит к бесконечным попыткам удаления объекта, связанного с перекрытой структурой, потому что деструктор вызывает ~IO_Context(){ while(!HasOverlappedIoCompleted(&overlapped)) Sleep(1); } для безопасного удаления.В DeleteClient call сокет закрывается с closesocket(), но, как вы можете видеть, я отправляю вызов shutdown(client->Socket(), SD_SEND); перед ним (в разделе FIN_CLOSE).

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

Как справиться с этими «плохими» событиями в IOCP?

1 Ответ

0 голосов
/ 23 декабря 2018

у вас много неправильного кода здесь.

while(WAIT_OBJECT_0 != WaitForSingleObject(EventShutdown, 0)){
    QCS = GetQueuedCompletionStatus(hIOCP, &transfered, (PULONG_PTR)&client, &overlapped, INFINITE);

это не эффективный и неправильный код для остановки WorkerThread.сначала вы делаете избыточный вызов WaitForSingleObject, используете избыточный EventShutdown и включаете его в любом случае, чтобы не завершить работу.если ваш код ожидает пакета внутри GetQueuedCompletionStatus, который вы говорите EventShutdown - не прерывайте GetQueuedCompletionStatus вызов - вы продолжаете бесконечное ожидание здесь.Правильный способ отключения - PostQueuedCompletionStatus(hIOCP, 0, 0, 0) вместо вызова SetEvent(EventShutdown) и если сработал вид потока client == 0 - он разрывает цикл.и обычно вам нужно иметь несколько WorkerThread (не один).и множественные вызовы PostQueuedCompletionStatus(hIOCP, 0, 0, 0) - точное количество рабочих потоков.также вам нужно синхронизировать эти вызовы с io - делайте это только после того, как все io уже завершено и новые io-пакеты не будут поставлены в очередь в iocp.поэтому «нулевые пакеты» должны быть последними в очереди на порт

if(!QCS || (QCS && !transfered)){
            printf("Error %d\n", WSAGetLastError());
            DeleteClient(client);
            continue;
        }

, если !QCS - значение в client не инициализировано, вы просто не можете его использовать, и вызов DeleteClient(client); неверенусловие

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

вам нужно сохранить указатель на клиента в IO_Context.и добавьте ссылку на клиента в IO_Context и освободите клиента в деструкторе IO_Context.

class IO_Context : public OVERLAPPED {
    Client *client;
    ULONG opcode;
    // ...

public:
    IO_Context(Client *client, ULONG opcode) : client(client), opcode(opcode) {
        client->AddRef();
    }

    ~IO_Context() {
        client->Release();
    }

    void OnIoComplete(ULONG transfered) {
        OnIoComplete(RtlNtStatusToDosError(Internal), transfered);
    }

    void OnIoComplete(ULONG error, ULONG transfered) {
        client->OnIoComplete(opcode, error, transfered);
        delete this;
    }

    void CheckIoError(ULONG error) {
        switch(error) {
            case NOERROR:
            case ERROR_IO_PENDING:
                break;
            default:
                OnIoComplete(error, 0);
        }
    }
};

тогда у вас есть один IO_Context?если да, то это фатальная ошибка.IO_Context должен быть уникальным для каждой операции ввода / вывода.

if (IO_Context* ctx = new IO_Context(client, op))
{
    ctx->CheckIoError(WSAxxx(ctx) == 0 ? NOERROR : WSAGetLastError());
}

и из обработанного потока s

ULONG WINAPI WorkerThread(void * param)
{
    ULONG_PTR key;
    OVERLAPPED *overlapped;
    ULONG transfered;
    while(GetQueuedCompletionStatus(hIOCP, &transfered, &key, &overlapped, INFINITE)) {
        switch (key){
        case '_io_':
            static_cast<IO_Context*>(overlapped)->OnIoComplete(transfered);
            continue;
        case 'stop':
            // ...
            return 0;
        default: __debugbreak();
        }
    }

    __debugbreak();
    return GetLastError();
}

код типа while(!HasOverlappedIoCompleted(&overlapped)) Sleep(1);всегда неправильно.абсолютный и всегда.никогда не пишите такой код.

ctx = (IO_Context *)overlapped;, несмотря на то, что в вашем конкретном случае это дает правильный результат, что не очень хорошо и может быть нарушено, если вы измените определение IO_Context.вы можете использовать CONTAINING_RECORD(overlapped, IO_Context, overlapped), если вы используете struct IO_Context{ OVERLAPPED overlapped; }, но лучше использовать class IO_Context : public OVERLAPPED и static_cast<IO_Context*>(overlapped)

, теперь о Почему IOCP выбирает эти события?Как справиться с этими «плохими» событиями в IOCP?

В IOCP ничего не выбрать .он просто сигнализирует о завершении ввода / вывода.все.какие конкретные ошибки wsa вы получили в различных сетевых операциях, абсолютно независимо от использования IOCP или любого другого механизма завершения.

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

...