AI编程:[体验]存量微服务架构下植入WebSocket的“踩坑”与“填坑”

发布于:2025-06-27 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、核心需求

  1. 功能需求:用户可以通过语音与AI对话,并实现类似ChatGPT的实时交互(流式响应,打字机效果)
  2. 技术需求:在现有微服务架构中进行扩展(SpringCloud)

二、技术盲点

  1. 陌生领域
  • 语音录入
  • 语音流传输
  • Java对接大模型API
  1. 技术选型难点
  • WebSocket框架选择
    • Spring WebSocket、t-io、Java-WebSocket、Socket.io等
  • 流式编程 or 响应式编程 (本质是分块传输)
    • Rxjava:UnicastProcessor、Flowable
    • Spring WebFlux:Flux、Mono、WebClient
    • Java9 Reactive Stream API:Publisher、Subscriber、Subscription、Processor
    • Spring MVC:StreamingResponseBody、SseEmitter 、Servlet3.0

三、技术选型与验证

  1. 第一步,技术选型
  • 已验证:Spring WebSocket、t-io、spring-ai-alibaba、百炼灵积等等
  • 未验证:Java-WebSocket、Socket.io
  1. 第二步,验证流程
  • 单点功能验证:
    • 第三方大模型API对接
    • WebSocket服务端与客户端通信
  • 功能集成验证:
    • 语音录入 + WebSocket传输 + 服务端 + 第三方大模型API对接
    • 整合现有微服务架构(语音录入 → 网关 → WebSocket链路验证

四、AI编程中的卡点问题与解决

  1. 问题1:消息大小限制的坑:WebSocket 消息未处理(二进制流)
  • 现象:
    • 服务端无日志、自定义处理器未触发(该问题最大的坑是,无从下手的感觉)
  • 分析:
    • AI解决:先问AI,效果不佳
    • 百度解决:再查百度,效果仍然不佳(有找到正确的解决方案,由于并未真正理解,仍然卡点了很久)
    • Debug源码:此时已无从下手,因此只能Debug源码,逐步排查(逐步理解WebSocket协议的实现原理)
    • Debug源码+AI提示:随着了解的越深,逐步建立手感,对问题原因有了大致的判断(验证判断是否正确)
  • 根因:
    • 消息大小超过限制
  • 解决:
    • 最终方案:调整消息大小,避免超出websocket server端的消息大小限制
# application.yml
spring:
  servlet:
    multipart:
      # 整个请求大小限制
      max-request-size: 10MB
      # 上传单个文件大小限制
      max-file-size: 20MB
  1. 问题2:网关Filter劫持WebSocket的101响应:网关转发WebSocket请求,导致消息未处理的异常
  • 现象:该问题最大的坑是,无从下手的感觉
    • 客户端:报错 “java.io.IOException: 你的主机中的软件中止了一个已建立的连接”
    • 服务端:握手请求成功,有打印握手日志(但数据请求的处理器没有执行)
    • 网关:有打印请求日志,但没有打印响应日志
  • 分析:由于无从下手,只能逐步排除各个环节的问题
    • 限制排查:下意识的认为还是消息大小超过限制的问题(未触发消息大小限制)
    • 网关配置检查:确认网关配置与请求转发的正确性(请求转发正常)
    • 握手请求检查:确认握手请求是否正常被执行(自定义拦截器,握手请求正常执行)
    • 网关请求头检查:检查握手请求头是否正常透传(websocket请求header正常透传)
    • 握手请求的响应检查:到这一步基本确定是response未被正确处理
    • 定位到根因:此处结合AI提示,定位到是网关中的过滤器,未正确处理握手请求的response
    • AI修复问题:定位到问题根因,此时让AI给出方案,但效果不佳,借助该思路,手动修正代码,解决问题
  • 根因:
    • 网关过滤器对Response进行了包装,导致握手请求的101状态码未被正确处理,最终导致WebSocket连接建立失败。
  • 解决:
    • 最终方案:优化网关AccessLogFilter过滤器代码,对websocket请求直接放行
public class AccessLogGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    ServerHttpRequest httpRequest = exchange.getRequest();
    ServerHttpResponse httpResponse = exchange.getResponse();

    // WebSocket 握手请求,直接进入下一个Filter,并返回原始response,确保 WebSocket 握手正常处理101状态码
    // 说明:WebSocket Client 通过Http请求与WebSocket Server握手建立长连接,后续的通信都是该长连接进行通信,不会再被filter拦截
    if (checkWebSocketHandShake(httpRequest)) {
        return filter("websocket handshake", exchange, chain, stopWatch, httpResponse, httpRequest);
    }

    // 网关:判断是否为流式请求,支持ResponseBodyEmitter、SseEmitter的流式响应
    String acceptHeader = httpRequest.getHeaders().getFirst(HttpHeaders.ACCEPT);
    boolean isStreamingRequest = acceptHeader != null && acceptHeader.contains(MediaType.TEXT_EVENT_STREAM_VALUE);
    if (isStreamingRequest) {
        return filter("stream request", exchange, chain, stopWatch, httpResponse, httpRequest);
    }

    // 省略相关代码 ... ...
    
    }

    /**
     * 执行拦截器链(不包装request和response)
     */
    private Mono<Void> filter(String logPrefix, ServerWebExchange exchange, GatewayFilterChain chain, StopWatch stopWatch, ServerHttpResponse httpResponse, ServerHttpRequest httpRequest) {
        if (log.isInfoEnabled() && configProperties.isLogEnabled()) {
            log.info("请求参数 {} [{}] [{}] query:{}, header:{}", logPrefix, httpRequest.getURI().getPath(), httpRequest.getMethod(), httpRequest.getURI().getRawQuery(), exchange.getRequest().getHeaders());
        }
        return chain.filter(exchange).doFinally(s -> MdcUtil.removeTraceId())// 清除MDC
                .then(Mono.fromRunnable(() -> {
                    stopWatch.stop();
                    // 为了方便排查问题,还是打印一个简单的日志
                    if (log.isInfoEnabled()) {
                        log.info("响应参数 {} {} time: {} ms", logPrefix, httpResponse.getRawStatusCode(), stopWatch.getTotalTimeMillis());
                    }
                }));
    }

    /**
     * 检查是否为WebSocket握手请求
     *
     * @return true 表示是WebSocket握手请求
     */
    public boolean checkWebSocketHandShake(ServerHttpRequest httpRequest) {
        // 从请求头中获取 Upgrade 标志,若为websocket,则表示将普通http请求,升级为websocket请求
        String upgradeHeader = httpRequest.getHeaders().getFirst(HttpHeaders.UPGRADE);
        if ("websocket".equalsIgnoreCase(upgradeHeader)) {
            return true;
        }
        return false;
    }
}

