我问她:WebSocket 怎么发大文本,她说:豆子,别一口气塞下去,会噎到的

痛点 · 使用问题

办公室的灯只剩几盏还亮着,像没关完的星星。空调出风口还在呼呼地响,小豆的屏幕上卡着一行发送失败的提示。

"又断了?"小语端着杯冷掉的咖啡走过来,俯身看了一眼,眉毛往上一挑。

"嗯,WebSocket 那边,发大段文本总是超时,用户那边说内容一半都没看到。"

"你是不是......又一股脑塞整篇小说过去了?"她语气轻快,像在讲笑话,但那句话有点像锤子,正好砸在小豆脑壳上。

我不好意思地抓了抓头发:"我想着反正是 WebSocket,就直接传了,没多想......"

小语拿指节轻轻敲了下控制台右侧一行字,翻了个白眼:"豆子,你这叫硬塞。系统都喘不过气来了。"

bufferedAmount: 3.2MB

"你这是在往水管里倒水泥。"她叹了口气,"WebSocket是全双工,不是传送门,发大包会堵。"

起点 · 技术交流

我的第一个反应是:"是不是服务器带宽不够?"

阿辰在会议室里翻着部署日志,头也不抬地说:"不是带宽,是你太冲了。"

"WebSocket 不是适合传数据吗?"

阿辰看了我一眼,语气淡淡:"适合,但你得慢慢来。你有没有试着分片?"

"没......我想着直接发能快一点......"

她没说话,从她的文件夹里调出一个小项目,是她年初写的在线 Markdown 协作编辑器。

"看这个,我当时也是要发大段文档。"她一边说,一边把代码往我这边推了推:

"分片?"我愣了一下。

江暮那时刚从外面拿了瓶水进来,淡淡补了一句:"别一口气塞下去,会噎到的。"


🧩第一招:分片传输

回到工位后,我翻出代码,试着按阿辰说的把文本切成1KB一片,一块一块发过去,接收端也一块块拼回来。

js 复制代码
// 发送端
const largeText = "很长很长的文本...";
const chunkSize = 1024 // 1KB
const totalChunks = Math.ceil(largeText.length / chunkSize)

for (let i = 0; i < totalChunks; i++) {
  const chunk = largeText.slice(i * chunkSize, (i + 1) * chunkSize)
  ws.send(JSON.stringify({ type: 'chunk', index: i, data: chunk }))
}


// 接收端
let receivedData = "";
websocket.onmessage = (event) => {
    receivedData += event.data;
    if (event.data.length < chunkSize) {
        console.log("完整数据接收完成:", receivedData);
    }
};

"然后接收端收完拼起来。"阿辰补了一句,"可以加个 done 标记,也可以等最后一个 chunk 收完自动合并。"

小语凑过来看看,说:"这样能避免一次性发送太大的数据,但是还不够快,如果数据本身太大,比如是几 MB 的 JSON,光是分片可能还不够高效。"


🧵第二招:压缩传输

她递给小豆一份文档,是她之前搞聊天室时写的小工具,使用压缩算法(如gzip、deflate)减少传输体积 ,用了 pako 这个库,可以在发送前把大段文本压缩掉,减少一半以上体积。

js 复制代码
import pako from 'pako';

// 发送端压缩
const compressed = pako.deflate(largeText);
websocket.send(compressed);

// 接收端解压
websocket.onmessage = (event) => {
    const restored = pako.inflate(event.data, { to: 'string' });
    console.log(restored);
};

"你试试看,接收那边再解压回来就好。"

"语宝,你太厉害了。"小豆真心感叹。

"别光嘴上甜,把确认机制也加上。发多少片,收了几片,别发一半丢一半。注意WebSocket单帧默认最大16KB"

于是我加了简单的控制协议,用 JSON 标记 startchunkack,发送每一片的时候都带上编号,接收后再确认拼完整了才算完工。

js 复制代码
import pako from 'pako';

// 配置
const CHUNK_SIZE = 1024 * 16; // 16KB 每个分片大小
const MAX_RETRIES = 3;        // 最大重试次数

class OptimizedWebSocketTransfer {
  constructor(websocket) {
    this.ws = websocket;
    this.pendingChunks = new Map();
    this.retryCounts = new Map();
    this.buffers = new Map();
    
    this.setupHandlers();
  }

  setupHandlers() {
    this.ws.onmessage = (event) => {
      try {
        const packet = JSON.parse(pako.inflate(event.data, { to: 'string' }));
        
        if (packet.type === 'start') {
          this.handleStartPacket(packet);
        } else if (packet.type === 'chunk') {
          this.handleChunkPacket(packet);
        } else if (packet.type === 'ack') {
          this.handleAckPacket(packet);
        }
      } catch (error) {
        console.error('Packet processing error:', error);
      }
    };
  }

  // 发送大文本(自动分片压缩)
  sendLargeData(data, id = Date.now().toString()) {
    return new Promise((resolve, reject) => {
      // 压缩原始数据
      const compressed = pako.deflate(data);
      
      // 创建分片
      const totalChunks = Math.ceil(compressed.length / CHUNK_SIZE);
      this.pendingChunks.set(id, {
        total: totalChunks,
        received: 0,
        chunks: new Array(totalChunks),
        resolve,
        reject
      });

      // 发送开始包
      this.sendPacket({
        type: 'start',
        id,
        total: totalChunks,
        size: compressed.length
      });

      // 发送数据分片
      for (let i = 0; i < totalChunks; i++) {
        const chunk = compressed.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
        this.sendPacket({
          type: 'chunk',
          id,
          index: i,
          data: chunk
        });
      }
    });
  }

