PAYATHON 2026

如何使用 winhttp 检测 websocket 连接是否已断开?

排障赵师傅

客户端需要检查 WinHTTP 的收发结果,为业务请求设置截止时间,并把网络等待移出 wxWidgets 主线程。连接出错、收到 Close 帧或等待响应超时后,应停止使用当前连接并禁用发送,等重新连接成功后再恢复。

keepalive 用于探测空闲连接是否仍然可用,无法恢复已经断开的连接,也不负责自动重连。业务响应是否按时返回,需要单独判断。

为什么发送成功后,接收仍然可能一直等待?

WebSocket 建立在 TCP 之上。服务器正常关闭、进程突然退出或网络中断时,客户端观察到的情况各不相同。

正常关闭时,对端可以发送 WebSocket Close 帧;异常退出时,底层连接可能报告错误。如果网络直接丢弃数据,客户端可能暂时收不到任何通知,只能等待网络错误,或通过超时判定连接不可用。

因此,Send() 成功不能证明服务器仍能处理请求,也不能保证下一次 Receive() 一定收到业务响应。即使增加 IsConnected(),连接仍可能在检查通过后立即中断,实际收发时还是要处理失败。

下面的调用还可能阻塞界面:

std::string response = client.Receive();

如果它在 wxWidgets 主线程中阻塞,窗口就无法继续处理事件。json::parse() 的异常处理要等接收返回后才会执行,解决不了接收阶段的卡死。

先检查接收错误和 Close 帧

WinHttpWebSocketSend()、WinHttpWebSocketReceive() 返回的是 DWORD 错误码:NO_ERROR 表示成功,其他值表示失败。不要按返回 BOOL 的函数来判断;排查问题时,应记录函数直接返回的错误码。WinHttpWebSocketSend 文档、WinHttpWebSocketReceive 文档

同步接收完成后,可以这样判断结果。OnReceiveError()、OnPeerClosed() 和 OnReceivedData() 需要自行实现:

// 同步模式示意:只展示结果判断,不包含超时与线程管理。
void ReceiveOnce(HINTERNET webSocket)
{
    char buffer[4096];
    DWORD bytesRead = 0;
    WINHTTP_WEB_SOCKET_BUFFER_TYPE type{};

    const DWORD error = WinHttpWebSocketReceive(
        webSocket,
        buffer,
        static_cast<DWORD>(sizeof(buffer)),
        &bytesRead,
        &type
    );

    if (error != NO_ERROR) {
        OnReceiveError(error);
        return;
    }

    if (type == WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE) {
        OnPeerClosed();
        return;
    }

    OnReceivedData(buffer, bytesRead, type);
}

处理接收结果时,还要注意:

  • WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE 表示收到了关闭帧。即使函数返回 NO_ERROR,也不能按普通消息处理。
  • 一次接收不一定能得到完整 JSON。收到 WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE 时,应继续累积数据;收到 WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE 才表示该文本消息结束。构造字符串时要使用 bytesRead,不要假设缓冲区以 '\0' 结尾。缓冲区类型说明

收到关闭帧后,可以用 WinHttpWebSocketQueryCloseStatus() 读取关闭状态和原因。该函数只能在文档规定的关闭阶段调用,不能随时用它查询连接是否存活。关闭状态查询文档

要让界面在服务器关闭后主动更新,就需要持续运行接收逻辑。如果只在用户点击发送后才接收,客户端处理关闭通知的时间也会推迟。

给每个业务请求设置截止时间

即使连接仍然存在,服务器也可能因为业务逻辑卡住而不返回响应。因此,Chat() 需要单独设置请求期限,例如等待 5 秒。5 秒只是示例,实际值应根据服务器的正常响应时间决定。

单次读取超时不足以判断业务请求是否超时。不断收到其他消息或消息片段,也不代表当前请求已经完成。应使用 std::chrono::steady_clock 记录当前请求的截止时间,只有收到匹配的完整响应才算成功。

WinHttpSetTimeouts() 可以在 session 或 request 句柄上设置 HTTP 事务相关超时,但无法保证升级后的 WebSocket 业务请求一定在指定时间内完成。不要直接把 WebSocket 句柄传给该函数;文档列出的句柄来自 WinHttpOpen() 或 WinHttpOpenRequest()。WinHttpSetTimeouts 文档

需要明确取消等待时,可以采用 WinHTTP 异步模式:

  1. 使用 WINHTTP_FLAG_ASYNC 创建 session,按异步流程完成连接和 WebSocket 升级。
  2. 用 WinHttpSetStatusCallback() 接收操作完成或失败的通知。
  3. 为业务请求登记截止时间,同时保持接收操作。
  4. 收到匹配响应时完成请求;超过期限时,让请求失败,并根据需求废弃当前连接。

