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 平板,最终用的是:
- 拍照 / 视频 → 前端
Blob Blob → ArrayBuffer- 按 1MB 分块
pako.deflate每块ws.send(binary)(不用 Base64)- 服务端
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()
大文件不是不能发,是不能“整块发”。
浙公网安备 33010602011771号