五、实践总结

仅针对上述场景的实践总结

  1. 技术实践经验
  • 需深入理解WebSocket协议原理(消息限制、握手流程)
  • 网关需特殊处理WebSocket协议(避免过滤器干扰)
  1. AI工具定位
  • 搜索引擎:替代传统搜索,大部分时候,AI的搜索效果,比百度的效果要好
  • 辅助神器:明确的问题,AI效果好;未知或复杂的问题,AI效果差,但用AI辅助理解技术原理效果好,可将未知或复杂的问题,转换为明确的问题,再让AI解决
  1. AI工具效果
  • 通义灵码(Chat模式):生成代码效果一般,解决问题效果一般
  • Trae(Builder模式):效果较好(有时也会抽疯,需要多轮对话)
  1. AI工具局限性
  • 生成代码需人工校验(无法直接投产)
  • 复杂问题需多轮对话(效率较低,很容易放弃)
  1. AI开发心得
  • 复杂场景,需分步拆解验证,再集成整合
  • 顽固问题,目前AI无法替代人工,需手动介入处理(给AI明确的问题点,AI才会有更好的效果)
  • 陌生领域,对于不了解的技术,上手变得容易很多(如流式编程)
  • 开发习惯,过往的开发习惯较难改变,存在习惯性忽略使用AI的情况(需多用多练)

六、开放性思考

  • 普通人,使用AI编程,对代码的要求是什么?
    • 生成简单的验证性代码,保证功能实现即可,不关注代码质量
  • 程序员,使用AI编程,对代码的要求是什么?
    • 生成可投产的生产级别代码,保证功能实现的同时,也关注代码质量和测试质量(要求完全自主可控)
  • 面对AI编程效果不佳,尤其经过多轮对话,问题还未解决时,你是否萌生退意??
    • 期望过高:通过一次性的需求描述,AI就能自动生成完整的代码或解决问题。
    • 实际拉垮:AI生成的代码,或用AI解决问题时,困难重重,始终无法达到预期。
    • 扪心自问:使用过程中,面对AI也解决不了的问题时,你有产生过,被劝退的感觉吗?

七、WebSocket相关原理

WebSocket握手请求与数据请求的区别

一、握手请求(Handshake Request):

  • 本质:握手请求是HTTP协议的升级请求,用于建立WebSocket连接
  • 说明:服务器需响应HTTP 101状态码,确认协议升级,并返回Sec-WebSocket-Accept验证字段

二、数据请求(Message Frame)

  • 本质:WebSocket连接建立后,客户端和服务端通过数据帧(Frame)进行双向通信。
  • 数据以帧格式传输,包含操作码(Opcode)、掩码(Mask)、载荷长度(Payload Length)等信息
  • 常见帧类型:文本帧(Opcode=1)、二进制帧(Opcode=2)、控制帧(如Ping/Pong)。

网站公告

今日签到

点亮在社区的每一天
去签到