与浏览器通信时出现奇怪的IOCP行为

时间:2018-12-20 12:47:38

标签: c++ sockets winapi winsock iocp

我正在编写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;
                    }

IO上下文结构:

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调用中,套接字以closesocket()关闭,但是,正如您所看到的,我在它之前发布了一个shutdown(client->Socket(), SD_SEND);调用(在FIN_CLOSE部分中)。

我知道连接有两个方面,而在服务器端将其关闭并不意味着另一端也会将其关闭。但是我需要创建一个稳定的服务器,避免出现不良连接和半开连接。例如,Web应用程序的用户可以快速按F5几次以重新加载页面(是的,有些家伙这样做:))-连接将重新打开几次,并且服务器不得由于此操作而滞后或崩溃。 >

如何处理IOCP中的“不良”事件?

1 个答案:

答案 0 :(得分:1)

您在这里有很多错误的代码。

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的完成密钥,该套接字对于套接字上的所有I / O操作都是单一的,因此绑定到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对于每个I / O操作必须唯一。

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 OVERLAPPEDstatic_cast<IO_Context*>(overlapped)

关于为什么IOCP为什么选择此事件?如何处理IOCP中的“不良”事件?

IOCP 无选择。他只是在I / O完成时发出信号。所有。绝对独立于使用IOCP或任何其他完成机制的情况,您在不同的网络操作上遇到哪些特定的wsa错误。

当错误代码为0并且在recv操作中传输了0个字节时,正常断开时的

是正常的。您需要在连接完成后永久激活recv请求,如果recv完成并传输了0个字节,则表示断开连接