当前位置:首页 > 文章列表 > 文章 > java教程 > Java HttpClient WebSocket 如何优雅关闭:CompletionStage 回调、状态竞态与重连边界

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

来源:17golang原创 2026-08-30 06:07:39 0浏览 收藏

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删除
Go runtime/metrics.Float64Histogram 如何读取延迟分布:桶边界、计数快照与百分位估算Go runtime/metrics.Float64Histogram 如何读取延迟分布:桶边界、计数快照与百分位估算
上一篇
Go runtime/metrics.Float64Histogram 如何读取延迟分布:桶边界、计数快照与百分位估算
Python 3.14 pathlib.Path.from_uri 怎么解析文件 URI:主机名、百分号编码与平台边界
下一篇
Python 3.14 pathlib.Path.from_uri 怎么解析文件 URI:主机名、百分号编码与平台边界
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    5444次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    4929次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    4846次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    5111次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    5065次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码