Java HttpClient WebSocket 如何优雅关闭:CompletionStage 回调、状态竞态与重连边界
来源:17golang原创
时间:2026-08-30 06:07:39 162浏览 收藏
Java HttpClient 的 WebSocket 客户端要下线时,真正容易出问题的不是调用一次 sendClose,而是把发送完成、对端确认和异常通知当成了同一个状态。更稳妥的做法是让关闭动作只进入一次,用 CompletionStage 记录主动关闭的结果,再由 onClose 和 onError 分别收口。
主动关闭以
sendClose为起点,最终状态以onClose或onError为准;不要用一个布尔值猜测网络连接是否已经收干净。
buildAsync只代表连接建立阶段,不能代替关闭确认。sendClose返回的CompletionStage适合记录发送结果,但不等于对端已经完成回收。- 重复关闭、回调竞态和异常断线都要进入同一套幂等状态机。
先把 WebSocket 的三个结束时刻分开
订阅客户端通常有三个容易混淆的时刻:buildAsync 成功,说明握手得到一个 WebSocket;sendClose 的阶段完成,说明关闭帧的发送动作已经有结果;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);
}
}

图中 buildAsync、sendClose 和 onClose 是三个真实节点:前者建立连接,中间发送关闭帧,后者完成监听器侧的收口。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);
}
});
}

这里的关键链路是 sendClose → CompletionStage → onError:发送阶段失败时只完成异常,不把失败包装成正常结束。ended 只完成一次,重连策略再根据 userRequested 和异常原因做决定。
把关闭代码放进产品流程时,检查这四个边界
重复调用 close
定时器、窗口关闭和网络错误可能同时调用。用 compareAndSet(false, true) 抢到唯一主动关闭权,其他调用返回同一个 ended 阶段。
reason 不是日志替代品
关闭原因适合给对端一个短提示,详细异常、连接 URI 和重连次数仍应由应用日志记录,避免把敏感上下文塞进关闭帧。
onError 之后不要再次假设 socket 可用
异常回调只完成结束阶段,重连需要新建连接并重新绑定 Listener。旧的 WebSocket 不应继续发送业务消息。
关闭完成不代表业务数据已保存
如果最后一条业务消息要求落库,先等待业务确认,再调用 sendClose。WebSocket 的关闭握手不能替代业务层 ACK。
相关问题:Java WebSocket 关闭时怎么判断结果
sendClose 的阶段完成后能立即退出吗?
不建议直接退出。它只表示发送阶段完成;应用需要等待 onClose 或明确的异常收口。
onClose 和 onError 都会到达怎么办?
把结束信号设计成只完成一次的 CompletableFuture,业务层只观察第一次最终结果,后续回调只做日志。
关闭后要不要自动重连?
主动退出通常不重连;异常断线才进入退避重连,并为重连次数、网络恢复和应用退出设置明确边界。
收尾:让连接状态成为可观察的事实
Java HttpClient WebSocket 的优雅关闭,核心不是把 API 调用写得更长,而是承认连接建立、关闭帧发送、关闭通知和异常回调是不同阶段。用 closing 保证入口幂等,用 ended 统一结果,再把是否重连交给外层策略,日志和测试都会清楚很多。
-
165 收藏
-
453 收藏
-
387 收藏
-
233 收藏
-
文章 · java教程 | 3小时前 | 并发 · Java · 线程池 · 性能优化 · future · java 并发任务 future take poll CompletionService 完成队列479 收藏
-
320 收藏
-
文章 · java教程 | 5小时前 | 调试 · 二进制 · Arrays · Java教程 · 边界处理 · Java Arrays.mismatch 二进制比较 byte数组 范围比较 差异定位204 收藏
-
文章 · java教程 | 5小时前 | 反射 · Java教程 · 方法句柄 · 类型适配 · 调用校验 · astype MethodType Java MethodHandle insertArguments invokeExact175 收藏
-
162 收藏
-
381 收藏
-
165 收藏
-
文章 · java教程 | 8小时前 | 字符集 · Java教程 · 异常排查 · CharsetDecoder · ByteBuffer · 乱码 ByteBuffer Java教程 Java CharsetDecoder 字符解码265 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习