登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java HttpClient WebSocket 如何优雅关闭:CompletionStage 回调、状态竞态与重连边界

来源:17golang原创

时间:2026-08-30 06:07:39 162浏览 收藏

Java HttpClient 的 WebSocket 客户端要下线时,真正容易出问题的不是调用一次 sendClose,而是把发送完成、对端确认和异常通知当成了同一个状态。更稳妥的做法是让关闭动作只进入一次,用 CompletionStage 记录主动关闭的结果,再由 onCloseonError 分别收口。

主动关闭以 sendClose 为起点,最终状态以 onCloseonError 为准;不要用一个布尔值猜测网络连接是否已经收干净。

要点速览
  • buildAsync 只代表连接建立阶段,不能代替关闭确认。
  • sendClose 返回的 CompletionStage 适合记录发送结果,但不等于对端已经完成回收。
  • 重复关闭、回调竞态和异常断线都要进入同一套幂等状态机。

先把 WebSocket 的三个结束时刻分开

订阅客户端通常有三个容易混淆的时刻:buildAsync 成功,说明握手得到一个 WebSocketsendClose 的阶段完成,说明关闭帧的发送动作已经有结果;onClose 到达,说明监听器收到了关闭消息。它们之间不是同一个回调。

下面的示例只保留一个客户端对象和一个关闭入口。AtomicBoolean 负责防止定时任务、用户退出和异常处理同时发起两次关闭,CompletableFuture 则把异步结果留给日志或上层调用方。

final class PriceSocket implements WebSocket.Listener {
    private final AtomicBoolean closing = new AtomicBoolean();
    private final CompletableFuture ended = new CompletableFuture();
    private volatile WebSocket socket;

    CompletionStage connect(HttpClient client, URI uri) {
        return client.newWebSocketBuilder()
                .buildAsync(uri, this)
                .thenAccept(ws -> socket = ws);
    }

    CompletionStage close(int code, String reason) {
        WebSocket ws = socket;
        if (ws == null || !closing.compareAndSet(false, true)) {
            return ended;
        }
        return ws.sendClose(code, reason)
                .thenApply(ignored -> null)
                .exceptionally(error -> { ended.completeExceptionally(error); return null; })
                .thenCompose(ignored -> ended);
    }

    @Override public CompletionStage> onClose(WebSocket ws, int code, String reason) {
        ended.complete(null);
        return CompletableFuture.completedFuture(null);
    }

    @Override public void onError(WebSocket ws, Throwable error) {
        ended.completeExceptionally(error);
    }
}
Java HttpClient WebSocket 从 buildAsync 建立连接,经 sendClose 到 onClose 的关闭生命周期图

图中 buildAsyncsendCloseonClose 是三个真实节点:前者建立连接,中间发送关闭帧,后者完成监听器侧的收口。close 返回的阶段最后等待 ended,所以调用者不会把“发送完成”误判成“连接已结束”。

为什么 sendClose 完成了,程序仍不能立刻退出

sendClose 的返回值是异步阶段。它适合表示发送动作成功或失败,但应用如果在这个阶段完成后马上销毁自己的执行环境,仍可能看不到对端的关闭通知。这里更重要的是把结束信号集中到 ended,而不是在每个调用点各写一套清理逻辑。

主动关闭和异常断线的入口不同,终点却应该相同:正常路径由 onClose 完成 ended,失败路径由 onError 以异常完成它。exceptionally 只负责把发送阶段的错误传给结束阶段,不能伪造正常关闭。

可见信号它说明什么不应据此推断什么
buildAsync 完成握手阶段得到 WebSocket连接未来一定正常关闭
sendClose 完成关闭帧发送阶段有结果对端已回调 onClose
onClose监听器收到关闭消息业务重连一定应该立即发生
onError连接或回调发生异常可以无条件再次调用关闭

回调竞态下,重连只看最终状态

实际运行中,网络断开可能先触发 onError,也可能在主动关闭后收到 onClose。不要在两个回调里直接创建新连接,否则一次断线可能变成两条订阅连接。重连调度应先读取 ended 的完成原因,并由外层策略判断这次结束是否是用户主动退出。

private final AtomicBoolean userRequested = new AtomicBoolean();

void requestStop() {
    userRequested.set(true);
    close(1000, "client shutdown");
}

void restartIfNeeded(ScheduledExecutorService scheduler) {
    ended.whenComplete((ok, error) -> {
        if (!userRequested.get() && error != null) {
            scheduler.schedule(this::connectAgain, 2, TimeUnit.SECONDS);
        }
    });
}
Java WebSocket sendClose、CompletionStage 与 onError 共同进入 ended 状态的竞态收口图

这里的关键链路是 sendCloseCompletionStageonError:发送阶段失败时只完成异常,不把失败包装成正常结束。ended 只完成一次,重连策略再根据 userRequested 和异常原因做决定。

把关闭代码放进产品流程时,检查这四个边界

重复调用 close

定时器、窗口关闭和网络错误可能同时调用。用 compareAndSet(false, true) 抢到唯一主动关闭权,其他调用返回同一个 ended 阶段。

reason 不是日志替代品

关闭原因适合给对端一个短提示,详细异常、连接 URI 和重连次数仍应由应用日志记录,避免把敏感上下文塞进关闭帧。

onError 之后不要再次假设 socket 可用

异常回调只完成结束阶段,重连需要新建连接并重新绑定 Listener。旧的 WebSocket 不应继续发送业务消息。

关闭完成不代表业务数据已保存

如果最后一条业务消息要求落库,先等待业务确认,再调用 sendClose。WebSocket 的关闭握手不能替代业务层 ACK。

相关问题:Java WebSocket 关闭时怎么判断结果

sendClose 的阶段完成后能立即退出吗?

不建议直接退出。它只表示发送阶段完成;应用需要等待 onClose 或明确的异常收口。

onCloseonError 都会到达怎么办?

把结束信号设计成只完成一次的 CompletableFuture,业务层只观察第一次最终结果,后续回调只做日志。

关闭后要不要自动重连?

主动退出通常不重连;异常断线才进入退避重连,并为重连次数、网络恢复和应用退出设置明确边界。

收尾:让连接状态成为可观察的事实

Java HttpClient WebSocket 的优雅关闭,核心不是把 API 调用写得更长,而是承认连接建立、关闭帧发送、关闭通知和异常回调是不同阶段。用 closing 保证入口幂等,用 ended 统一结果,再把是否重连交给外层策略,日志和测试都会清楚很多。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>