如何在libuv中读取数据时限制带宽(含HTTP下载客户端场景)
嘿,我来帮你搞定libuv里的HTTP下载限速问题!作为用libuv折腾过不少异步IO场景的开发者,这事儿我门儿清~
在libuv中实现HTTP下载限速的方案
1. 核心思路:用令牌桶算法控制IO带宽
libuv本身并没有内置的限速API,所以得我们自己在上层实现。最常用也最靠谱的方案就是令牌桶算法,原理很简单:
- 我们想象有一个“令牌桶”,每秒会往里面固定加入一定数量的令牌(数量对应你要限制的带宽,比如1MB/s就加
1*1024*1024个令牌) - 每次从socket读取数据前,必须先从桶里拿对应字节数的令牌,拿多少就读多少;如果令牌不够,就暂停读取,等桶里有足够令牌了再继续。
具体到libuv的实现步骤:
- 定义一个令牌桶结构体,存当前令牌数、桶的最大容量、每秒生成的令牌数,还有一个libuv定时器
- 用
uv_timer_init初始化定时器,每隔100ms左右(别太长,不然限速精度差)往桶里补充令牌,注意不要超过桶的最大容量(防止突发流量冲破限速) - 在
uv_read_cb回调里,不要一股脑处理所有读到的数据:先看当前令牌数够不够读这次的数据,取两者的最小值作为允许读取的字节数,处理完后减去对应令牌;如果令牌被耗尽,就调用uv_read_stop()暂停读取,等定时器补充令牌后再调用uv_read_start()恢复。
2. 关于libuv限速方案#738的细节
那个issue里社区讨论的核心其实和上面的思路一致——libuv官方不会内置限速,推荐上层用令牌桶实现。当时有开发者分享了具体的实现框架:
讨论里提到,限速的关键是不要让IO回调无限制地读取数据,而是通过令牌桶来节流。有人给出了极简的实现逻辑:
- 初始化令牌桶时,设置好目标速率(比如2MB/s)和桶的缓冲容量(比如最多存500KB的令牌,应对短时间的突发流量)
- 用定时器定期填充令牌,比如每50ms填充一次,每次填充的令牌数是总速率的1/20
- 在读取回调中,根据当前令牌量决定处理多少数据,令牌不足就暂停读取,等令牌补充后再恢复
给你一个简化的代码示例
// 令牌桶结构体 typedef struct { size_t capacity; // 桶的最大令牌数(比如500*1024) size_t current_tokens; // 当前剩余令牌数 size_t rate_per_sec; // 每秒生成的令牌数(比如1*1024*1024 = 1MB/s) uv_timer_t refill_timer; uv_stream_t* target_stream; // 要限速的stream } SpeedLimiter; // 定时器回调:补充令牌 void refill_tokens(uv_timer_t* timer) { SpeedLimiter* limiter = (SpeedLimiter*)timer->data; // 每100ms补充1/10的令牌 size_t add = limiter->rate_per_sec / 10; limiter->current_tokens = (limiter->current_tokens + add) > limiter->capacity ? limiter->capacity : limiter->current_tokens + add; // 如果之前因为令牌不足暂停了读取,现在恢复 if (limiter->current_tokens > 0) { uv_read_start(limiter->target_stream, alloc_buffer, on_read); } } // 读取回调:根据令牌数控制读取量 void on_read(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { if (nread <= 0) { // 处理错误或连接关闭 uv_buf_free(buf); return; } SpeedLimiter* limiter = (SpeedLimiter*)stream->data; // 最多读取当前令牌数允许的字节数 size_t allowed_size = nread > limiter->current_tokens ? limiter->current_tokens : nread; // 处理这部分数据(比如写入文件) write_to_file(buf->base, allowed_size); // 消耗令牌 limiter->current_tokens -= allowed_size; // 如果令牌耗尽,暂停读取 if (limiter->current_tokens == 0) { uv_read_stop(stream); } uv_buf_free(buf); } // 初始化限速器 void init_speed_limiter(uv_loop_t* loop, SpeedLimiter* limiter, uv_stream_t* stream, size_t rate_per_sec) { limiter->rate_per_sec = rate_per_sec; limiter->capacity = rate_per_sec / 2; // 桶容量设为半秒的令牌数 limiter->current_tokens = limiter->capacity; limiter->target_stream = stream; stream->data = limiter; uv_timer_init(loop, &limiter->refill_timer); limiter->refill_timer.data = limiter; // 每100ms触发一次令牌补充 uv_timer_start(&limiter->refill_timer, refill_tokens, 0, 100); }
几个需要注意的小细节
- 令牌数的单位是字节,所以要把你要限制的带宽(比如MB/s)换算成字节/秒
- 定时器的间隔不要太大,建议50-100ms,这样限速的精度会更高
- 如果是多线程环境下使用,记得给令牌桶的
current_tokens加互斥锁,避免竞态条件 - HTTP下载如果是分块传输的,每个分块的读取都要经过令牌桶的校验,别漏掉任何一个读取回调
内容的提问来源于stack exchange,提问作者Andy Cong
相关产品推荐
相关产品推荐

