一、WSS是什么,为什么你值得认真配置一次
Web Socket Secure(WSS)就是 WebSocket 的加密版,协议头写的是 wss://,本质上是 WebSocket 先走 TLS 握手,再升级成 WebSocket 连接。它比 WS(明文)安全得多,因为浏览器、服务器、网关、CDN 每一跳都在加密通道里传输。很多生产故障不是业务代码写错,而是 TLS 链路没配稳,证书、反向代理、负载均衡、超时参数、子协议、心跳包一个环节松了,前端就报连接失败或证书错误。
我遇到过最典型的场景:前端用 wss://,后端服务正常,但经过 Nginx 或 Cloudflare 后降级成 HTTP,或者证书是 Let’s Encrypt 自动续期失败,或者中间代理没透传 Upgrade 头,或者 TLS 版本不匹配。这类问题用日志盲猜效率极低,正确的做法是先把”环境检测”这一关过掉,再用”分阶段验证”定位卡点。下面我以生产可用的姿态,把完整流程讲清楚,代码、配置、排查命令都给全。
二、先做环境检测,别急着写代码
环境检测的目的,是把不确定性降到低。建议你先回答以下问题,再动手:
- 域名是否已解析?
nslookup或dig能查到 A/AAAA 记录吗? - 目标端口是否开放?
telnet、nc或curl -v能通吗? - 证书是否有效? issuer、subject、notBefore/notAfter 是否正常?
- TLS 版本是否匹配?服务端是否支持 TLS 1.2⁄1.3?
- 中间是否有代理或 CDN?它们是否支持 WebSocket 升级?
- 操作系统、运行时、依赖库版本是否稳定?
我把这些检测写成可执行的脚本,你可以直接复制使用。下面给出 Linux 和 Windows 两个版本的检测命令。
2.1 Linux 环境检测脚本
#!/bin/bash
# check_wss_env.sh
DOMAIN="${1:-your-domain.com}"
PORT="${2:-443}"
echo "=== 1. DNS 解析 ==="
dig +short "$DOMAIN"
echo "=== 2. 端口连通性 ==="
nc -zv "$DOMAIN" "$PORT" || telnet "$DOMAIN" "$PORT"
echo "=== 3. HTTPS 可达性 ==="
curl -vI --max-time 10 "https://$DOMAIN" 2>&1 | head -30
echo "=== 4. TLS 版本与证书信息 ==="
echo | openssl s_client -connect "$DOMAIN:$PORT" -servername "$DOMAIN" 2>/dev/null | sed -n '/Certificate chain/,/---/p'
echo "=== 5. WebSocket 升级试探 ==="
curl -v --max-time 5 \
-H "Upgrade: websocket" \
-H "Connection: Upgrade" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Version: 13" \
"wss://$DOMAIN/ws" 2>&1 | head -40
执行示例:
chmod +x check_wss_env.sh
./check_wss_env.sh example.com 443
2.2 Windows 环境检测脚本
PowerShell 版本:
# check_wss_env.ps1
param($Domain="example.com",$Port=443)
Write-Host "=== 1. DNS 解析 ==="
Resolve-DnsName -Name $Domain -Type A | Select-Object -First 5
Write-Host "=== 2. 端口连通性 ==="
Test-NetConnection -ComputerName $Domain -Port $Port
Write-Host "=== 3. HTTPS 可达性 ==="
Invoke-WebRequest -Uri "https://$Domain" -Method Head -TimeoutSec 10 -UseBasicParsing | Select-Object StatusCode,Headers
Write-Host "=== 4. TLS 证书信息 ==="
try {
$cert = New-Object System.Net.Security.SslStream(
New-Object System.IO.NetworkStream($Domain,$Port,$true), $true
)
$cert.AuthAsClient($Domain)
$cert.RemoteCertificate.Subject
$cert.RemoteCertificate.Issuer
$cert.RemoteCertificate.GetExpirationDateString()
} catch { Write-Host "TLS 握手失败: $_" }
Write-Host "=== 5. WebSocket 升级试探 ==="
# Windows 原生不支持 WebSocket,用第三方工具或Python
python -c "
import asyncio, websockets
async def probe():
try:
ws = await websockets.connect('wss://$Domain/ws')
await ws.close()
print('WSS 连接成功')
except Exception as e:
print('WSS 连接失败:', e)
asyncio.run(probe())
"
运行:
powershell -ExecutionPolicy Bypass -File check_wss_env.ps1 example.com 443
2.3 检测结果的解读原则
- DNS 解析失败:检查 hosts、防火墙、ISP、域名注册商。
- 端口不通:检查安全组、ACL、NAT、iptables、Windows Defender。
- HTTPS 失败:证书链不完整、中间 CA 缺失、hostname 不匹配。
- TLS 握手失败:服务端不支持客户端请求的 TLS 版本或密码套件。
- WebSocket 升级失败:反向代理未转发
Upgrade和Connection头,或网关限制了长连接。
三、证书是核心,证书配置错了等于白干
WSS 必须依赖有效的 TLS 证书。常见错误有:
- 自签名证书在浏览器被拒绝。
- Let’s Encrypt 证书未续期,过期后连接失败。
- 证书链不完整,中间 CA 缺失。
- SAN(Subject Alternative Name)不包含访问域名。
- 私钥与证书不匹配。
3.1 验证证书完整性的命令
# 查看证书详情
openssl x509 -in server.crt -text -noout
# 验证证书链
openssl verify -CAfile ca-chain.crt server.crt
# 查看私钥与证书是否匹配
openssl rsa -in server.key -check -noout
openssl x509 -in server.crt -noout -modulus | md5
openssl rsa -in server.key -noout -modulus | md5
# 两者的 md5 应该一致
3.2 使用 Let’s Encrypt 申请证书
# 安装 certbot
sudo apt-get install certbot python3-certbot-nginx -y
# 为 Nginx 申请证书
sudo certbot --nginx -d example.com -d www.example.com
# 自动续期
sudo certbot renew --dry-run
证书到期前 30 天会自动尝试续期。建议写一个 cron 任务:
# /etc/cron.d/certbot-renew
0 3 * * * root certbot renew --quiet && systemctl reload nginx
3.3 证书链完整性的重要性
很多开发者只部署了 server.crt,没部署中间 CA。浏览器会报错”证书链不完整”。正确的做法是生成完整的 fullchain.crt:
# Let's Encrypt 完整证书链
cat /etc/letsencrypt/live/example.com/fullchain.pem > fullchain.crt
# Nginx 配置
ssl_certificate /etc/nginx/conf.d/fullchain.crt;
ssl_certificate_key /etc/nginx/conf.d/server.key;
四、Nginx 反向代理配置 WebSocket
Nginx 是最常用的反向代理。WebSocket 升级需要特殊处理 Upgrade 和 Connection 头。
4.1 最小可工作的 Nginx WebSocket 配置
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream websocket_backend {
server 127.0.0.1:8080;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/conf.d/fullchain.crt;
ssl_certificate_key /etc/nginx/conf.d/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location /ws {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 超时设置,WebSocket 长连接需要放宽
proxy_connect_timeout 60s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
# 缓冲区设置
proxy_buffering off;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
}
}
4.2 关键参数解释
map $http_upgrade $connection_upgrade:根据客户端是否发送Upgrade头动态设置Connection头。proxy_http_version 1.1:WebSocket 要求 HTTP/1.1。proxy_set_header Upgrade $http_upgrade:透传客户端的Upgrade头。proxy_set_header Connection $connection_upgrade:动态设置Connection头。proxy_send_timeout和proxy_read_timeout:默认 60 秒,WebSocket 空闲时会超时断开,需要设为更大值。proxy_buffering off:关闭缓冲,避免大消息被截断。
4.3 测试 Nginx 配置
sudo nginx -t
sudo systemctl reload nginx
五、后端 WebSocket 服务实现
以 Node.js 为例,使用 ws 库。
5.1 安装依赖
npm install ws uuid
5.2 服务器代码
// wss-server.js
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('/etc/nginx/conf.d/server.key'),
cert: fs.readFileSync('/etc/nginx/conf.d/fullchain.crt'),
};
const server = https.createServer(options, (req, res) => {
res.writeHead(404);
res.end();
});
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws, req) => {
const clientId = uuidv4();
console.log(`[+] Client connected: ${clientId}`);
ws.clientId = clientId;
ws.on('message', (data) => {
const msg = data.toString();
console.log(`[<-] ${clientId}: ${msg}`);
// 回声服务器
ws.send(`[->] Echo: ${msg}`);
});
ws.on('close', (code, reason) => {
console.log(`[-] Client disconnected: ${clientId}, code: ${code}, reason: ${reason}`);
});
ws.on('error', (err) => {
console.error(`[!] Error for client ${clientId}:`, err.message);
});
// 心跳检测
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true;
});
});
// 心跳定时器
const heartbeat = () => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
console.log(`[!] Heartbeat failed, closing connection: ${ws.clientId}`);
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
};
const interval = setInterval(heartbeat, 30000);
wss.on('close', () => {
clearInterval(interval);
});
server.listen(8443, () => {
console.log('[*] WSS server listening on wss://0.0.0.0:8443');
});
5.3 启动服务
node wss-server.js
六、前端连接与错误处理
6.1 浏览器 WebSocket 连接示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WSS 连接测试</title>
</head>
<body>
<h1>WSS 连接测试</h1>
<div id="status">状态:未连接</div>
<input id="msg" placeholder="输入消息">
<button onclick="send()">发送</button>
<ul id="log"></ul>
<script>
const wsUrl = 'wss://example.com/ws';
let ws = null;
function log(msg) {
const li = document.createElement('li');
li.textContent = `${new Date().toISOString()} - ${msg}`;
document.getElementById('log').appendChild(li);
}
function connect() {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
document.getElementById('status').textContent = '状态:已连接';
log('连接成功');
};
ws.onmessage = (event) => {
log(`收到:${event.data}`);
};
ws.onclose = (event) => {
document.getElementById('status').textContent = `状态:已断开 (code=${event.code}, reason=${event.reason})`;
log(`连接关闭: code=${event.code}, reason=${event.reason}`);
// 5 秒后重连
setTimeout(connect, 5000);
};
ws.onerror = (error) => {
log(`连接错误: ${error}`);
};
}
function send() {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('未连接');
return;
}
const msg = document.getElementById('msg').value;
ws.send(msg);
log(`发送:${msg}`);
document.getElementById('msg').value = '';
}
connect();
</script>
</body>
</html>
6.2 前端错误处理策略
- 连接失败时指数退避重连,避免频繁重试。
- 记录错误码和原因,便于排查。
- 心跳包检测连接是否存活。
- 消息序列化使用 JSON,避免二进制解析错误。
七、常见证书报错与解决方案
7.1 ERR_CERT_COMMON_NAME_INVALID
浏览器报错:证书域名与访问域名不匹配。
解决:确保证书的 SAN 包含访问域名。重新申请证书,或在 Nginx 配置中指向正确的证书文件。
7.2 ERR_CERT_DATE_INVALID
证书已过期或未生效。
解决:检查 notBefore 和 notAfter,使用 certbot renew 续期。
7.3 ERR_CERT_AUTHORITY_INVALID
证书链不完整或 CA 不受信任。
解决:部署完整的 fullchain.crt,包含中间 CA。
7.4 ERR_CONNECTION_CLOSED
连接被服务端关闭。
解决:检查服务端日志,可能是心跳超时、内存不足、连接数超限。
7.5 ERR_SSL_PROTOCOL_ERROR
TLS 协议版本不匹配。
解决:服务端启用 TLS 1.2⁄1.3,禁用 SSLv3/TLS 1.0/1.1。
八、网络设置与性能优化
8.1 超时参数调优
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| proxy_connect_timeout | 60s | 60s | 连接超时 |
| proxy_send_timeout | 60s | 3600s | 发送超时 |
| proxy_read_timeout | 60s | 3600s | 读取超时 |
| keepalive_timeout | 75s | 3600s | 长连接超时 |
8.2 连接数限制
# worker_processes auto;
worker_connections 4096;
8.3 CDN 场景下的 WebSocket
CDN(如 Cloudflare)默认不支持 WebSocket,需要:
- 在 CDN 控制台启用 WebSocket。
- 确保 CDN 边缘节点支持
Upgrade头透传。 - 设置源站为 TLS 1.2+。
8.4 负载均衡场景
使用 L4 负载均衡(如 HAProxy、AWS NLB)时,需要启用 stickiness(会话保持),否则 WebSocket 连接可能被分配到不同后端。
九、完整部署清单
- [ ] 域名解析正确,DNS 记录已生效。
- [ ] TLS 证书有效,链完整,SAN 包含域名。
- [ ] Nginx 配置
map、Upgrade、Connection头。 - [ ] 后端 WebSocket 服务启动,证书路径正确。
- [ ] 前端使用
wss://,错误处理完善。 - [ ] 心跳包机制启用,超时参数调优。
- [ ] 日志记录连接事件,便于排查。