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]]"
相关推荐
王哲晓2 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4115 分钟前
无网络安装ionic和运行
前端·npm
理想不理想v6 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云16 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:1379712058718 分钟前
web端手机录音
前端
齐 飞24 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
神仙别闹41 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
GIS程序媛—椰子2 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0012 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试