生产环境Socket.IO无法连接,本地正常的排查求助
问题概述
本地运行正常的MERN应用,通过Socket.IO实现WebSocket功能,部署到GCP虚拟机实例后,WebSocket连接完全无法建立。已确认GCP防火墙允许HTTP连接,VPS本地用TCP客户端能成功连接WebSocket服务,但通过网站无法建立连接。
服务器端核心代码 (index.js)
// Import required modules const express = require("express"); const cors = require("cors"); const connectToMongo = require("./mongo/dbConnection"); require("dotenv").config(); const cookieParser = require("cookie-parser"); const socketIo = require("socket.io"); const http = require("http"); // Create Express app const app = express(); connectToMongo(); // Middleware app.use(express.json()); app.use(cors()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); // ------- ROUTES ------- app.use('/api/', require('path to file')) // Serving WS and Express servers together const server = http.createServer(app); global.io = socketIo(server, { cors: { origin: process.env.FRONTEND_DOMAIN, methods: ["GET", "POST"], }, }); io.on("connection", (socket) => { console.log("Client connected:", socket.id); socket.on("disconnect", () => { console.log("Client disconnected:", socket.id); }); }); // Start the server const PORT = process.env.PORT; server.listen(PORT, () => { console.log(`Socket Server is running on port ${PORT}`); });
关联路由文件代码
const express = require("express"); const router = express.Router(); const User = require("../../mongo/schema/userSchema"); const Request = require("../../mongo/schema/requestSchema"); const fetchUser = require("../userAuth/fetchUserMiddleware"); router.post("/history", fetchUser, async (req, res) => { const filterCount = 10; const trialFilterCount = 3; try { const userId = req.user.id; const user = await User.findById(userId); const historyData = await Request.find({ userId: userId }) .select(["-_id", "-__v", "-userId"]) .sort({ timestamp: -1 }); if ( historyData.length >= trialFilterCount && user.subscriptionType === "trial" ) { await user.updateOne({ subscriptionType: "notSubscribed" }); global.io.emit("subscriptionStatusChange", { subStatus: false }); } const userHistory = historyData.slice(0, filterCount); res.status(200).send({ success: true, userHistory }); } catch (error) { console.error("Error in Filter History fetching" + error); res.status(500).send("Internal Server Error!"); } }); module.exports = router;
Nginx配置
server { server_name www.<app>.com; # Serve frontend files location / { root /var/www/<app>/; try_files $uri /index.html; } location /api/ { proxy_pass http://localhost:5000/api/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } location /socket/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_pass http://localhost:5000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
前端Socket.IO客户端代码
import axios from "axios"; import React, { createContext, useEffect, useState } from "react"; import io from "socket.io-client"; const AuthContext = createContext(); const backendHost = import.meta.env.VITE_BACKEND_HOST; const socket = io(`${import.meta.env.VITE_BACKEND_SOCKET_HOST}`); socket.on('connect',()=>{ console.log('Socket Connected') }) export const AuthProvider = ({ children }) => { const [isSubscribed, setIsSubscribed] = useState(false); useEffect(() => { socket.on("subscriptionStatusChange", ({ subStatus }) => { setIsSubscribed(subStatus); }); // Clean up // return () => { // socket.off("subscriptionStatusChange"); // }; }, []); return ( <AuthContext.Provider value={{ isSubscribed, setIsSubscribed }} > {children} </AuthContext.Provider> ); }; export default AuthContext;
测试用TCP客户端代码
const net = require('net'); const HOST = '127.0.0.1'; // localhost const PORT = 5000; // Port number to connect to // Create a TCP socket client const client = new net.Socket(); // Attempt to connect to the server client.connect(PORT, HOST, () => { console.log(`Connected to ${HOST}:${PORT}`); // Once connected, send a message to the server client.write('Hello, server! I am a client.'); }); // Handle incoming data from the server client.on('subscriptionStatusChange', (data) => { console.log(`Received data from server: ${data}`); // Close the connection after receiving data client.end(); }); // Handle connection closed by the server client.on('close', () => { console.log('Connection closed by server'); }); // Handle connection errors client.on('error', (err) => { console.error('Error:', err); });
排查步骤建议
修正Nginx Socket路径匹配
Socket.IO默认使用/socket.io/路径,当前Nginx配置的location /socket/不匹配,需修改为:location /socket.io/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_pass http://localhost:5000/socket.io/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }或在服务端Socket.IO初始化时指定路径:
global.io = socketIo(server, { cors: { origin: process.env.FRONTEND_DOMAIN, methods: ["GET", "POST"], }, path: "/socket/" // 匹配Nginx的location路径 });验证前端连接路径
确认VITE_BACKEND_SOCKET_HOST的值为https://www.<app>.com/socket.io/(对应默认路径)或https://www.<app>.com/socket/(自定义路径),可在浏览器控制台直接测试:const socket = io('https://www.<app>.com/socket.io/'),查看是否触发connect事件。检查GCP防火墙规则
确认防火墙允许443端口(HTTPS)入站流量,WebSocket依赖HTTP/HTTPS升级,若前端用HTTPS,必须确保443端口开放。同时检查实例的网络标签是否关联正确的防火墙规则。查看服务日志
- 查看Node.js服务日志,确认是否有客户端连接请求到达;
- 查看Nginx访问日志(
/var/log/nginx/access.log)和错误日志(/var/log/nginx/error.log),排查请求是否被拦截或报错。
确认CORS配置正确性
检查process.env.FRONTEND_DOMAIN是否为前端完整域名(如https://www.<app>.com),Socket.IO的CORS配置必须允许该域名的连接请求。验证HTTPS配置
若前端使用HTTPS,Socket连接必须使用wss://协议,确保Nginx配置了有效的SSL证书,避免浏览器阻止混合内容。
内容的提问来源于stack exchange,提问作者Owl0223