  // 处理开始包
  handleStartPacket(packet) {
    this.buffers.set(packet.id, {
      total: packet.total,
      received: 0,
      chunks: new Array(packet.total),
      size: packet.size
    });
    
    // 确认收到开始包
    this.sendAck(packet.id, 'start');
  }

  // 处理数据分片
  handleChunkPacket(packet) {
    const buffer = this.buffers.get(packet.id);
    if (!buffer) return;

    // 存储分片
    buffer.chunks[packet.index] = packet.data;
    buffer.received++;
    
    // 发送确认
    this.sendAck(packet.id, 'chunk', packet.index);

    // 检查是否接收完成
    if (buffer.received === buffer.total) {
      this.assembleData(packet.id);
    }
  }

  // 处理确认包
  handleAckPacket(packet) {
    if (packet.ackType === 'start') {
      console.log(`传输 ${packet.id} 已开始`);
    } else if (packet.ackType === 'chunk') {
      const pending = this.pendingChunks.get(packet.id);
      if (pending) {
        pending.received++;
        
        // 检查是否全部完成
        if (pending.received === pending.total) {
          pending.resolve();
          this.pendingChunks.delete(packet.id);
          this.retryCounts.delete(packet.id);
        }
      }
    }
  }

  // 发送确认
  sendAck(id, ackType, index = null) {
    this.sendPacket({
      type: 'ack',
      id,
      ackType,
      index
    });
  }

  // 组装数据
  assembleData(id) {
    const buffer = this.buffers.get(id);
    if (!buffer) return;

    try {
      // 合并所有分片
      const compressed = new Uint8Array(buffer.size);
      let offset = 0;
      for (const chunk of buffer.chunks) {
        compressed.set(chunk, offset);
        offset += chunk.length;
      }
      
      // 解压数据
      const data = pako.inflate(compressed, { to: 'string' });
      
      // 触发消息事件
      this.ws.dispatchEvent(new MessageEvent('message', {
        data: data
      }));
      
      // 清理
      this.buffers.delete(id);
    } catch (error) {
      console.error('Data assembly failed:', error);
    }
  }

  // 发送数据包(自动压缩)
  sendPacket(packet) {
    try {
      const compressed = pako.deflate(JSON.stringify(packet));
      this.ws.send(compressed);
    } catch (error) {
      console.error('Packet send failed:', error);
    }
  }
}

// 使用示例
const ws = new WebSocket('wss://your-websocket-url');
const transfer = new OptimizedWebSocketTransfer(ws);

// 发送大数据
transfer.sendLargeData(veryLargeText)
  .then(() => console.log('传输完成'))
  .catch(err => console.error('传输失败', err));

// 接收消息
ws.addEventListener('message', (event) => {
  console.log('收到完整消息:', event.data);
});

晚上快十一点,小豆在后台看到最后一条日志:「大文本发送成功,耗时 480ms」。


⛱️第三招:二进制传输

"看起来优化的不错。"我点点头,"那如果是二进制数据呢?比如图片、文件之类的?"

"那可以直接用 Array Buffer。"小语写了一段示例:

js 复制代码
// 发送端
const encoder = new TextEncoder();
const binaryData = encoder.encode(largeText);
websocket.send(binaryData);

// 接收端
websocket.onmessage = (event) => {
    const decoder = new TextDecoder();
    const text = decoder.decode(event.data);
    console.log(text);
};

"文本转成二进制,效率更高。"小语补充道,"而且如果你的服务器支持 WebSocket 的 permessage-deflate 扩展,还可以让协议自动帮你压缩,省掉手动压缩的步骤。"

启用permessage-deflate扩展(服务器需支持),自动压缩帧。

javascript 复制代码
const ws = new WebSocket("ws://example.com", ["permessage-deflate"])

我听得入神,一边记笔记一边点头。

小语站起来收电脑包,路过我椅背时拍了拍:"豆子,今天你没噎着,系统也没呛着。"

窗外的灯光透过百叶窗,落在工位上的键盘边缘。

我点点头,笑着说:"都是你教得好。"

📌 技术回顾 · 小结

在 WebSocket 传输大文本时,可以组合以下策略:

  • 分片传输:将数据按固定大小切片,按序发送。
  • 压缩算法 :用如 pako 进行 deflate 压缩,减轻传输负担。
  • 二进制传输 :用 TextEncoder 编码成 Uint8Array,性能更优。
  • 控制协议 :加上 start / chunk / ack 协议,确保完整性。
相关推荐
游戏开发爱好者81 小时前
iOS重构期调试实战:架构升级中的性能与数据保障策略
websocket·网络协议·tcp/ip·http·网络安全·https·udp
小小小小宇2 小时前
虚拟列表兼容老DOM操作
前端
悦悦子a啊2 小时前
Python之--基本知识
开发语言·前端·python
却道天凉_好个秋3 小时前
音视频学习(三十六):websocket协议总结
websocket·音视频
安全系统学习3 小时前
系统安全之大模型案例分析
前端·安全·web安全·网络安全·xss
涛哥码咖4 小时前
chrome安装AXURE插件后无效
前端·chrome·axure
OEC小胖胖4 小时前
告别 undefined is not a function:TypeScript 前端开发优势与实践指南
前端·javascript·typescript·web
行云&流水4 小时前
Vue3 Lifecycle Hooks
前端·javascript·vue.js
Sally璐璐4 小时前
零基础学HTML和CSS:网页设计入门
前端·css
老虎06274 小时前
JavaWeb(苍穹外卖)--学习笔记04(前端:HTML,CSS,JavaScript)
前端·javascript·css·笔记·学习·html