做实时游戏或者聊天功能的时候,WebSocket 是绕不开的。但一旦网站上了 HTTPS,普通的 ws:// 连接就会被浏览器拦截,必须用 wss://。这篇记录一下我用 Nginx 配置 HTTPS 和 WSS 时踩过的坑。
为什么必须用 WSS
先看个简单的对比:
1 2 3 4 5 6 7 8 9 10 11 12
| HTTP 页面 (http://) HTTPS 页面 (https://) │ │ ▼ ▼ ┌──────────┐ ┌──────────┐ │ ws:// │ ✓ 允许 │ ws:// │ ✗ 被阻止 │ 明文传输 │ │ 明文传输 │ (Mixed Content) └──────────┘ └──────────┘
┌──────────┐ ┌──────────┐ │ wss:// │ ✓ 允许 │ wss:// │ ✓ 允许 │ SSL加密 │ │ SSL加密 │ └──────────┘ └──────────┘
|
浏览器的安全策略就是这样:HTTPS 页面里不能加载 HTTP 资源,包括 ws://。这是为了防止中间人攻击,没办法,只能遵守。
Nginx 基础安装
1 2 3 4 5 6 7 8 9 10
| sudo apt-get update sudo apt-get install nginx
sudo yum install epel-release sudo yum install nginx
nginx -v
|
获取 SSL 证书
1 2 3 4 5 6 7 8 9
| sudo apt-get install certbot python3-certbot-nginx sudo certbot --nginx -d yourdomain.com
|
完整配置示例
基础 HTTPS + WSS 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
|
upstream wss_backend { server 192.168.0.100:38080 weight=5; server 192.168.0.101:38080 weight=5; server 192.168.0.102:38080 weight=5 backup;
keepalive 32; }
server { listen 443 ssl http2; server_name api.yourgame.com;
ssl_certificate /etc/nginx/ssl/yourdomain.pem; ssl_certificate_key /etc/nginx/ssl/yourdomain.key;
ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;
ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers on;
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
access_log /var/log/nginx/wss_access.log; error_log /var/log/nginx/wss_error.log;
location / { root /var/www/html; index index.html; try_files $uri $uri/ /index.html; }
location /wss { proxy_pass http://wss_backend;
proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header 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;
proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s;
proxy_buffering off; proxy_buffers 8 32k; proxy_buffer_size 64k; }
location /api { proxy_pass http://wss_backend; proxy_http_version 1.1;
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;
proxy_connect_timeout 30s; proxy_send_timeout 30s; proxy_read_timeout 30s; } }
server { listen 80; server_name api.yourgame.com; return 301 https://$server_name$request_uri; }
|
关键配置项详解
WebSocket 核心配置
1 2 3 4
| proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
|
工作原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Client Nginx Backend │ │ │ │ 1. 发送升级请求 │ │ │ GET /wss HTTP/1.1 │ │ │ Upgrade: websocket│ │ │──────────────────►│ │ │ │ │ │ │ 2. 转发升级请求 │ │ │ Upgrade: websocket│ │ │──────────────────►│ │ │ │ │ │ 3. 返回 101 Switching Protocols │ │◄──────────────────│ │ │ │ │ 4. 返回 101 │ │ │◄──────────────────│ │ │ │ │ │ 5. WebSocket 数据传输(双向) │ │◄═══════════════════════════════════════════════►
|
超时配置说明
| 配置项 |
默认值 |
建议值 |
说明 |
| proxy_connect_timeout |
60s |
60s |
连接建立超时 |
| proxy_send_timeout |
60s |
60s |
发送数据超时 |
| proxy_read_timeout |
60s |
300s |
读取数据超时 |
注意: WebSocket 是长连接,proxy_read_timeout 需要根据心跳间隔设置。如果心跳是 30 秒一次,建议设置为 60-120 秒。
高级配置
负载均衡配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| upstream websocket_cluster { server 192.168.0.101:38080 weight=5; server 192.168.0.102:38080 weight=5; server 192.168.0.103:38080 weight=3;
check interval=3000 rise=2 fall=3 timeout=1000 type=http; check_http_send "GET /health HTTP/1.0\r\n\r\n"; check_http_expect_alive http_2xx http_3xx; }
|
负载均衡策略对比:
| 策略 |
指令 |
适用场景 |
| 轮询 |
默认 |
后端性能一致 |
| 加权轮询 |
weight= |
后端性能不同 |
| IP Hash |
ip_hash |
需要会话保持 |
| 最少连接 |
least_conn |
长连接场景 |
连接限制与限流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| limit_conn_zone $binary_remote_addr zone=addr:10m; limit_conn addr 10;
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s; limit_req zone=one burst=20 nodelay;
server { listen 443 ssl; server_name api.yourgame.com;
location /wss { limit_conn addr 5; limit_rate 100k;
proxy_pass http://wss_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
|
SSL 性能优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| server { listen 443 ssl http2;
ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_session_cache shared:SSL:50m; ssl_session_timeout 1d; ssl_session_tickets off;
ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/nginx/ssl/chain.pem; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/ssl/dhparam.pem; }
|
多域名配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| server { listen 443 ssl; server_name game1.yourdomain.com;
ssl_certificate /etc/nginx/ssl/game1.pem; ssl_certificate_key /etc/nginx/ssl/game1.key;
location /wss { proxy_pass http://game1_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
server { listen 443 ssl; server_name game2.yourdomain.com;
ssl_certificate /etc/nginx/ssl/game2.pem; ssl_certificate_key /etc/nginx/ssl/game2.key;
location /wss { proxy_pass http://game2_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
|
客户端连接示例
JavaScript WebSocket 客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| const wsUrl = 'wss://api.yourgame.com/wss'; const socket = new WebSocket(wsUrl);
socket.onopen = function(event) { console.log('WSS Connection established');
socket.send(JSON.stringify({ type: 'login', token: 'your_auth_token' })); };
socket.onmessage = function(event) { const data = JSON.parse(event.data); console.log('Received:', data);
switch(data.type) { case 'game_state': updateGameState(data.payload); break; case 'player_joined': handlePlayerJoined(data.player); break; case 'error': handleError(data.message); break; } };
socket.onclose = function(event) { console.log('WSS Connection closed:', event.code, event.reason);
if (event.code !== 1000) { setTimeout(reconnect, 3000); } };
socket.onerror = function(error) { console.error('WSS Error:', error); };
function sendMessage(type, payload) { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type, payload })); } else { console.warn('WebSocket is not open'); } }
function reconnect() { console.log('Attempting to reconnect...'); socket = new WebSocket(wsUrl); }
|
Cocos Creator 客户端示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| const WebSocketManager = cc.Class({ extends: cc.Component,
statics: { instance: null },
onLoad() { WebSocketManager.instance = this; this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; },
connect(url) { const wssUrl = url.replace('ws://', 'wss://');
this.ws = new WebSocket(wssUrl); this.ws.binaryType = 'arraybuffer';
this.ws.onopen = this.onOpen.bind(this); this.ws.onmessage = this.onMessage.bind(this); this.ws.onclose = this.onClose.bind(this); this.ws.onerror = this.onError.bind(this); },
onOpen(event) { console.log('WSS Connected'); this.reconnectAttempts = 0; this.emit('connected'); },
onMessage(event) { const data = JSON.parse(event.data); this.emit('message', data); },
onClose(event) { console.log('WSS Disconnected:', event.code); this.emit('disconnected');
if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); setTimeout(() => this.connect(this.url), delay); } },
onError(error) { console.error('WSS Error:', error); this.emit('error', error); },
send(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } } });
|
踩坑记录
坑 1:WebSocket 连接失败
症状: 客户端无法连接到 wss://
排查步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| sudo nginx -t
sudo systemctl status nginx
sudo netstat -tlnp | grep 443
sudo iptables -L | grep 443
curl -i -N \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Host: api.yourgame.com" \ -H "Origin: https://yourgame.com" \ https://api.yourgame.com/wss
|
坑 2:SSL 证书错误
症状: 浏览器提示证书不安全
解决方案:
1 2 3 4 5 6 7 8 9 10 11 12
| openssl x509 -in /etc/nginx/ssl/yourdomain.pem -noout -dates
openssl x509 -in /etc/nginx/ssl/yourdomain.pem -noout -subject -issuer
openssl verify -CAfile /etc/nginx/ssl/ca_bundle.pem /etc/nginx/ssl/yourdomain.pem
sudo certbot renew --force-renewal sudo systemctl reload nginx
|
坑 3:连接一段时间后断开
症状: WebSocket 连接几分钟后自动断开
原因: 中间件(防火墙/负载均衡器)超时
解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| server { location /wss { proxy_read_timeout 86400s; proxy_send_timeout 86400s;
proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
proxy_buffering off; proxy_cache off; } }
|
1 2 3 4 5 6 7 8
| function startHeartbeat() { setInterval(() => { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'ping' })); } }, 30000); }
|
坑 4:502 Bad Gateway
症状: Nginx 返回 502 错误
排查:
1 2 3 4 5 6 7 8 9
| curl http://localhost:38080/health
sudo tail -f /var/log/nginx/wss_error.log
sudo getsebool -a | grep httpd_can_network_connect sudo setsebool -P httpd_can_network_connect 1
|
性能监控
Nginx 状态监控
1 2 3 4 5 6 7 8 9 10 11
| server { listen 80; server_name localhost;
location /nginx_status { stub_status on; allow 127.0.0.1; deny all; } }
|
输出示例:
1 2 3 4
| Active connections: 291 server accepts handled requests 16630948 16630948 31070465 Reading: 6 Writing: 125 Waiting: 160
|
WebSocket 连接数监控
1 2 3 4 5 6 7 8
| sudo netstat -an | grep :443 | grep ESTABLISHED | wc -l
sudo netstat -an | grep :443 | grep ESTABLISHED | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
watch -n 1 'netstat -an | grep :443 | grep ESTABLISHED | wc -l'
|
最后
Nginx 配置 HTTPS 和 WSS 的几个关键点:
- 必须用 WSS:HTTPS 网站里的 WebSocket 必须使用 wss:// 协议
- 核心配置:proxy_http_version 1.1 + Upgrade + Connection “upgrade”
- 超时设置:根据业务场景调整,长连接需要设置较长的 proxy_read_timeout
- 性能优化:启用 SSL 会话缓存、HTTP/2、OCSP Stapling
- 监控告警:关注连接数、错误率、响应时间等指标
有问题欢迎留言交流。