Nginx配置websocket反向代理

1,545 阅读4分钟

一、 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小程序用于测试

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
       }
    }
}

关键配置

#传递请求头Upgrade和Connection
proxy_set_header Upgrade $xxx;
proxy_set_header Connection $xxx;

即只需要配置UpgradeConnection即可达到目的!

最终方案:

只需在http块添加如下两个配置直接从来源获取请求头的值,无需其他改动即可原地支持ws且不影响其他http

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;

1. nginx官方方案

nginx完整配置

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都响应正常

WebSocket connection opened.
json fetch test:  {ok: true, code: 1, msg: 'success'}
Received message: serv echo:first massage

后端日志,正常响应并收到消息

2023/12/20 11:36:57 Received message: first massage

2. nginx官方方案小改进

nginx完整配置

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完整配置

worker_processes 1;
events {
    worker_connections 1024;
}
http {
    include mime.types;
    default_type application/octet-stream; #默认MIME
    sendfile on; #允许发送文件
    keepalive_timeout 95; 
    proxy_set_header Host $host:$server_port; #设置请求host:port为客户原始值
    proxy_set_header X-Real-IP $remote_addr; #设置客户原始IP
    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; #代理时向后端的http协议
    #直接使用下面这两个代理请求头设置也可以达到升级连接的效果且不会影响到非websocket的请求
    proxy_set_header Upgrade $http_upgrade; #升级websocket必备
    proxy_set_header Connection $http_connection; #升级websocket必备
    server {
        listen 80;
        #全部请求,不必为websocket的path单独配置location
        location ^~ / {
            proxy_pass http://127.0.0.1:8080;
        }
    }
    #gzip配置
    gzip on; #开启响应客户端时gzip压缩
    gzip_comp_level 6; #gzip压缩级别
    gzip_min_length 512; #触发gzip压缩的最小报文长度
    gzip_disable msie6; #不进行压缩的MIME类型,多个用英文逗号隔开
}

测试效果

与官方方案一致,且后端非websocket请求不会收到请求头upgrade,同时conncation也为keep-alive。

后端日志如下:

/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]]"