第7章 7.3 流的背压与暂停恢复
📖 本文是系列文章「Node.js 从入门到精通」的第 33 章,点击查看完整目录
🎯 开场 3 分钟:为什么要学这个?
想象一下这个场景:
你家楼下有个快递柜,取件速度是每秒 5 个,但快递员投放速度是每秒 20 个。结果会怎样?
没错,快递柜会爆满,快递员要么停止投放,要么把快递堆在地上乱成一团。
这就是背压(Backpressure)问题。
上一章我们学了 Stream 的基本用法,知道怎么用 pipe 把数据从 A 传到 B。但现实世界有个残酷的真相:数据的生产速度和消费速度往往不匹配。
- 你从网络下载一个大文件,想同时解压它 → 下载速度 > 解压速度,内存爆了
- 你从数据库读取 1000 万条数据,想同时写入文件 → 读的速度 > 写的速度,内存爆了
- 你用
pipe管道连接多个流 → 某个环节卡住了,整条链都在堆积数据
这一章我们要解决的核心问题就是:当数据来得太快、来不及处理时,怎么办?
学完本文,你就能:
- 理解什么是「背压」,为什么它会拖垮你的程序
- 用 highWaterMark 控制缓冲区大小
- 用 pause() / resume() 让数据流「可快可慢」
- 用异步迭代器优雅地消费流数据
🧱 基础 25 分钟:核心概念(小白视角)
7.3.1 什么是背压?先别急着看定义
生活类比 🎯
想象你在厨房洗碗,水龙头哗哗地开着,下面水池塞子堵住了但没完全堵死,水流得很慢。
如果你不管,继续让水龙头开最大,结果会怎样?水会溢出来,洒一地。
聪明人的做法:先把水龙头关小一点(减慢生产),等水池里的水排掉一些,再把水龙头开大(加快生产)。
这就是背压——当下游处理不过来时,上游要「喘口气」,别一股脑儿往下灌。
7.3.2 highWaterMark:快递柜的容量上限
Node.js 的 Stream 给每个流都设置了一个「缓冲区容量上限」,叫 highWaterMark。
是什么:一个数字,表示缓冲区里最多能缓存多少数据(单位是字节或对象数量,取决于流类型)。
为什么要用:防止数据无限堆积,把服务器内存撑爆。你可以理解为快递柜的总格子数。
怎么用:
const { Readable, Writable } = require('stream');
// 创建一个读取流,缓冲区最多放 16KB 数据(默认 16KB)
const readable = Readable({
highWaterMark: 16 * 1024 // 16KB
});
// 创建一个写入流,缓冲区最多放 8KB 数据
const writable = Writable({
highWaterMark: 8 * 1024 // 8KB
});
console.log('读取流容量:', readable.readableHighWaterMark); // 16384
console.log('写入流容量:', writable.writableHighWaterMark); // 8192
这行在干嘛:创建两个流,并打印它们各自的缓冲区容量上限。
7.3.3 pause() / resume():让数据流「踩刹车」
生活类比 🎯
你在自动售货机前买东西,投币后商品掉出来。如果你接商品的速度太慢,售货机就会「卡住」,等你拿走一个,它才掉下一个。
pause() 就是告诉上游:「慢点发,我还没处理完」
resume() 就是告诉上游:「好了,可以继续发了」
是什么:Readable 流的方法,用来暂停和恢复数据发射。
为什么要用:当你处理数据的速度跟不上数据到来的速度时,需要手动控制节奏。
怎么用:
const { Readable } = require('stream');
// 创建一个自定义的读取流,每 100ms 发送一个数字
const myStream = new Readable({
highWaterMark: 2, // 缓冲区只能放 2 个数字,很小!
read() {
let count = 0;
const interval = setInterval(() => {
count++;
console.log(`发送数据: ${count}`);
this.push(count); // 往缓冲区放数据
if (count >= 10) {
clearInterval(interval);
this.push(null); // 告诉消费者:我没数据了
}
}, 100);
}
});
// 模拟慢速消费者:每处理一个数据要 300ms
myStream.on('data', (chunk) => {
console.log(`开始处理: ${chunk},预计耗时 300ms`);
// 300ms 后才处理完,此时缓冲区已经在堆积数据了
setTimeout(() => {
console.log(`处理完成: ${chunk}`);
}, 300);
});
myStream.on('end', () => {
console.log('数据流结束');
});
这行在干嘛:创建一个每秒发送 10 个数据的流,模拟「生产快、消费慢」的场景。运行后你会看到数据堆积,缓冲区很快达到上限(2 个)。
7.3.4 手动实现背压控制
生活类比 🎯
回到洗碗的例子。聪明人会观察水池里的水:快满了就关水龙头,快排空了就开大。
代码来了:
const { Readable, Writable } = require('stream');
// 创建一个慢速消费者(每秒只能处理 3 个数据)
const slowConsumer = new Writable({
objectMode: true,
highWaterMark: 2, // 写缓冲区只能放 2 个对象
write(chunk, encoding, callback) {
console.log(`处理数据: ${chunk}`);
// 模拟耗时操作(300ms)
setTimeout(() => {
console.log(`处理完成: ${chunk}`);
callback(); // 告诉上游:我处理完了,可以发下一个
}, 300);
}
});
// 创建一个数据源(每秒发送 10 个数据)
const dataSource = new Readable({
objectMode: true,
highWaterMark: 2,
read() {
let count = 0;
const interval = setInterval(() => {
count++;
this.push(count);
if (count >= 20) {
clearInterval(interval);
this.push(null);
}
}, 100);
}
});
// 手动实现背压控制
let canContinue = true;
dataSource.on('data', (chunk) => {
if (!canContinue) {
// 缓冲区满了,暂停读取
dataSource.pause();
console.log('--- 暂停读取,等待消费 ---');
return;
}
const needToPause = !slowConsumer.write(chunk); // 写入,返回 false 表示缓冲区满了
if (needToPause) {
canContinue = false;
dataSource.pause();
console.log('--- 缓冲区满,暂停读取 ---');
// 等 drain 事件再恢复
slowConsumer.once('drain', () => {
console.log('--- 缓冲区已清空,恢复读取 ---');
canContinue = true;
dataSource.resume();
});
}
});
dataSource.on('end', () => {
console.log('数据源发送完毕');
});
关键点解释:
writable.write(chunk)返回true表示「我还能继续接收」,返回false表示「我缓冲区满了,你等等」- 当返回
false时,调用readable.pause()暂停上游 - 当缓冲区清空时,会触发
drain事件,此时调用readable.resume()恢复上游
7.3.5 异步迭代器:更优雅的消费方式
生活类比 🎯
传统的 on('data') 方式像是在餐厅门口等位,服务员喊「下一位」你才能进去,容易造成排队混乱。
异步迭代器方式像是取号排队,叫到你的号你再进去,有序且不浪费资源。
是什么:ES2018 引入的 for await...of 语法,可以直接遍历 Stream。
为什么要用:代码更简洁,而且天然支持背压——每迭代一次才处理一个数据块。
怎么用:
const { Readable } = require('stream');
// 创建一个数据流,每 200ms 发送一个数字
const myStream = new Readable({
objectMode: true,
highWaterMark: 2,
read() {
let count = 0;
const interval = setInterval(() => {
count++;
this.push(count);
if (count >= 10) {
clearInterval(interval);
this.push(null);
}
}, 200);
}
});
async function consumeStream() {
console.log('开始异步迭代消费...');
// 关键:for await...of 自动处理背压
for await (const chunk of myStream) {
console.log(`收到数据: ${chunk},开始处理...`);
await new Promise(resolve => setTimeout(resolve, 500)); // 模拟耗时 500ms
console.log(`处理完成: ${chunk}`);
}
console.log('消费完毕');
}
consumeStream();
这行在干嘛:用 for await...of 异步迭代消费流数据,每 500ms 才处理一个,而数据是每 200ms 来一个,会自然产生背压。
🔥 实战 35 分钟:3 个递进的小项目
📦 项目 1:实现一个带背压控制的文件复制器(5 分钟)
需求:把一个大文件复制到另一个位置,要求手动实现背压,不让内存爆掉。
完整代码:
const fs = require('fs');
const { Readable, Writable } = require('stream');
// 源文件和目标文件(用自己的任意一个大文件测试)
const sourceFile = 'source.txt';
const targetFile = 'target.txt';
// 先创建测试文件(10000 行文本)
const testContent = '这是第 ${i} 行测试数据,用于演示背压控制\n'.repeat(10000);
fs.writeFileSync(sourceFile, testContent);
console.log('测试文件已创建,共 10000 行');
// 创建读取流
const readable = fs.createReadStream(sourceFile, {
highWaterMark: 64 * 1024 // 64KB 缓冲区
});
// 创建写入流
const writable = fs.createWriteStream(targetFile, {
highWaterMark: 32 * 1024 // 32KB 缓冲区
});
let totalBytes = 0;
let canContinue = true;
readable.on('data', (chunk) => {
if (!canContinue) {
readable.pause();
return;
}
const bytesWritten = writable.write(chunk);
totalBytes += chunk.length;
if (!bytesWritten) {
canContinue = false;
readable.pause();
writable.once('drain', () => {
canContinue = true;
readable.resume();
});
}
});
readable.on('end', () => {
writable.end(() => {
console.log(`复制完成,共写入 ${totalBytes} 字节`);
console.log(`源文件大小: ${fs.statSync(sourceFile).size} 字节`);
console.log(`目标文件大小: ${fs.statSync(targetFile).size} 字节`);
});
});
预期输出:
测试文件已创建,共 10000 行
复制完成,共写入 420000 字节
源文件大小: 420000 字节
目标文件大小: 420000 字节
一句话解释:手动监听 write() 的返回值,当返回 false 时暂停读取,等 drain 事件恢复,实现背压控制。
📦 项目 2:实时处理 CSV 数据流(15 分钟)
需求:从 CSV 文件读取用户数据(姓名、年龄、分数),实时计算平均分,并写入结果文件。要求用背压控制内存使用。
准备一个 CSV 文件 students.csv:
name,age,score
张三,18,85
李四,20,92
王五,19,78
赵六,21,88
钱七,17,95
孙八,20,81
周九,18,90
吴十,19,86
完整代码:
const fs = require('fs');
const { Readable, Writable } = require('stream');
// ============ 第一部分:创建 CSV 解析流 ============
class CSVParser extends Readable {
constructor(options) {
super({ objectMode: true, ...options });
this.buffer = '';
}
_read() {
// 模拟每 100ms 发送一批数据
const chunk = `name,age,score\n张三,18,85\n李四,20,92\n王五,19,78\n赵六,21,88\n钱七,17,95\n孙八,20,81\n周九,18,90\n吴十,19,86`;
const lines = chunk.split('\n');
let index = 0;
const sendLine = () => {
if (index >= lines.length) {
this.push(null);
return;
}
const line = lines[index++];
const isSent = this.push(line); // 返回 false 时会暂停
if (!isSent) {
// 缓冲区满了,等一会儿再发
setTimeout(sendLine, 50);
} else {
// 正常发送下一行
setImmediate(sendLine);
}
};
sendLine();
}
}
// ============ 第二部分:数据处理器 ============
class DataProcessor extends Writable {
constructor(options) {
super({ objectMode: true, ...options });
this.students = [];
this.sum = 0;
this.count = 0;
}
_write(chunk, encoding, callback) {
const line = chunk.toString().trim();
if (!line || line === 'name,age,score') {
// 跳过表头和空行
callback();
return;
}
const [name, age, score] = line.split(',');
this.students.push({ name, age: parseInt(age), score: parseInt(score) });
this.sum += parseInt(score);
this.count++;
console.log(`处理学生: ${name}, 年龄: ${age}, 分数: ${score}`);
console.log(`当前平均分: ${(this.sum / this.count).toFixed(2)}`);
callback();
}
}
// ============ 第三部分:结果写入器 ============
const resultStream = new Writable({
objectMode: true,
highWaterMark: 2,
write(chunk, encoding, callback) {
const result = chunk;
fs.appendFileSync('result.txt', result + '\n');
console.log(`写入结果: ${result}`);
callback();
}
});
// ============ 第四部分:组装背压管道 ============
const csvParser = new CSVParser({ highWaterMark: 2 });
const processor = new DataProcessor({ highWaterMark: 2 });
// 清空结果文件
fs.writeFileSync('result.txt', '=== 学生数据处理结果 ===\n');
let canContinue = true;
csvParser.on('data', (line) => {
if (!canContinue) {
csvParser.pause();
return;
}
const isWritten = processor.write(line);
if (!isWritten) {
canContinue = false;
csvParser.pause();
processor.once('drain', () => {
canContinue = true;
csvParser.resume();
});
}
});
// processor 处理完的数据写入文件
processor.on('finish', () => {
const avg = processor.sum / processor.count;
const summary = `班级平均分: ${avg.toFixed(2)}`;
resultStream.write(summary, () => {
resultStream.end();
console.log('\n处理完成!结果已写入 result.txt');
});
});
csvParser.on('end', () => {
processor.end();
});
预期输出:
处理学生: 张三, 年龄: 18, 分数: 85
当前平均分: 85.00
处理学生: 李四, 年龄: 20, 分数: 92
当前平均分: 88.50
处理学生: 王五, 年龄: 19, 分数: 78
当前平均分: 85.00
...
班级平均分: 86.88
处理完成!结果已写入 result.txt
一句话解释:CSV 解析流每行数据都要经过处理器计算,处理器写入文件时如果缓冲区满,会通过背压机制暂停解析流。
📦 项目 3:做一个「网站日志实时分析器」(15 分钟)
需求:实时分析网站访问日志,统计每个小时的访问量、热门页面、错误率。模拟 1000 条日志数据流。
完整代码:
const { Readable, Writable } = require('stream');
// ============ 第一部分:日志生成器 ============
class LogGenerator extends Readable {
constructor(options) {
super({ objectMode: true, ...options });
this.count = 0;
this.total = 1000;
}
_read() {
if (this.count >= this.total) {
this.push(null);
return;
}
// 生成随机日志
const hour = String(Math.floor(Math.random() * 24)).padStart(2, '0');
const minute = String(Math.floor(Math.random() * 60)).padStart(2, '0');
const second = String(Math.floor(Math.random() * 60)).padStart(2, '0');
const time = `${hour}:${minute}:${second}`;
const pages = ['/home', '/about', '/product', '/contact', '/api/data'];
const page = pages[Math.floor(Math.random() * pages.length)];
const statusCodes = [200, 200, 200, 200, 404, 500]; // 80% 成功率
const status = statusCodes[Math.floor(Math.random() * statusCodes.length)];
const ip = `192.168.${Math.floor(Math.random()*255)}.${Math.floor(Math.random()*255)}`;
const log = `[${time}] "${ip}" "${page}" ${status}`;
this.count++;
// 控制发送速度,模拟真实场景
const delay = Math.random() * 10; // 0-10ms 随机延迟
setTimeout(() => {
this.push(log);
}, delay);
}
}
// ============ 第二部分:日志分析器 ============
class LogAnalyzer extends Writable {
constructor(options) {
super({ objectMode: true, ...options });
this.hourlyStats = {};
this.pageStats = {};
this.errorCount = 0;
this.totalCount = 0;
}
_write(log, encoding, callback) {
this.totalCount++;
// 解析日志
const match = log.match(/\[(\d+):\d+:\d+\] "[\d.]+" "([^"]+)" (\d+)/);
if (!match) {
callback();
return;
}
const hour = match[1];
const page = match[2];
const status = parseInt(match[3]);
// 统计每小时访问量
this.hourlyStats[hour] = (this.hourlyStats[hour] || 0) + 1;
// 统计页面访问量
this.pageStats[page] = (this.pageStats[page] || 0) + 1;
// 统计错误
if (status >= 400) {
this.errorCount++;
}
// 每 100 条输出一次进度
if (this.totalCount % 100 === 0) {
console.log(`[进度] 已处理 ${this.totalCount} 条日志,错误率 ${(this.errorCount/this.totalCount*100).toFixed(1)}%`);
}
callback();
}
getReport() {
const topPages = Object.entries(this.pageStats)
.sort((a, b) => b[1] - a[1])
.slice(0, 3);
return {
total: this.totalCount,
errorRate: (this.errorCount / this.totalCount * 100).toFixed(2),
hourlyStats: this.hourlyStats,
topPages: topPages
};
}
}
// ============ 第三部分:带背压的数据管道 ============
const logGen = new LogGenerator({ highWaterMark: 10 });
const analyzer = new LogAnalyzer({ highWaterMark: 5 });
let paused = false;
let pendingData = null;
logGen.on('data', (log) => {
if (paused) {
// 缓冲区满了,先存着,等 drain 再处理
pendingData = log;
logGen.pause();
return;
}
const canContinue = analyzer.write(log);
if (!canContinue) {
paused = true;
logGen.pause();
analyzer.once('drain', () => {
paused = false;
if (pendingData) {
const data = pendingData;
pendingData = null;
logGen.resume();
analyzer.write(data);
} else {
logGen.resume();
}
});
}
});
analyzer.on('finish', () => {
const report = analyzer.getReport();
console.log('\n========== 日志分析报告 ==========');
console.log(`总访问量: ${report.total}`);
console.log(`错误率: ${report.errorRate}%`);
console.log('各小时访问量:', report.hourlyStats);
console.log('热门页面 TOP3:', report.topPages);
console.log('===================================');
});
logGen.on('end', () => {
analyzer.end();
});
console.log('开始生成并分析 1000 条日志...');
预期输出:
开始生成并分析 1000 条日志...
[进度] 已处理 100 条日志,错误率 18.0%
[进度] 已处理 200 条日志,错误率 16.5%
[进度] 已处理 300 条日志,错误率 17.3%
...
========== 日志分析报告 ==========
总访问量: 1000
错误率: 16.80%
各小时访问量: { '02': 45, '08': 38, '14': 52, ... }
热门页面 TOP3: [ [ '/home', 245 ], [ '/api/data', 203 ], [ '/product', 188 ] ]
===================================
一句话解释:日志生成器快速产生数据,分析器缓慢处理,通过背压机制协调速度差,保证数据不丢失、内存不爆。
💪 进阶 20 分钟:常见坑 + 性能小贴士
❌ 坑 1:忘记调用 callback() 或 push(null)
// ❌ 错误:write 方法里忘了调用 callback
_writ(chunk, encoding, callback) {
processData(chunk);
// 忘记 callback() 了!
// 这会导致流永远卡住,不继续接收数据
}
// ✅ 正确:一定要调用 callback
_writ(chunk, encoding, callback) {
processData(chunk);
callback(); // 处理完毕,通知上游可以发下一个了
}
❌ 坑 2:highWaterMark 设置过大或过小
// ❌ 错误:设置太小,频繁触发背压,性能差
const slowStream = new Readable({
highWaterMark: 1 // 只能缓存 1 个数据,几乎没有缓冲作用
});
// ❌ 错误:设置太大,内存占用过高
const hugeStream = new Readable({
highWaterMark: 1024 * 1024 * 1024 // 1GB!小心内存爆炸
});
// ✅ 正确:根据实际场景设置合理值
const normalStream = new Readable({
highWaterMark: 16 * 1024 // 16KB,适合大多数场景
});
❌ 坑 3:在 for await...of 里忘记处理异步
// ❌ 错误:用了同步的 setTimeout,不起作用
for await (const chunk of myStream) {
setTimeout(() => console.log(chunk), 1000); // 这不会等 1 秒!
// 循环会立即继续,不会暂停
}
// ✅ 正确:用 await 包装 Promise
for await (const chunk of myStream) {
await new Promise(resolve => setTimeout(resolve, 1000)); // 这里会真的暂停
console.log(chunk);
}
❌ 坑 4:混用 pipe() 和手动背压控制
// ❌ 错误:同时使用 pipe 和手动 pause/resume
source.pipe(dest);
source.pause(); // 混用会导致不可预期的行为
// ✅ 正确:二选一
// 方式 1:用 pipe(自动背压)
source.pipe(dest);
// 方式 2:手动控制背压
source.on('data', (chunk) => {
if (!dest.write(chunk)) {
source.pause();
dest.once('drain', () => source.resume());
}
});
❌ 坑 5:忘记处理 drain 事件
// ❌ 错误:没有监听 drain,直接 resume
readable.on('data', (chunk) => {
if (!writable.write(chunk)) {
readable.pause();
// 没有等 drain 就 resume,等于没暂停
readable.resume();
}
});
// ✅ 正确:一定要等 drain 事件
readable.on('data', (chunk) => {
if (!writable.write(chunk)) {
readable.pause();
writable.once('drain', () => readable.resume()); // 等缓冲区清了再恢复
}
});
💡 性能小贴士:对象模式 vs 字节模式
// 对象模式:highWaterMark 是对象数量
const objectStream = new Readable({
objectMode: true,
highWaterMark: 100 // 最多缓存 100 个对象
});
// 字节模式:highWaterMark 是字节数(更节省内存)
const byteStream = new Readable({
objectMode: false, // 默认是字节模式
highWaterMark: 64 * 1024 // 64KB
});
// 如果处理的是字符串/Buffer 数据,用字节模式更高效
// 如果处理的是 JavaScript 对象,用对象模式更方便
🔍 调试技巧:用 inspect 查看流状态
const { Readable } = require('stream');
const myStream = new Readable({
objectMode: true,
highWaterMark: 3
});
console.log('流状态:', {
readableHighWaterMark: myStream.readableHighWaterMark,
readableLength: myStream.readableLength, // 当前缓冲区有多少数据
readableFlowing: myStream.readableFlowing
});
// 发几个数据看看变化
myStream.push(1);
myStream.push(2);
console.log('发送 2 个数据后:', {
readableLength: myStream.readableLength
});
myStream.push(3);
console.log('发送 3 个数据后:', {
readableLength: myStream.readableLength
});
myStream.push(4); // 超过 highWaterMark,看看会怎样
console.log('发送 4 个数据后:', {
readableLength: myStream.readableLength
});
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):调整缓冲区大小
- 输入:把项目 1 的 highWaterMark 从 64 * 1024 改成 1024
- 预期输出:程序仍然正常运行,但会触发更多次 drain 事件
- 提示:观察控制台输出的 drain 次数变化
练习 2(2 分钟):添加暂停条件
- 输入:在项目 1 的基础上,当 totalBytes > 200000 时暂停 1 秒
- 预期输出:复制过程中会看到「暂停 1 秒」的日志
- 提示:在 data 事件里加一个 if 判断
练习 3(2 分钟):用异步迭代器重写项目 2
- 输入:用 for await...of 替代手动 on('data') 方式
- 预期输出:代码更简洁,功能相同
- 提示:for await...of 自动处理背压,不需要手动 pause/resume
练习 4(3 分钟):统计项目 3 中的错误日志
- 输入:在 LogAnalyzer 中额外统计 404 和 500 分别出现了多少次
- 预期输出:报告里多显示「404 次数: X」和「500 次数: Y」
- 提示:在 LogAnalyzer._write 方法里加判断
练习 5(3 分钟):分析这个报错
- 输入:运行以下代码,观察报错
const { Readable, Writable } = require('stream');
const r = new Readable({ highWaterMark: 2 });
const w = new Writable({ highWaterMark: 1 });
r.push('a');
r.push('b');
r.push('c'); // 这里会发生什么?
r.pipe(w);
- 预期输出:观察
r.readableLength的变化,理解缓冲区溢出 - 提示:
push()超过highWaterMark时数据不会丢失,只是read()会被暂停
📝 作业题:做一个「实时股票行情处理器」
需求描述:
模拟接收股票实时行情数据(股票代码、价格、成交量),实时计算并输出每个股票的最新均价、总成交额。
功能点:
1. StockGenerator 类:每秒生成 10 条随机股票行情
2. StockProcessor 类:计算每个股票的均价和总成交额
3. StockDisplay 类:每秒输出一次所有股票的当前状态
4. 用背压机制协调三个组件的速度差
加分项:
1. 当某股票价格波动超过 5% 时,高亮显示「⚠️ 剧烈波动」
2. 支持动态添加/删除关注的股票
验收标准:
- 能持续运行 30 秒以上不崩溃
- 内存占用保持稳定(不随时间增长)
- 输出格式清晰,每秒刷新一次
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学到的 3 个核心点:
1. 背压原理:当下游处理不过来时,上游要「踩刹车」暂停发送数据
2. highWaterMark:控制缓冲区容量上限,防止内存爆炸
3. pause/resume + drain:实现背压控制的三板斧
延伸学习资源:
- Node.js 官方文档 - Stream API(英文)
- 《深入浅出 Node.js》- 第 5 章「内存控制」,有 Stream 内存管理的深入讲解
- 视频:What is Backpressure? - YouTube(英文,有动画演示)
🎬 互动钩子:
你在项目中遇到过内存暴涨的问题吗?是用什么方法定位和解决的?评论区聊聊你的经历,老粉优先回复!
下章预告:
学会了背压控制,你的流已经能「快慢自如」了。但光能传数据还不够——下一章我们要解决一个更实际的问题:大文件怎么分片上传和下载?想像一下 5GB 的视频文件,怎么切成小块并行传输,还能断点续传……敬请期待第 7.4 章!

评论(0)