一、 nginx官方说明
1. WebSocket 代理
要将客户端和服务器之间的连接从 HTTP/1.1 转换为 WebSocket, 协议使用 HTTP/1.1 中可用的切换机制。
然而,有一个微妙之处:由于"升级"是逐跳标头,因此它不会从客户端传递到代理服务器。 通过正向代理,客户端可以使用该方法来规避此问题。 但是,这不适用于反向代理, 由于客户端不知道任何代理服务器, 并且需要在代理服务器上进行特殊处理。
从版本 1.3.13 开始, nginx 实现特殊操作模式 这允许在客户端和代理之间设置隧道 服务器(如果代理服务器返回了包含代码的响应) 101(交换协议), 客户端通过"升级"要求协议切换 标头。
如上所述,包括"Upgrade"在内的逐跳标头 和 "Connection" 不会从客户端传递到代理 服务器,因此为了让代理服务器了解客户端的 打算将协议切换到 WebSocket,这些标头必须是 明确传递。
2. map 模块
模块ngx_http_map_module (nginx.org)
二、 实操
准备一个Go编写的web小程序用于测试
go
package main
import (
"github.com/gorilla/websocket"
"log"
"net/http"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
var indexHtml = []byte(`<!DOCTYPE html>
<html>
<head>
<title>WebSocket Test</title>
</head>
<body>
<script>
var ws = new WebSocket("ws://localhost:8080/ws");
ws.onopen = function() {
console.log("WebSocket connection opened.");
setTimeout(()=>ws.send("first massage"),100)
};
ws.onmessage = function(event) {
console.log("Received message: " + event.data);
};
ws.onclose = function() {
console.log("WebSocket connection closed.");
};
ws.onerror = function(event) {
console.log("WebSocket error: " + event.data);
};
fetch("/json").then(r=>r.json()).then(r=>console.log('json fetch test: ',r))
</script>
</body>
</html>`)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write(indexHtml)
})
http.HandleFunc("/json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true,"code":1,"msg":"success"}`))
})
http.HandleFunc("/ws", handleWebSocket)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
var msg = string(p)
log.Println("Received message:", msg)
err = conn.WriteMessage(messageType, []byte("serv echo:"+msg))
if err != nil {
log.Println(err)
return
}
}
}
关键配置
bash
#传递请求头Upgrade和Connection
proxy_set_header Upgrade $xxx;
proxy_set_header Connection $xxx;
1. nginx官方方案
nginx完整配置
ini
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 95;
proxy_set_header Host $host:$server_port;
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 5s;
#这个超时时间将影响websocket空闲连接超时时间
proxy_read_timeout 30s;
proxy_send_timeout 30s;
proxy_http_version 1.1;
#官方方案:对特定url默认升级连接upgrade
map $http_upgrade $connection_upgrade {
#默认升级连接
default upgrade;
#其他情况都cloce,这会导致keep-alive无法正常传递keep-alive
'' close;
}
server {
listen 80;
#需要升级为websocket的url前缀
#map的开销九牛一毛,可以不必为websocket单独配置,但官方的map会导致keep-alive无法正常传递到后端
location ^~ /ws {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
#常规请求
location ^~ / {
proxy_pass http://127.0.0.1:8080;
}
}
}
测试效果
前端日志,html和websocket都响应正常
yaml
WebSocket connection opened.
json fetch test: {ok: true, code: 1, msg: 'success'}
Received message: serv echo:first massage
后端日志,正常响应并收到消息
sql
2023/12/20 11:36:57 Received message: first massage
2. nginx官方方案小改进
nginx完整配置
ini
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 95;
proxy_set_header Host $host:$server_port;
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 5s;
#这个超时时间将影响websocket空闲连接超时时间
proxy_read_timeout 30s;
proxy_send_timeout 30s;
proxy_http_version 1.1;
#官方方案升级版本
map $http_upgrade $connection_upgrade {
#map也支持变量值为参数,这里直接让默认值为原址,可以保持keep-alive
default $http_connection;
#当upgrade为websocket时,设置$connection_upgrade值为字符串upgrade,但无法为其他协议升级,可以在下面添加其他协议的升级
'websocket' upgrade;
}
server {
listen 80;
#需要升级为websocket的url前缀
#map的开销九牛一毛,可以不必为websocket单独配置,但此map配置仅支持升级websocket
location ^~ /ws {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
#常规请求
location ^~ / {
proxy_pass http://127.0.0.1:8080;
}
}
}
测试效果
与官方方案一致
3. 直接使用变量的最简方案
nginx完整配置
ini
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 95;
proxy_set_header Host $host:$server_port;
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 5s;
#这个超时时间将影响websocket空闲连接超时时间
proxy_read_timeout 30s;
proxy_send_timeout 30s;
proxy_http_version 1.1;
#直接使用下面这两个代理请求头设置也可以达到升级连接的效果且不会影响到非websocket的请求
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
server {
listen 80;
#全部请求,不必为websocket的path单独配置location
location ^~ / {
proxy_pass http://127.0.0.1:8080;
}
}
}
测试效果
与官方方案一致,且后端非websocket请求不会收到请求头upgrade,同时conncation也为keep-alive。
ini
/json request header=
"map[Accept:[*/*] Accept-Encoding:[gzip, deflate, br] Accept-Language:[zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6] Connection:[keep-alive] Referer:[http://127.0.0.1/] Sec-Ch-Ua:[\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Microsoft Edge\";v=\"12
0\"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:[\"Windows\"] Sec-Fetch-Dest:[empty] Sec-Fetch-Mode:[cors] Sec-Fetch-Site:[same-origin] User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0] X-Forwarded-For:[127.0.0.1] X
-Forwarded-Proto:[http] X-Real-Ip:[127.0.0.1]]"