Nginx 配置 WebSocket SSL 踩坑记录:HTTPS 与 WSS 代理实战

做实时游戏或者聊天功能的时候,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
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install nginx

# CentOS/RHEL
sudo yum install epel-release
sudo yum install nginx

# 验证安装
nginx -v

获取 SSL 证书

1
2
3
4
5
6
7
8
9
# 方式 1:Let's Encrypt(免费,推荐)
sudo apt-get install certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com

# 方式 2:商业证书
# 把证书文件传到服务器,通常包括:
# - yourdomain.crt(证书)
# - yourdomain.key(私钥)
# - ca_bundle.crt(中间证书)

完整配置示例

基础 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
# /etc/nginx/conf.d/websocket.conf

# 定义 upstream(后端 WebSocket 服务器)
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;
}

# HTTPS + WSS 服务器配置
server {
listen 443 ssl http2;
server_name api.yourgame.com;

# SSL 证书配置
ssl_certificate /etc/nginx/ssl/yourdomain.pem;
ssl_certificate_key /etc/nginx/ssl/yourdomain.key;

# SSL 会话缓存
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

# SSL 协议和加密套件
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;

# HSTS(强制 HTTPS)
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;
}

# WebSocket 代理配置
location /wss {
proxy_pass http://wss_backend;

# 关键:WebSocket 支持
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;
}

# 游戏 API 代理
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;
}
}

# HTTP 重定向到 HTTPS
server {
listen 80;
server_name api.yourgame.com;
return 301 https://$server_name$request_uri;
}

关键配置项详解

WebSocket 核心配置

1
2
3
4
# WebSocket 升级必需的三行
proxy_http_version 1.1; # 必须使用 HTTP/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;

# IP Hash(保证同一客户端连接到同一后端)
# ip_hash;

# 最少连接
# least_conn;

# 健康检查(需要 nginx_upstream_check_module)
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;

# 应用到 WebSocket 路径
location /wss {
limit_conn addr 5; # 每个 IP 最多 5 个并发连接
limit_rate 100k; # 限制带宽 100KB/s

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 证书
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;

# SSL 会话缓存(提升性能)
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# 启用 OCSP Stapling
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;

# 启用 Diffie-Hellman 密钥交换
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
// 连接 WSS(WebSocket Secure)
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
// WebSocketManager.js
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) {
// 使用 wss:// 协议
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
# 1. 检查 Nginx 配置语法
sudo nginx -t

# 2. 检查 Nginx 是否运行
sudo systemctl status nginx

# 3. 检查端口监听
sudo netstat -tlnp | grep 443

# 4. 检查防火墙
sudo iptables -L | grep 443

# 5. 测试 WebSocket 连接
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
# 1. 检查证书有效期
openssl x509 -in /etc/nginx/ssl/yourdomain.pem -noout -dates

# 2. 检查证书与域名匹配
openssl x509 -in /etc/nginx/ssl/yourdomain.pem -noout -subject -issuer

# 3. 验证证书链完整性
openssl verify -CAfile /etc/nginx/ssl/ca_bundle.pem /etc/nginx/ssl/yourdomain.pem

# 4. 使用 Let's Encrypt 重新申请
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; # 24 小时
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); // 30 秒一次心跳
}

坑 4:502 Bad Gateway

症状: Nginx 返回 502 错误

排查:

1
2
3
4
5
6
7
8
9
# 1. 检查后端服务是否运行
curl http://localhost:38080/health

# 2. 检查 Nginx 错误日志
sudo tail -f /var/log/nginx/wss_error.log

# 3. 检查 SELinux(CentOS)
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
# 启用 stub_status
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
# 统计当前 WebSocket 连接数
sudo netstat -an | grep :443 | grep ESTABLISHED | wc -l

# 按 IP 统计连接数
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 的几个关键点:

  1. 必须用 WSS:HTTPS 网站里的 WebSocket 必须使用 wss:// 协议
  2. 核心配置:proxy_http_version 1.1 + Upgrade + Connection “upgrade”
  3. 超时设置:根据业务场景调整,长连接需要设置较长的 proxy_read_timeout
  4. 性能优化:启用 SSL 会话缓存、HTTP/2、OCSP Stapling
  5. 监控告警:关注连接数、错误率、响应时间等指标

有问题欢迎留言交流。