【问题标题】:IIS Native Module - WebsocketIIS 本机模块 - Websocket
【发布时间】:2022-11-17 07:32:57
【问题描述】:

我希望通过 IIS 中的本机模块实现本机 websocket 处理程序。我发现围绕它的文档非常模糊并且缺少很多细节。

我已经创建了一个 IIS Native 模块 DLL,它正在运行。我可以保持简单,只返回一个带有 HTTP/200 响应的 hello world html 文件,一切都很好。

但是,我在尝试让它处理 websocket 连接时遇到了问题。微软的 IIS 博客网站 here 上有一篇博文描述了如何处理 websocket 连接。我遵循它并进行了测试,是的,连接是从网络浏览器打开的,但是我无法从本机模块中的套接字读取数据,并且连接经常在客户端出现错误关闭 - 在随机看起来的时间。

该模块的 OnBeginRequest 方法是:

REQUEST_NOTIFICATION_STATUS CKIIS::OnBeginRequest(IN IHttpContext* pHttpContext, IN IHttpEventProvider* pProvider) {
    UNREFERENCED_PARAMETER(pProvider);

    HRESULT hr;
    
    // I've only placed this here so I can attach a debugger.
    std::this_thread::sleep_for(std::chrono::seconds(10));

    this->_context = pHttpContext;

    IHttpResponse* pHttpResponse = pHttpContext->GetResponse();

    if (pHttpResponse != NULL)
    {
        pHttpResponse->Clear();

        pHttpResponse->SetStatus(101, "Switching Protocols");
        pHttpResponse->SetHeader(
            HttpHeaderUpgrade, "websocket",
            (USHORT)strlen("websocket"), TRUE);
        pHttpResponse->SetHeader(
            HttpHeaderConnection, "Upgrade",
            (USHORT)strlen("Upgrade"), TRUE);
        DWORD cbSent = 0;
        BOOL fCompletionExpected = false;
        hr = pHttpResponse->Flush(false, true, &cbSent, &fCompletionExpected);

        std::this_thread::sleep_for(std::chrono::seconds(10));

        IHttpContext3* pHttpContext3;
        HttpGetExtendedInterface(this->_server, pHttpContext, &pHttpContext3);

        IWebSocketContext* cts = (IWebSocketContext*)pHttpContext3->GetNamedContextContainer()->GetNamedContext(L"websockets");
    
        char buffer[1024 * 100];
        DWORD sz = 1024 * 100;
        BOOL utf;
        BOOL finalfrag;
        BOOL conclose;
        DWORD clxc = 78;
        BOOL expected;

        // This method call returns E_NOTIMPL.  
        // The documentation does not even indicate this is an expected return of this.
        HRESULT res = cts->ReadFragment(
            &buffer,
            &sz,
            false,
            &utf,
            &finalfrag,
            &conclose,
            Compl,
            &clxc,
            &expected);

        // Start a thread to read/write from the websocket.
        this->_runner = thread(&CKIIS::RunWork, this);

        // Tell IIS to keep the connection pending...
        return RQ_NOTIFICATION_PENDING;
    }

    // Return processing to the pipeline.
    return RQ_NOTIFICATION_CONTINUE;
}

void CKIIS::RunWork() {

    IHttpContext3* pHttpContext3;
    HttpGetExtendedInterface(this->_server, this->_context, &pHttpContext3);

    IWebSocketContext* cts = (IWebSocketContext*)pHttpContext3->GetNamedContextContainer()->GetNamedContext(L"websockets");
    
    for (;;) {
        // Loop to read/write the socket... 
        
        // If I call cts->ReadFragment() or cts->WriteFragment() here
        // the method will return E_NOTIMPL too.

        /// Eventually break out of the loop.
    }

    try {
        //this->_context->IndicateCompletion(RQ_NOTIFICATION_FINISH_REQUEST);
        this->_context->PostCompletion(0);
    }
    catch(const std::exception& e){
        const char* barf = e.what();
        std::cout << e.what();
    }
}

几个问题:

  • 为什么 ReadFragment/WriteFragment 返回 E_NOTIMPL。在 OnBeginRequest 方法中,或者如果我尝试在它自己的线程中执行它。
  • 这里使用新线程来处理对 websocket 的读/写是首选模式吗?像轮询数据这样的繁忙循环似乎很浪费,但是当客户端提供数据时,我看不到任何注册通知的方法

【问题讨论】:

  • 你在说什么?我正在尝试利用IIS 的 websocket 模块,不是克隆。该接口完全针对我的用例公开。

标签: c++ winapi websocket iis-10


【解决方案1】:

