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]]"
相关推荐
程序员清洒6 小时前
Flutter for OpenHarmony:GridView — 网格布局实现
android·前端·学习·flutter·华为
VX:Fegn08956 小时前
计算机毕业设计|基于ssm + vue超市管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
0思必得06 小时前
[Web自动化] 反爬虫
前端·爬虫·python·selenium·自动化
LawrenceLan6 小时前
Flutter 零基础入门(二十六):StatefulWidget 与状态更新 setState
开发语言·前端·flutter·dart
秋秋小事7 小时前
TypeScript 模版字面量与类型操作
前端·typescript
2401_892000527 小时前
Flutter for OpenHarmony 猫咪管家App实战 - 添加提醒实现
前端·javascript·flutter
Yolanda947 小时前
【项目经验】vue h5移动端禁止缩放
前端·javascript·vue.js
广州华水科技9 小时前
单北斗GNSS形变监测一体机在基础设施安全中的应用与技术优势
前端
EndingCoder9 小时前
案例研究:从 JavaScript 迁移到 TypeScript
开发语言·前端·javascript·性能优化·typescript
阿珊和她的猫10 小时前
React 路由:构建单页面应用的导航系统
前端·react.js·状态模式