Handshake failed due to invalid Upgrade header: null 解决方案

本文解决WebSocket在QA环境中因Nginx忽略Upgrade头导致的问题,通过在Nginx配置中添加proxy_set_header升级请求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

webSocket推送前端数据,本地开发环境没问题,QA环境报错:

org.springframework.web.socket.server.support.DefaultHandshakeHandler : Handshake failed due to invalid Upgrade header: null

  经过比较后发现,QA环境使用nginx反向代理服务器忽略了headers中的:Upgrade:websocket。

  解决办法:在  Nginx  ,location 中添加以下红色代码:

  proxy_set_header Upgrade $http_upgrade;

  proxy_set_header Connection  "upgrade";

server {
        listen      80;
        server_name  localhost;
        client_max_body_size 10240m;

        location /jk_ers{
            proxy_pass http://jkErs/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";

        }
       
    }

  使得请求中包含Upgrade。

import { Client } from '@stomp/stompjs'; import SockJS from 'sockjs-client'; // npm remove stompjs // npm install @stomp/stompjs sockjs-client --save export default { connect() { return new Promise((resolve, reject) => { const socket = new SockJS('http://10.228.73.15:7788/websocket'); const stompClient = new Client({ webSocketFactory: () => socket, debug: () => { // 禁用调试输出,或根据需要处理 }, reconnectDelay: 5000, heartbeatIncoming: 4000, heartbeatOutgoing: 4000, }); stompClient.onConnect = (frame) => { console.log('Connected:', frame); resolve(stompClient); // 连接成功后立即订阅个人主题 // stompClient.subscribe(`/topic/commandOutput/${userId}`, (message) => { // console.log(1,message) // }); }; stompClient.onStompError = (error) => { console.error('Connection error:', error); reject(error); }; stompClient.activate(); }); } };<template> <div> <span style="display: flex; justify-content: center; margin-bottom: 8px"> <img alt="Vue logo" src="../assets/logo.png" style="width: 90px;"> </span> <span style="display: flex; justify-content: center; margin-bottom: 8px"> 当前登录用户: <input v-model="userId" placeholder="请输入p13" @keyup.enter="connectWebSocket"> <button @click="connectWebSocket" style="margin-left: 10px;">登录</button> </span> <span style="display: flex; justify-content: center"> <input ref="commandInput" v-model="currentCommand" style="width: 600px" placeholder="输入任意命令(如:ipconfig / dir)" :disabled="isExecuting" @keyup.enter="sendCommand" > <button @click="sendCommand" style="margin-left: 10px;" :disabled="isExecuting">执行命令</button> <button @click="abortCommand" style="margin-left: 10px; background: #ff4d4f" :disabled="!isExecuting" > 中止命令 </button> </span> <div style="display: flex"> <div style="margin-top: 20px; width: 30%;"> <h3 style="text-align: center">已执行命令</h3> <div style="height: 482px; overflow-y: auto; background: #f8f8f8;"> <div v-for="(msg, index) in messages" :key="index" style="margin-top: 8px; margin-left: 8px; border-radius: 4px; overflow-y: auto;" >  </div> </div> </div> <div style="padding: 20px; width: 70%"> <h3 style="text-align: center">实时命令输出</h3> <pre ref="outputPre" style="background: #282c34; color: #fff; padding: 15px; border-radius: 4px; height: 450px; overflow-y: auto" >  </pre> </div> </div> </div> </template> <script> import websocket from '@/utils/websocket' export default { name: 'HelloWorld', props: { msg: String }, data() { return { currentCommand: '', messages: [], currentOutput: '', stompClient: null, index: 1, userId: '', isExecuting: false // 控制命令是否正在执行 }; }, methods: { async connectWebSocket() { alert("登录成功"); try { this.stompClient = await websocket.connect(this.userId); this.$nextTick(() => { this.$refs.commandInput.focus(); // 命令结束后重新聚焦到输入框 }); const subscription = this.stompClient.subscribe(`/topic/commandOutput/${this.userId}`, (message) => { const output = message.body; if (output.startsWith("⏹")) { this.isExecuting = false; // 命令执行结束,允许用户继续输入 this.$nextTick(() => { this.$refs.commandInput.focus(); // 命令结束后重新聚焦到输入框 }); return; } this.currentOutput += output.trim() + '\n'; this.$nextTick(() => { if (this.$refs.outputPre) { this.$refs.outputPre.scrollTop = this.$refs.outputPre.scrollHeight; } }); }); this.stompClient.onDisconnect = () => { subscription.unsubscribe(); }; // 检查命令执行状态 await this.checkCommandStatus(); } catch (error) { console.error('WebSocket连接失败:', error); } }, async checkCommandStatus() { try { const response = await fetch(`/win/status/${this.userId}`); if (!response.ok) { console.error('无法获取命令状态'); return; } this.isExecuting = await response.json(); // 更新前端状态 if (this.isExecuting) { alert("上一个命令尚未完成,请等待或中止命令!"); } } catch (error) { console.error('检查命令状态时出错:', error); } }, sendCommand() { if (this.userId === '') { alert("请输入当前登录用户"); return; } this.isExecuting = true; if (this.currentCommand.trim() && this.stompClient) { // 删除 currentOutput 的最后一个非空行 if (this.currentOutput) { let lastNewLineIndex = this.currentOutput.lastIndexOf('\n'); let lastNonEmptyIndex = -1; // 从后往前遍历,找到最后一个非空行的起始位置 for (let i = this.currentOutput.length - 1; i >= 0; i--) { if (this.currentOutput[i] === '\n') { // 如果当前字符是换行符,并且之前的字符不是空白字符,则记录位置 if (lastNewLineIndex !== -1 && i < lastNewLineIndex) { lastNonEmptyIndex = lastNewLineIndex; break; } lastNewLineIndex = i; } else if (this.currentOutput[i].trim() !== '') { // 找到非空字符,更新 lastNewLineIndex lastNewLineIndex = i; } } // 如果找到了非空行,则删除它 if (lastNonEmptyIndex !== -1) { const nextNewLineIndex = this.currentOutput.indexOf('\n', lastNonEmptyIndex); if (nextNewLineIndex === -1) { this.currentOutput = this.currentOutput.substring(0, lastNonEmptyIndex); } else { this.currentOutput = this.currentOutput.substring(0, lastNonEmptyIndex) + this.currentOutput.substring(nextNewLineIndex); } } // 删除最后一个空行 if (this.currentOutput.endsWith('\n')) { this.currentOutput = this.currentOutput.substring(0, this.currentOutput.length - 1); } } // 发送命令 this.stompClient.publish({ destination: '/app/executeCommand', body: JSON.stringify({ command: this.currentCommand.trim(), //todo 之后改为动态 host: '10.228.73.15', userId: this.userId }), }); this.messages.push(this.index + '. ' + this.currentCommand); this.index += 1; this.currentCommand = ''; } }, async abortCommand() { if (this.stompClient) { const response = await fetch(`/win/handleAbort/${this.userId}`); if (response){ this.messages.push("已发送中止命令"); } } }, onTabChange(key, type) { this[type] = key; }, }, beforeDestroy() { if (this.stompClient) { this.stompClient.deactivate(); } } } </script> <style scoped> pre { white-space: pre-line; background-color: #f4f4f4; padding: 10px; border: 1px solid #ddd; } </style>为什么我这上面的可以连接成功,并发送成功。但是我本地访问ws://10.228.73.15:7788/websocket不成功。使用package com.example.demo.config; import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.simp.stomp.*; import org.springframework.util.MimeTypeUtils; import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.client.standard.StandardWebSocketClient; import org.springframework.web.socket.messaging.WebSocketStompClient; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; public class StompClientSender { private final String serverUrl = "ws://10.228.73.15:7788/websocket"; // 根据实际端口调整 private WebSocketStompClient stompClient; public StompClientSender() { this.stompClient = new WebSocketStompClient(new StandardWebSocketClient()); stompClient.setMessageConverter(new MappingJackson2MessageConverter()); } public void sendCommand(String command, String host, String userId) { CompletableFuture<Void> future = new CompletableFuture<>(); try { StompSessionHandler sessionHandler = new StompSessionHandlerAdapter() { @Override public void afterConnected(StompSession session, StompHeaders connectedHeaders) { System.out.println("✅ 成功连接到STOMP服务器!Session ID: " + session.getSessionId()); StompHeaders headers = new StompHeaders(); headers.setDestination("/app/executeCommand"); headers.setContentType(MimeTypeUtils.APPLICATION_JSON); Map<String, Object> payload = new HashMap<>(); payload.put("command", command); payload.put("host", host); payload.put("userId", userId); session.send(headers, payload); // 使用带headers的send方法 future.complete(null); session.disconnect(); } @Override public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) { future.completeExceptionally(exception); } }; // 建立连接 stompClient.connect( serverUrl, new WebSocketHttpHeaders(), new StompHeaders(), sessionHandler ); future.get(5, TimeUnit.SECONDS); // 添加5秒超时等待 } catch (Exception e) { e.printStackTrace(); // 打印详细错误 future.completeExceptionally(e); } } }也不成功
最新发布
06-24
<think>我们正在讨论WebSocket连接问题。用户提到通过代码(使用SockJS和STOMP)可以成功连接到ws://10.228.73.15:7788/websocket并发送消息,但通过浏览器或其他工具手动访问同一地址失败。我们需要分析可能的原因。首先,我们需要理解WebSocket连接的基本原理。WebSocket是一种在单个TCP连接上进行全双工通信的协议,通常用于实时应用。在建立WebSocket连接时,客户端会发送一个HTTPUpgrade请求,将连接从HTTP升级为WebSocket。用户使用SockJS和STOMP库通过代码连接成功,而手动通过浏览器(比如在浏览器控制台使用WebSocketAPI)或其他工具(如curl或WebSocket测试工具)访问失败。这表明问题可能出现在连接建立的方式上。可能的原因包括:1.**协议差异**:SockJS并不是直接使用原生WebSocket,而是先尝试使用WebSocket,如果失败则降级到其他传输方式(如长轮询)。STOMP是基于WebSocket(或其他传输层)的简单文本消息协议。因此,使用SockJS+STOMP时,实际建立的连接可能不是纯WebSocket连接,而是降级到了其他传输方式。但用户明确提到代码中使用的是WebSocket地址,并且成功,所以这里可能是直接WebSocket连接成功。那么为什么手动连接失败呢?2.**连接URL的差异**:使用SockJS时,实际的WebSocketURL可能不是简单的ws://10.228.73.15:7788/websocket,而是有一个附加的路径。例如,SockJS会在基础URL后面添加一个随机路径(如`/websocket/{server-id}/{session-id}/websocket`)。但是,用户提供的地址是基础地址,可能手动访问时也需要特定的路径?然而,用户通过代码连接时使用的就是这个基础地址,说明这个基础地址在SockJS客户端中会被正确处理,而手动访问时直接使用该地址可能不正确。3.**服务器要求特定的子协议**:WebSocket握手时可以指定子协议(如STOMP)。在代码中,STOMP客户端会明确设置子协议,而手动连接时如果没有设置子协议,服务器可能拒绝连接。4.**跨域问题(CORS)**:如果从浏览器手动测试,且页面来源与WebSocket服务器不同源,则可能因为服务器没有正确设置CORS响应头而被浏览器拒绝。但是,WebSocket握手并不受同源策略的限制,浏览器会发送Origin头,服务器可以接受或拒绝。如果服务器拒绝,则连接失败。5.**服务器对User-Agent或其他请求头的检查**:服务器可能根据请求头中的某些字段(如User-Agent)来拒绝连接。代码中使用的库会发送特定的User-Agent(如SockJS的User-Agent),而浏览器或其他工具发送的User-Agent可能被服务器拒绝。6.**认证问题**:如果服务器需要认证(如cookie、token等),代码中可能已经处理了认证,而手动连接时没有提供。7.**网络环境问题**:本地浏览器访问时,可能因为代理设置、防火墙或DNS问题导致无法连接到该地址。但代码运行在同样的本地环境却能成功,所以这个可能性较低。8.**服务器端配置**:服务器可能只接受特定的客户端,例如只接受已知的客户端库(如SockJS)的请求,拒绝其他客户端。为了排查问题,我们可以建议用户进行以下操作:-使用浏览器开发者工具查看WebSocket连接尝试的详细情况(网络面板,筛选WS类型)。-使用Wireshark等网络抓包工具,分别捕获代码连接和手动连接时的网络包,比较握手请求的差异。-尝试使用其他WebSocket客户端工具(如Postman或wscat)连接,看是否同样失败。具体到用户的问题,我们重点分析URL、子协议和请求头。**关于URL**:SockJS客户端使用的URL通常是基础URL,例如`http://10.228.73.15:7788/websocket`(注意这里是http,但WebSocket连接使用ws),在内部,SockJS会构建实际的WebSocketURL。而用户手动访问时直接使用`ws://10.228.73.15:7788/websocket`,这可能是SockJS的端点,但SockJS服务器可能要求连接到一个包含随机路径的URL。因此,直接使用基础URL可能不正确。**子协议**:STOMP客户端在WebSocket连接建立后,会发送一个STOMPCONNECT帧,但WebSocket握手阶段需要指定子协议。在代码中,STOMP客户端会设置`Sec-WebSocket-Protocol`头为`v10.stomp,v11.stomp,v12.stomp`等。而手动连接时,如果不指定子协议,服务器可能因为缺少子协议而拒绝连接。**请求头**:SockJS客户端在建立连接时会发送一些特定的请求头,例如:```Upgrade:websocketConnection:UpgradeSec-WebSocket-Version:13Sec-WebSocket-Key:[随机字符串]Sec-WebSocket-Protocol:v12.stomp,v11.stomp,v10.stomp,stomp```而浏览器手动使用WebSocketAPI时,可能不会设置`Sec-WebSocket-Protocol`(除非在构造函数中指定),或者User-Agent不同。因此,手动连接失败的原因很可能是缺少必要的子协议(STOMP)或者请求的路径不正确。**测试建议**:1.在浏览器控制台尝试连接时,指定子协议:```javascriptvarws=newWebSocket('ws://10.228.73.15:7788/websocket',['v12.stomp']);//注意:子协议数组可以尝试多个版本,如['v12.stomp','v11.stomp','v10.stomp','stomp']```然后监听事件:```javascriptws.onopen=function(){console.log('connected');};ws.onerror=function(e){console.log('error',e);};ws.onclose=function(e){console.log('closed',e);};```2.使用wscat命令行工具(需要安装):```bashwscat-c"ws://10.228.73.15:7788/websocket"--header"Sec-WebSocket-Protocol:v12.stomp"```3.如果以上方法仍然失败,则需要检查服务器日志,查看连接失败的具体原因。另外,注意SockJS端点通常不是直接使用WebSocket协议,而是通过`/websocket`作为入口,然后重定向到具体的传输端点。因此,直接连接`ws://10.228.73.15:7788/websocket`可能不被接受。实际上,SockJS的WebSocket传输通道的URL格式是:`ws://10.228.73.15:7788/websocket/{server-id}/{session-id}/websocket`。所以,手动连接基础URL是不正确的。在代码中,当我们创建SockJS实例时,我们提供基础URL,然后SockJS客户端会通过HTTPGET请求获取服务器信息,其中会包含可用的传输方式以及具体的WebSocketURL。因此,手动直接连接基础URL并不等同于SockJS客户端实际建立的WebSocket连接。综上所述,手动连接失败的最可能原因是:-直接使用了SockJS的基础URL,而实际WebSocket连接需要具体的路径(由服务器动态分配)。-缺少必要的子协议(STOMP)。因此,如果用户想通过浏览器手动测试WebSocket连接,应该使用SockJS客户端实际建立的WebSocketURL(可以通过浏览器的网络面板查看)并指定STOMP子协议。如果用户只是想测试WebSocket服务是否可用,可能需要使用与SockJS兼容的方式,或者直接使用STOMP客户端库进行测试,而不是直接使用原生WebSocket。</think>### WebSocket连接失败排查分析 您遇到的问题本质上是**协议级差异**和**握手过程要求不同**导致的。以下是详细原因分析和排查方案: --- ### 一、核心原因分析 1. **SockJS的协议封装机制** SockJS在建立连接时会自动添加必要的协议层: - 发送`Origin`请求头(浏览器会限制手动添加) - 包含`Sec-WebSocket-Protocol: v12.stomp`等子协议声明 - 自动处理STOMP心跳机制 *代码连接时这些由库自动完成,手动访问时缺失导致握手失败*[^1] 2. **STOMP协议依赖** ```mermaid graph LR A[客户端] -->|原始WebSocket| B[服务器] C[SockJS+STOMP客户端] -->|封装帧结构| D[STOMP协议层] -->|处理心跳/订阅| B ``` 服务器期望收到**STOMP格式帧**(如`CONNECT\naccept-version:1.2\n\n\x00`),手动发送原始WebSocket消息会被拒绝[^2]。 3. **跨域限制(CORS)** 浏览器对`ws://`有严格同源策略: - 代码中可通过`withCredentials`配置跨域 - 手动访问时缺少`Origin`头或服务端未配置`Access-Control-Allow-Origin` 4. **路径差异** SockJS实际连接路径可能包含动态后缀(如`/websocket/732/xy8zoi3k/websocket`),直接访问基础路径无效。 --- ### 二、逐步排查方案 #### 步骤1:验证基础连接 ```bash # 使用telnet检查端口可达性 telnet 10.228.73.15 7788 # 成功应显示空光标(若失败需检查防火墙/NAT) ``` #### 步骤2:捕获实际请求 在浏览器开发者工具的网络面板中: 1. 筛选`WS`类型请求 2. 找到SockJS建立的连接(路径含随机字符串) 3. 检查`Request Headers`中的关键头: ```http Sec-WebSocket-Protocol: v12.stomp, v11.stomp Origin: http://your-domain.com ``` #### 步骤3:手动模拟连接 使用**wscat**工具指定协议头: ```bash npm install -g wscat wscat -c "ws://10.228.73.15:7788/websocket" \ -H "Sec-WebSocket-Protocol: v12.stomp" \ -H "Origin: http://allowed-domain.com" ``` *若成功则证明协议头缺失问题* #### 步骤4:服务端配置检查 确认服务端实现: ```java // Spring Boot配置示例 @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/websocket") .setAllowedOrigins("*") // 临时允许所有源 .withSockJS(); } } ``` --- ### 三、根本解决方案 1. **浏览器端测试** 使用STOMP客户端库模拟连接: ```html <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@6/lib/stomp.min.js"></script> <script> const client = new StompJs.Client({ brokerURL: 'ws://10.228.73.15:7788/websocket', connectHeaders: { host: 'allowed-domain.com' }, debug: str => console.log(str) }); client.activate(); </script> ``` 2. **专业工具测试** 使用支持STOMP的调试工具: - [STOMP Client for VS Code](https://marketplace.visualstudio.com/items?itemName=taniko.vscode-stomp-client) - [Postman WebSocket测试](https://learning.postman.com/docs/sending-requests/websocket/websocket/) 3. **服务端日志分析** 检查连接拒绝时的具体错误: ```log # Spring Boot日志关键词 org.springframework.web.socket | Handshake failed due to invalid Origin header org.springframework.web.socket | Missing STOMP 'CONNECT' frame ``` --- ### 四、关键结论 | 对比项 | 代码连接成功 | 手动访问失败 | |--------|-------------|-------------| | **协议处理** | SockJS自动封装STOMP | 原始WebSocket | | **请求头** | 自动添加Origin/Protocol | 需手动指定 | | **消息格式** | STOMP帧结构 | 原始二进制 | | **跨域处理** | 库自动处理 | 受浏览器限制 | **根本原因**:手动访问未满足STOMP-over-WebSocket的协议要求,而非网络层问题[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值