异步模式下,WinHttpWebSocketReceive() 的完成结果应从回调中取得。WINHTTP_CALLBACK_STATUS_READ_COMPLETE 对应的状态数据是 WINHTTP_WEB_SOCKET_STATUS,不能继续按同步模式读取输出参数。状态回调文档

控制流程可以写成下面的伪代码,其中的函数由应用自行封装:

StartChat(message):
    if state != Connected:
        CompleteWithError(NotConnected)
        return

    RegisterPendingRequest(message, steady_clock::now() + timeout)
    BeginAsyncSend(message)

OnCompleteMessage(message):
    if MatchesPendingRequest(message):
        CompleteWithResponse(message)

OnSendOrReceiveFailure(error):
    DisconnectCurrentConnection(error)

OnDeadlineExpired(request):
    if request is still pending:
        DisconnectCurrentConnection(ResponseTimeout)

DisconnectCurrentConnection(reason):
    state = Disconnected
    RejectNewSends()
    FailAllPendingRequests(reason)
    CancelPendingAsyncOperationsAndReleaseHandle()
    NotifyUiDisconnected(reason)

超时只能说明请求未在期限内完成,不能据此确定服务器已经退出。按这里的需求,客户端可以在请求超时后主动断开当前连接。

取消接收时,要区分同步和异步模式

当一个线程阻塞在同步 WinHttpWebSocketReceive() 调用中时,不要从另一个线程直接关闭同一个句柄来强行打断接收,这会造成句柄的并发使用问题。

对于已经挂起的异步操作,可以在遵守并发规则的前提下,关闭对应的 WebSocket 句柄来请求取消。关闭动作必须与其他使用该句柄的调用协调。关闭后仍可能收到回调,因此接收缓冲区和回调上下文必须保持有效,直到操作安全结束。WinHTTP 并发规则、WinHttpCloseHandle 文档

正常结束连接时,可以执行 WebSocket 关闭握手。WinHttpWebSocketClose() 不会释放 HINTERNET,最后仍需调用 WinHttpCloseHandle()。关闭握手超时与业务响应超时需要分别设置。WinHttpWebSocketClose 文档

在 wxWidgets 中更新状态并禁用发送

界面层提交网络任务后,应通过事件或完成通知接收结果。不要在按钮事件里同步等待网络响应,也不要在提交任务后立即调用阻塞的 future.get()。

连接状态可以先用三个值表示:

enum class ConnectionState {
    Disconnected,
    Connecting,
    Connected
};

发送按钮仅在 Connected 时启用,网络层也必须检查该状态。禁用按钮是给用户的界面反馈,收发操作本身仍需处理错误。

断开后应执行以下操作:

  • 让所有等待中的请求以错误结束。
  • 通知 wxWidgets 主线程更新状态并禁用发送。
  • 停止向旧连接提交操作。
  • 重新建立连接,完成 WebSocket 升级后再恢复发送。

网络线程可以通过 wxQueueEvent() 或 CallAfter() 通知界面,由主线程修改控件。接收通知的窗口必须仍然有效。重连后还要区分新旧连接,避免旧连接迟到的回调改变新连接的状态。

如果服务器允许并发请求或主动推送消息,应由统一的接收入口分发消息。能否用示例中的 id 匹配响应,取决于服务器协议;不能默认下一条消息就是当前 Chat() 的回复。

keepalive 能解决什么?

keepalive 可以帮助探测空闲连接,并减少中间设备因连接长时间没有流量而将其清除的情况。WebSocket 协议定义了 Ping/Pong 控制帧,用于保活和检查对端是否响应。RFC 6455 第 5.5 节

WinHTTP 提供了 WINHTTP_OPTION_WEB_SOCKET_KEEPALIVE_INTERVAL:

DWORD intervalMs = 30000;

if (!WinHttpSetOption(
        webSocket,
        WINHTTP_OPTION_WEB_SOCKET_KEEPALIVE_INTERVAL,
        &intervalMs,
        sizeof(intervalMs))) {
    const DWORD error = GetLastError();
    // 记录或处理设置失败。
}

该选项设置保活包的发送间隔,单位为毫秒,最小值为 15000。设置为 30 秒,并不保证能在 30 秒内报告断线,也不能代替业务请求的截止时间。WinHTTP 保活选项说明

如果服务器协议已有应用层心跳,可以发送约定的心跳消息并等待回复。由于服务器不受你控制,不能自行发送 "ping" 字符串并假设它会回复。文本消息 "ping" 与 WebSocket Ping 控制帧也不是一回事。

即使底层保活正常,服务器的业务处理仍可能没有响应。客户端应保留请求超时机制:到期后结束等待、废弃当前连接,并保持界面不可发送,直到新连接建立成功。

备注:内容仅供参考。