Nginx配置websocket反向代理

一、 nginx官方说明

1. WebSocket 代理

WebSocket 代理 (nginx.org)

要将客户端和服务器之间的连接从 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]]"
相关推荐
&白帝&24 分钟前
uniapp中使用picker-view选择时间
前端·uni-app
谢尔登31 分钟前
Babel
前端·react.js·node.js
ling1s31 分钟前
C#基础(13)结构体
前端·c#
卸任38 分钟前
使用高阶组件封装路由拦截逻辑
前端·react.js
lxcw1 小时前
npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED
前端·npm·node.js
秋沐1 小时前
vue中的slot插槽,彻底搞懂及使用
前端·javascript·vue.js
这个需求建议不做1 小时前
vue3打包配置 vite、router、nginx配置
前端·nginx·vue
QGC二次开发1 小时前
Vue3 : Pinia的性质与作用
前端·javascript·vue.js·typescript·前端框架·vue
云草桑1 小时前
逆向工程 反编译 C# net core
前端·c#·反编译·逆向工程
布丁椰奶冻1 小时前
解决使用nvm管理node版本时提示npm下载失败的问题
前端·npm·node.js