WebSocket 发大文件为什么总失败?长度限制 + 流压缩一次讲透

在做 uni-app 工控平板、前端录屏、查验设备上传时,很多人都会踩同一个坑:

WebSocket 连上了,小数据正常,一传视频 / 高清图片就断。

不是 WebSocket 不可靠,而是你没搞清楚它真正的限制在哪里,以及该用哪种压缩策略。

这篇文章把协议层、浏览器、服务器,再到前端可用的压缩方案,一次性理清楚。

一、先说结论:WebSocket 本身不限长度

WebSocket 协议(RFC 6455)没有规定消息最大长度

它的帧头里,payload length 是:

  • 7 bit → 小包
  • 16 bit → 中等包
  • 63 bit → 超大包

理论上支持:

9,223,372,036,854,775,807 字节
≈ 9 EB(艾字节)

👉 协议层不拦你,拦你的是运行环境。


二、真实世界里,到底被谁限制了?

1️⃣ 浏览器端:1MB 是道坎

实测经验(也是文档里验证过的):

浏览器 常见限制
Chrome / Firefox ~1MB
Safari ~512KB
Edge ~1MB

而且这是隐性限制

  • 没有统一报错
  • 可能直接 close 连接
  • 或者 onerror 只给一个模糊事件

另外还有两个隐藏杀手:

  • 内存压力:大 ArrayBuffer 一申请,老设备(比如工控平板)直接 GC 卡死
  • Blob 转二进制慢:你以为在发,其实主线程已经快炸了

2️⃣ 服务器端:Nginx 是第一个拦路虎

如果你走的是:

Client → Nginx → WS Server

Nginx 默认:

proxy_websocket_buffer_size 1m;

超过就直接断。

其他服务端默认值:

服务端 默认限制
Node.js ws 无硬限(但吃内存)
Spring WebSocket 64KB
Apache LimitRequestBody

👉 很多项目其实是被 Nginx 或 Spring 拦的,不是 WebSocket。


三、正确思路:不要“大消息”,要“流 + 压缩”

核心原则

✅ 分块(chunk)
✅ 压缩(compress)
✅ 流式发送(stream / incremental)

而不是:

ws.send(hugeBlob)

四、前端可用的 3 种压缩方案

方案一:现代浏览器 —— CompressionStream(最优雅)

// 压缩流
const readableStream = new ReadableStream({
  start(controller) {
    controller.enqueue(new TextEncoder().encode('大量数据...'));
    controller.close();
  }
});

const compressedStream = readableStream.pipeThrough(
  new CompressionStream('gzip')
);

接收端:

const decompressedStream = compressedStream.pipeThrough(
  new DecompressionStream('gzip')
);

✅ 优点:

  • 原生 API
  • 不阻塞主线程
  • 语义清晰

❌ 缺点:

  • Chrome 100+ / Edge 100+
  • 工控平板 / WebView 83 不支持(之前的设备就不满足此项)

方案二:老环境兜底 —— pako(工控设备首选)

<script src="https://cdn.jsdelivr.net/npm/pako@2.0.4/dist/pako.min.js"></script>

压缩:

const data = new Uint8Array([...]);
const compressed = pako.deflate(data);

解压:

const restored = pako.inflate(compressed);

转 Base64 给接口 / WS:

const compressedBase64 = btoa(
  String.fromCharCode.apply(null, compressed)
);

✅ 优点:

  • 兼容老 WebView
  • 工控屏、uni-app、小程序 WebView 都能用

❌ 缺点:

  • 同步压缩,数据太大还是会卡

方案三:分块压缩(推荐生产用)

不要一次压缩 10MB,而是:

function* chunkData(data, chunkSize) {
  for (let i = 0; i < data.length; i += chunkSize) {
    yield data.slice(i, i + chunkSize);
  }
}

for (const chunk of chunkData(largeData, 1024 * 1024)) {
  const compressed = pako.deflate(chunk);
  ws.send(compressed);
}

接收端拼接:

[chunk1.gz] → buffer queue → 全部收齐 → 解压

✅ 这是最稳的方案

  • 每个 WS 帧 < 1MB
  • 浏览器不崩
  • Nginx 不拦

五、Node.js 服务端怎么接?

zlib 而不是自己解析:

const WebSocket = require('ws');
const zlib = require('zlib');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  const gunzip = zlib.createGunzip();

  ws.on('message', (message) => {
    gunzip.write(message);
  });

  gunzip.on('data', (decompressed) => {
    // 处理业务数据
  });
});

发送端同理:

const gzip = zlib.createGzip();

gzip.write(JSON.stringify(payload));
gzip.flush(() => {
  ws.send(gzip.read());
});

六、HTTP 场景别忘了:中间件压缩

如果你不是 WebSocket,而是普通上传接口:

app.use(compression({
  level: 6,
  filter: (req, res) => {
    if (req.headers['x-no-compression']) return false;
    return compression.filter(req, res);
  }
}));

👉 对 JSON / 日志 / 文本数据,压缩率非常高。


七、我们工控项目的实际组合方案

结合前面那台 Android 11 + Chrome 83 平板,最终用的是:

  1. 拍照 / 视频 → 前端 Blob
  2. Blob → ArrayBuffer
  3. 1MB 分块
  4. pako.deflate 每块
  5. ws.send(binary)(不用 Base64)
  6. 服务端 zlib.createGunzip() 拼接

效果:

  • 单帧 512KB ~ 800KB
  • 连续 5 分钟传输不中断
  • 内存占用稳定

八、几个容易忽略的细节

✅ WS 优先发 binary,不要 Base64

ws.binaryType = 'arraybuffer';
ws.send(compressedBuffer);

Base64 会:

  • 体积膨胀 33%
  • 占用更多 CPU

✅ 不要依赖 onmessage 的 string

大文件一定用 binaryType,否则 UTF-8 解析直接炸。

✅ Nginx 必须改

proxy_websocket_buffer_size 8m;

或者按你的 chunk 大小调。


九、一句话总结

  • WebSocket 协议不限长度,但运行环境限
  • 浏览器 ≈ 1MB 是安全线
  • 工控 / 老 WebView → pako + 分块
  • 新浏览器 → CompressionStream
  • 服务端 → zlib.createGunzip()

大文件不是不能发,是不能“整块发”。

posted on 2026-08-13 15:44  羽丫头不乖  阅读(9)  评论(0)    收藏  举报