我现在有这个工作,你会想要做一些事情:

  • 关于您轮询发送/接收问题,我发现轮询接收时输入 Sleep(10) 会导致 IIS 使 w3wp.exe 进程暂停,使用 Sleep(100) 轮询可以缓解此问题.当我需要使用 while 循环轮询其他一些线程时,我试着养成使用 Sleep() 的习惯。
  • Read/WriteFragment 正在给你 E_NOTIMPL 因为它们显然没有实现,我尝试了它们并且我得到了 E_NOTIMPL 或者它只是挂起 w3wp.exe 进程。我不知道你从哪里得到DWORD cxlc = 78,因为预期的完成是void*,它指向一些IHttpCompletionInfo*,显然......我所做的是通过阅读RFC 6455实现我自己的websocket消息传递。我将把它留给你作为练习,因为我的粗略实现仍然需要一些改进,例如适当地分块消息,同时考虑用于有效负载和密钥大小的标头字节。
  • 您对初始握手的响应需要包含带有正确响应密钥的 Sec-WebSocket-Accept 标头,该密钥是密钥的 Base64 编码 SHA1 散列作为字符串,在散列之前将 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 连接到该字符串的末尾和编码,参见RFC 6455
  • 我选择使用 OnExecuteRequestHandler 而不是 OnBeginRequest 并制作一个文件格式,例如*.chatsocket 升级到 websocket。理想情况下,这将使我能够将控制台程序的 stdin/stdout/stderr 重定向到 websocket,而无需为每个程序编写单独的 TCP 服务器,因为它可以简单地驻留在 IIS 服务器上。
  • 但是,如果您返回 OnExecuteRequestHandler 并返回 RQ_NOTIFICATION_PENDING,这将挂起直到您终止 w3wp.exe 进程,所以我所做的是实现自定义通知并在每次迭代时运行 pHttpContext-&gt;NotifyCustomNotification(m_pCustomProvider, &amp;expected)。无论出于何种原因,IIS 在 RQ_NOTIFICATION_PENDINGOnExecuteRequestHandler 上挂起,但在 OnCustomRequestNotification 上挂起。
  • 确保在初始握手和响应Flush操作后启用IHttpContext3::EnableFullDuplex()
  • 然后继续阅读但不要依赖request-&gt;GetRemainingBytes(),因为它会继续返回 0。

我改为做这样的事情:

asyncReadLock.lock();
if (!isInAsyncRead) {
    isInAsyncRead = true;
    DWORD bytesReceived = 0;
    BOOL completionPending = false;
    HRESULT result = (*request)->ReadEntityBody(
        this->receiveBuffer,
        this->receiveBufferSize,
        true,
        &bytesReceived,
        &completionPending
    );
}
asyncReadLock.unlock();

运行 ReadEntityBody 但将 fAsync 指定为 true,并像这样处理异步调用:

REQUEST_NOTIFICATION_STATUS OnAsyncCompletion(
    IN IHttpContext* pHttpContext,
    IN DWORD dwNotification,
    IN BOOL fPostNotification,
    IN OUT IHttpEventProvider* pProvider,
    IN IHttpCompletionInfo* pCompletionInfo
)
{
    currentModule.asyncReadLock.lock();
    DWORD completedBytes = pCompletionInfo->GetCompletionBytes();
    HRESULT status = pCompletionInfo->GetCompletionStatus();
    if (completedBytes > 0) {
        for (int i = 0; i < (int)completedBytes; i++) {
            this->currentModule.ReceiveQueue.push_back(this->currentModule.receiveBuffer[i]);
        }
    }
    this->currentModule.isInAsyncRead = false;
    currentModule.asyncReadLock.unlock();
    return RQ_NOTIFICATION_PENDING;
}

我保留了一个 std::deque&lt;unsigned char&gt; ReceiveQueue,它保留了通过 ReadEntityBody 传入的任何字节,在我的主循环中,我将传递这些字节以根据需要创建 WebSocketMessages,而不是使用 ReadFragment

总而言之,我现在有一个用于 IIS 的文件处理程序,格式如 *.chatext*.chatsocket 将其转换为 IIS 中的 websocket 作为本机模块,而无需运行单独的 TCP 服务器,因此我获得了 IIS 的好处HTTPS 等功能无需通过 ARR 或 URL 重写,我让事情按照我最初期望的方式工作。我使用 ReadEntityBody 而不是 ReadFragment 在异步读取握手后继续读取传入数据。我在每次轮询时使用带有 Sleep(100) 的读取轮询,我还实现了我自己的超时设置秒数以确保 IIS 正确关闭连接。

如果您还有其他问题,请告诉我。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-12
    • 2018-06-01
    • 1970-01-01
    相关资源
    最近更新 更多