【问题标题】:Strange IOCP behaviour when communicating with browsers与浏览器通信时出现奇怪的 IOCP 行为
【发布时间】:2019-05-20 23:56:13
【问题描述】:

我正在为从桌面客户端到浏览器的视频流编写 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 并将 transfered 设置为 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 中处理这种“坏”事件?

【问题讨论】:

    标签: c++ sockets winapi winsock iocp


    【解决方案1】:

    这里有很多错误的代码。

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

    这不是停止WorkerThread 的有效和错误代码。起初你做多余的电话WaitForSingleObject,使用多余的EventShutdown 和main this 无论如何都无法关闭。如果您的代码在 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 的套接字上的所有 I/O 操作都是单一的。这一切都是错误的设计。

    您需要在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(&amp;overlapped)) Sleep(1); 之类的代码总是错误的。绝对的,永远的。永远不要写这样的代码。

    ctx = (IO_Context *)overlapped; 尽管在您的具体情况下这给出了正确的结果,但如果您更改 IO_Context 的定义,这并不好并且可能会中断。如果你使用struct IO_Context{ OVERLAPPED overlapped; },你可以使用CONTAINING_RECORD(overlapped, IO_Context, overlapped),但最好使用class IO_Context : public OVERLAPPEDstatic_cast&lt;IO_Context*&gt;(overlapped)

    现在关于为什么 IOCP 选择这个事件?如何在 IOCP 中处理这种“坏”事件?

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

    当错误代码为 0 且在 recv 操作中传输 0 字节时,正常断开连接是正常的。您需要在连接完成后永久激活接收请求,如果接收完成并传输 0 个字节,这意味着发生断开连接

    【讨论】:

    • 感谢您的回复。客户端对象存储与套接字相关的信息和客户端类型,它有两个具有固定操作码的 IO_Context - 用于输入和输出。我可以为相关操作重用这个上下文吗?或者我需要为每个操作创建新的上下文(IO 上下文对于每个 I/O 操作必须是唯一的)?
    • @Iceman - 不能是多个使用相同 IO_STATUS_BLOCK (OVERLAPPED) 的 I/O。您可以在它使用的 I/O 完成后重用(为什么不呢?)IO_Context。 main 它只能用于单个 I/O。如果不想每次都从堆中分配/释放IO_Context 上下文,可以说后备并分配/释放它。
    • @Iceman - 是的,iocp 完成服务器的最佳方式,例如(github.com/rbmm/LIB/blob/master/ASIO/socket.hgithub.com/rbmm/LIB/blob/master/ASIO/port.h
    • @Iceman - 通常我们有单个 IOCP (不需要更多)和许多客户端的对象 - 所以我们不能将指向客户端的指针设置为完成键。如果使用系统通过BindIoCompletionCallback 提供的 IOCP,它将指向用户回调 (Function) 的指针设置为完成键。例如,您也可以这样做。或者,如果您使用硬编码的函数指针(由单个函数处理的所有数据包),您可以在此处使用一些标签(例如'_io_''stop' 等)。可以简单地传递0而不使用
    • 当您的 io 操作完成时,您会在 IOCP 中收到通知。除了你的(发送、接收、连接、断开)
    猜你喜欢
    • 1970-01-01
    • 2015-07-08
    • 2013-11-30
    • 1970-01-01
    • 2020-04-25
    • 2012-07-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多