如何使用 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 异步模式:
- 使用
WINHTTP_FLAG_ASYNC创建 session,按异步流程完成连接和 WebSocket 升级。 - 用
WinHttpSetStatusCallback()接收操作完成或失败的通知。 - 为业务请求登记截止时间,同时保持接收操作。
- 收到匹配响应时完成请求;超过期限时,让请求失败,并根据需求废弃当前连接。
异步模式下,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 控制帧也不是一回事。
即使底层保活正常,服务器的业务处理仍可能没有响应。客户端应保留请求超时机制:到期后结束等待、废弃当前连接,并保持界面不可发送,直到新连接建立成功。
备注:内容仅供参考。
