Nginx 反向代理 WebSocket 服务

前一篇 尝试 FastAPI WebSocket 写简单的聊天应用 刚体验了 WebSocket, HTTP 可以共用下层的 TCP 连接保持网页与服务器的双向连接。先简单作个回顾,也是为了解释后面关于 Nginx 反向代理 WebSocket 作准备,比如说下面的 WebSocket 服务

ws://localhost:8000/ws

当在浏览器中用 JavaScript

var ws = new WebSocket("ws://test.exmple.com:8080/ws");

后,浏览器首先会发送一个 GET /ws 的 HTTP 请求,如下(不相关的 HTTP 头省去)

服务器收到请求后,会回应

客户端与服务器之间依照请求响应中与 WebSocket 相关的头完成了 HTTP 到 WebSocket 协议的升级。

那么通过反向代理的方式访问 ws://localhost:8000/ws 需不需要特别的处理呢?

下面用 Nginx 作反向代理服务器进行测试,为使用配置以 server_name 的方式识别请求进行代理转发,在 /etc/hosts 中添加

127.0.0.1 test.example.com

首先如往常一样只在 nginx.conf 文件中添加

Nginx 服务启动在 8080 端口号,对所有以 test.example.com 域名的访问反向代理到 http://localhost:8000(FastAPI 写的 WebSocket 服务启动在该端口号上)

仍然参照 FastAPI 的 WebSockets 官方文档,写一个 WebSocket 服务

该服务启动在 8000 端口号

现在,在浏览器中用代码访问 ws://test.example.com/ws

用 WireShark 工具捕获网络包,条件用 tcp.port == 8080 || tcp.port = 8000,因为需要同时观察前端与 Nginx, Nginx 与后端之间的通信

观察到如下通信,省去非 WebSocket 相关的 HTTP 头

浏览器发起的

GET /ws HTTP/1.1
Host: test.example.com:8080
Connection: Upgrade
Upgrade: websocket
Origin: http://test.example.com:8080
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: 842MJ6JxeagM78Cp4n5ykw==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

然后是 Nginx 代理发向 FastAPI 的请求

GET /ws HTTP/1.0
Host: localhost:8000
Origin: http://test.example.com:8080
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: 842MJ6JxeagM78Cp4n5ykw==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

HTTP 协议变成了 HTTP/1.0, 请求头方面,其他的头都还在,唯独缺失了 WebSocket 依赖的

Connection: Upgrade
Upgrade: websocket

在 FastAPI 端收到请求也就理所当然的不被认为是 WebSocket 请求,而当作普通的 HTTP 请求,所以回了

HTTP/1.1 404 Not Found
date: Fri, 13 Jun 2025 15:09:13 GMT
server: uvicorn
content-length: 22
content-type: application/json
connection: close

因为 http://test.example.com:8000/ws 找不到

这样也就无法完成 HTTP 到 WebSocket 协议的升级,也可用 wscat(需用 npm --install -g wscat 安装) 来测试 WebSocket 连接

wscat -c ws://test.example.com:8080/ws
error: Unexpected server response: 404

问题就出在 Nginx 反向代理未把 Connection 和 Upgrade 两个请求头转发到后端,不知道 Nginx 的默认行为什么能转发 Sec-WebSocket-xxx 等其他的头,却偏偏要无视 Connection 和 Upgrade 头。

在 Nginx 中要能同时代理 WebSocket 服务的话, 需显式配置转发 Connection 和 Upgrade 请求头,改为

nginx -s reload 重启 Nginx 服务,再次用 wscat 就没问题了

wscat -c ws://test.example.com:8080/ws
Connected (press CTRL+C to quit)

Nginx 能正确的向后端服务转发请求

GET /ws HTTP/1.1
Upgrade: websocket
Connection: upgrade
Host: localhost:8000
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: BAbcsmO25oMa/eU/I0BGYA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

从 Nginx 回过来的响应也没问题

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: KimOgq/mSttogBD9xo+Kai5dHB4=
Sec-WebSocket-Extensions: permessage-deflate
date: Fri, 13 Jun 2025 15:12:47 GMT
server: uvicorn

注意到我们多添加了

proxy_http_version 1.1;

如果去掉这一行又会如何呢? 一样能建立正常的 WebSocket 连接,只是由 Nginx 代理发送到 FastAPI 的请求是

GET /ws HTTP/1.0
Upgrade: websocket
Connection: upgrade
Host: localhost:8000
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: nnV1+D8S+WP50PdV0F+2Gg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

但由 FastAPI 回送的响应是坚定的 HTTP/1.1

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 3YpMC13b4DP98JySgxqDg8K0wwg=
Sec-WebSocket-Extensions: permessage-deflate
date: Fri, 13 Jun 2025 15:56:27 GMT
server: uvicorn

因此最终 WebSocket 客户端收到的依然是 HTTP/1.1, 不影响 WebSocket 的通信。只是不明白为什么都 2025 年了, Ngix 1.27(28 May 2024) 的版本还是默认使用 HTTP/1.0 向后端转发请求。

本文链接 https://yanbin.blog/nginx-reverse-proxy-websocket/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments