• 正文
  • 相关推荐
申请入驻 产业图谱

Nginx | HTTP 反向代理:WebSocket 实时双向数据传输

01/07 10:26
578
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是 WeiyiGeek,一名深耕安全运维开发(SecOpsDev)领域的技术从业者,致力于探索DevOps与安全的融合(DevSecOps),自动化运维工具开发与实践,企业网络安全防护,欢迎各位道友一起学习交流、一起进步 ,若此文对你有帮助,一定记得点个关注⭐与小红星❤️或加入到作者知识星球『 全栈工程师修炼指南』,转发收藏学习不迷路  。

Nginx 反向代理 Websocket 浅析与实践

描述:前面详细介绍了在 Nginx 中 HTTP 反向代理的配置与实践,以及如何反向代理 Memcached NoSQL 数据库,接下来我们将探讨如何在 Nginx 中实现 WebSocket 的反向代理,不过在此之前,我们先来简单了解下 WebSocket 协议,让大家知其然知其所以然。

温馨提示:若文章代码块中存在乱码或不能复制,请联系作者,也可通过文末的阅读原文链接,加入知识星球中阅读,原文链接:https://articles.zsxq.com/id_7wc0af7viwqr.html

Websocket 介绍

WebSocket 提供了一种在单个 TCP 连接上进行全双工通信的方法,这对于实时应用(如聊天室、在线游戏等)非常有用,是目前广泛使用的一种 Web 通讯方式。它相比较于 HTTP 协议而言,可主动推送数据,无需客户端轮询服务器,从而及时的向用户端反映服务变化,WebSocket 具有以下特点:

    持久连接:连接建立后无需重复握手,数据以帧的形式传输。低延迟:相比 HTTP 长轮询,WebSocket 减少了数据传输延迟。高效性:传输数据量更小,适合高频实时通信。

另外,目前大家所熟知的开发语言如 Java、Python、Node.js、Go 等,都提供了对 WebSocket 的原生支持,开发成本低,易于上手,其次在 JS 中使用 WebSocket 也是非常方便的。

Websocket 协议帧的基本结构如下图所示,作者也简单介绍一下:

FIN 位用于表示这是当前帧的最后一部分。当 FIN 设置为1时,表示这是一个完整的消息;如果为0,则表示还有更多的数据片段需要接收。

RSV1、RSV2 和 RSV3 保留位,用于自定义扩展,值为0时表示不使用这些扩展,否则为非 0 值。

Opcode 用于表示帧的类型,例如文本、二进制数据或连接关闭等,若接受到未知的 opcode,则会关闭连接。

      • 0x0:表示一个继续帧(Continuation Frame),用于传输分段消息的中间部分。0x1:表示一个文本帧(Text Frame),用于传输文本数据。0x2:表示一个二进制帧(Binary Frame),用于传输二进制数据。0x3 到 0x7:保留的 opcode,目前未定义,为以后非控制帧保留。0x8:表示一个连接关闭帧(Connection Close Frame),用于请求或响应关闭 WebSocket 连接。0x9:表示一个 Ping 帧,用于检测连接是否仍然活跃。0xA:表示一个 Pong 帧,用于响应 Ping 帧。0xB 到 0xF:保留的 opcode,目前未定义,为以后的控制帧保留。

MASK 掩码位,表示帧中数据是否经过加密,客户端发出的数据帧需经过掩码处理,当值为1时掩码键(masking-key)的数据为掩码密钥,用于解码 PayloadData 数据,否则为0,不进行掩码处理。

Payload length 表示负载长度,即数据的字节数,有 7 bits(最大128) 或 7 bits + 16 bits 或 7 bits + 64 bits 的形式表示。

      • 如果值为 0 到 125,则直接表示负载长度。例如,Payload length 为 13 表示 PayloadData 的长度是 13 个字节。如果值为 126,则后续有两个字节表示负载长度。如果值为 127,则后续有八个字节来表示实际的负载长度。

Extended Payload Length 表示扩展负载长度,用于表示当 Payload length 为 126 或 127 时实际的负载长度。例如,如果 Payload length 为 126,则 Extended Payload Length 会占用两个字节来表示实际的数据长度,127 则为8个字节。

Masking-key 表示掩码密钥,占用 0 或 4 个字节,当 MASK 掩码位为1时使用,用于解码 PayloadData 数据。如果 MASK 为0,则此字段不存在。

Payload Data 表示负载数据,即实际传输的数据内容。如果设置了 MASK 掩码位为1,则需要对 PayloadData 进行解码处理。

