You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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回调无限制地读取数据,而是通过令牌桶来节流。有人给出了极简的实现逻辑:

  1. 初始化令牌桶时,设置好目标速率(比如2MB/s)和桶的缓冲容量(比如最多存500KB的令牌,应对短时间的突发流量)
  2. 用定时器定期填充令牌,比如每50ms填充一次,每次填充的令牌数是总速率的1/20
  3. 在读取回调中,根据当前令牌量决定处理多少数据,令牌不足就暂停读取,等令牌补充后再恢复

给你一个简化的代码示例

// 令牌桶结构体
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 16:57:38