weiyigeek.top-Websocket 协议帧图

知识扩展:前面说到 Websocket 的优点,这里再提一下其缺点:

数据分片是有序的,前一个帧包与后一个帧包之间是有顺序的,不能随意打乱,所以没办法支持多路复用,但可用通过 A Multiplexing Extensions for WebSocket 插件实现。

协议原生不支持压缩,但是可通过插件实现,如 Compression Extensions for WebSocket (RFC 7692)。

Websocket 反向代理指令

Websocket 反向代理同样是由 ngx_http_proxy_module 模块提供,其配置方式与 HTTP 反向代理类似,但需要特别注意以下所需配置:

# 设置 HTTP 版本为 1.1
prxoy_http_version 1.1;  

# 设置 WebSocket 协议头
proxy_set_header Upgrade $http_upgrade;

# 将 connection 头设置为 "upgrade" 以支持 WebSocket 的升级握手
proxy_set_header Connection "upgrade";

weiyigeek.top-Websocket请求响应头部示例图

再来看看,WebSocket 相关的请求响应给客户端的扩展头部,如下所示:

Sec-WebSocket-Version: 客户端发送要使用的 WebSocket 协议版本,例如 13 (RFC 6455),若服务器不支持此版本,则必须回应自己支持的版本。

Sec-WebSocket-Extensions: 客户端发送用于协商本次连接要使用的 Websocket 扩展,例如压缩、分片等。

Sec-WebSocket-Key: 客户端发送生成的密钥,用于验证服务器,以验证服务器支持请求的协议版本。

Sec-WebSocket-Protocol: 用于协商应用子协议,客户端发送支持的协议列表,服务器必须只回应一个协议名,例如 chat、superchat 等。

Sec-WebSocket-Accept: 服务器生成的响应密钥,包含 Sec-WebSocket-Key 的签名值,用于验证客户端的请求。

实践演示

步餪 01. 首先,我们需要一个 websocket 环境,可以使用 Python (3.11) 的 websocket 库来创建一个简单的 WebSocket 服务端,即客户端发送什么就返回什么,如下所示:

# 安装必要的依赖(若不存在则安装)
pip install websockets -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com

# 创建一个 WebSocket 服务器
tee -a websocket_server.py <<EOF
import asyncio
import logging
from datetime import datetime
import websockets
from websockets.server import serve

# 设置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

async def echo_handler(websocket):
    """
    处理 WebSocket 连接,将客户端发送的消息原样返回
    """
    # 获取客户端地址
    client_address = websocket.remote_address
    logger.info(f"客户端连接成功: {client_address}")

    try:
        async for message in websocket:
            # 记录接收到的消息
            logger.info(f"收到来自 {client_address} 的消息: {message}")

            # 记录发送时间
            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]

            # 构建响应消息(原样返回客户端发送的内容)
            response = f"[{timestamp}] 服务端消息: {message}"

            # 发送响应
            await websocket.send(response)
            logger.info(f"发送响应到 {client_address}: {response}")

    except websockets.exceptions.ConnectionClosed as e:
        logger.info(f"客户端断开连接: {client_address}, 原因: {e}")
    except Exception as e:
        logger.error(f"处理客户端 {client_address} 时发生错误: {e}")
        await websocket.close(code=1011, reason=str(e))

async def main():
    """
    启动 WebSocket 服务器
    """
    host = "0.0.0.0"# 监听地址
    port = 8765         # 监听端口

    logger.info(f"启动 WebSocket 服务器在 ws://{host}:{port}")
    logger.info("按 Ctrl+C 停止服务器")

    # 启动服务器
    async with serve(
        echo_handler,
        host,
        port,
        ping_interval=20,     # 每20秒发送一次ping
        ping_timeout=20,      # ping超时时间
        close_timeout=10,     # 关闭超时时间
    ) as server:
        # 保持服务器运行
        await server.wait_closed()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        logger.info("服务器已停止")
EOF

# 启动 WebSocket 服务器
$ python3 websocket_server.py
INFO:__main__:启动 WebSocket 服务器在 ws://0.0.0.0:8765
INFO:__main__:按 Ctrl+C 停止服务器
INFO:websockets.server:server listening on 0.0.0.0:8765

步骤 02.在 Linux 服务器上安装 websocat 工具进行测试 websocket 服务是否正常,如下所示:

wget https://github.com/vi/websocat/releases/download/v1.14.0/websocat.x86_64-unknown-linux-musl
mv websocat.x86_64-unknown-linux-musl /usr/local/bin/websocat
chmod +x /usr/local/bin/websocat
websocat ws://127.0.0.1:8765

weiyigeek.top-测试websocket 服务图

步骤 03. 然后,配置 Nginx 作为 WebSocket 的反向代理服务器,与此同时采用前面学习实践过的 SSL 配置(参考文章:Nginx | 核心知识150讲之SSL证书签发与HTTPS加密传输学习实践笔记),使之支持 ws 与 wss 两种协议,如下所示:

tee /usr/local/nginx/conf.d/server.conf <<'EOF'
# 后端 WebSocket 服务地址
upstream websocket_backend {
  server 10.20.172.214:8765; 
}

server {
  listen 80;
# 监听 443 端口,启用 SSL 
  listen 443 ssl;
# 虚拟主机服务器名称
  server_name server.weiyigeek.top;
  default_type text/html;

# 开起 HTTP/2 支持
  http2 on;

# 日志文件
  access_log /var/log/nginx/server.log main;
  error_log /var/log/nginx/server.err.log debug;

# SSL 证书文件
  ssl_certificate /usr/local/nginx/certs/server.crt;
# ssl_certificate_key /usr/local/nginx/certs/server.key;

# 加密的 SSL 证书密钥文件(根据需求选择)
  ssl_certificate_key /usr/local/nginx/certs/server_encrypted.key;
  ssl_password_file /usr/local/nginx/certs/ssl_password.txt;

# 支持的 SSL/TLS 协议版本
  ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;

# 支持的 SSL/TLS 加密套件
  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE:ECDH:AES:HIGH:EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:!NULL:!aNULL:!eNULL:!EXPORT:!PSK:!ADH:!DH:!DES:!MD5:!RC4;

# SSL 会话缓存
  ssl_session_cache shared:SSL:10m;
# SSL 会话超时时间
  ssl_session_timeout 10m;
# 优先使用服务器端支持的加密套件
  ssl_prefer_server_ciphers on;

# 强制使用 HTTPS 访问
  add_header Strict-Transport-Security "max-age=31536000;includeSubDomains;preload" always;

# Websocket 反向代理配置
  location ^~ /ws {
    proxy_pass http://websocket_backend; # 转发到后端服务
    proxy_http_version 1.1; # 使用 HTTP/1.1 协议
    proxy_set_header Upgrade $http_upgrade; # 升级协议头
    proxy_set_header Connection "upgrade"; # 保持连接
    proxy_set_header Host $host;
    proxy_read_timeout 60s; # 设置超时时间
    proxy_set_header X-Real-IP $remote_addr; # 转发客户端 IP 地址
    proxy_buffer_size 128k; # 缓冲区
    proxy_buffers 4 256k;
    proxy_busy_buffers_size 256k;
  }
}
EOF

 

步骤 04. 验证配置、重启 Nginx 服务,并使用 websocat 工具验证反向代理,命令如下所示:

# 重载
nginx -t && nginx -s reload

# 测试反向代理
# 非 SSL 连接
websocat ws://server.weiyigeek.top/ws
# SSL 连接
websocat wss://server.weiyigeek.top/ws

weiyigeek.top-使用 websocat 工具验证反向代理图

知识扩展:另外,还可使用 javascript 连接到我们部署的 websocket ,如下所示:

// Connect to the WebSocket echo server
const socket = new WebSocket('wss://server.weiyigeek.top/ws');

// Connection opened
socket.addEventListener('open', (event) => {
console.log('Connected to echo server');

// Send a test message
  socket.send('Hello, WebSocket Echo Server!');

// Send JSON data
  socket.send(
    JSON.stringify({
      type: 'test',
      timestamp: Date.now(),
      message: 'Testing echo functionality',
    })
  );
});

// Listen for echoed messages
socket.addEventListener('message', (event) => {
console.log('Echoed back:', event.data);

// Parse JSON if needed
try {
    const data = JSON.parse(event.data);
    console.log('Received JSON:', data);
  } catch (e) {
    console.log('Received text:', event.data);
  }
});

// Handle errors
socket.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
});

// Connection closed
socket.addEventListener('close', (event) => {
console.log('Disconnected from echo server');
console.log('Close code:', event.code);
console.log('Close reason:', event.reason);
});

好了,上面我们介绍 Websocket 相关知识,并演示了如何在 Linux 服务器上使用 Python3 搭建一个简易的 Websocket 服务,并通过 Nginx 进行反向代理,最后使用 websocat 工具进行测试。

加入:作者【全栈工程师修炼指南】知识星球

相关推荐