
怎么让发送队列按指定带宽发包,而不是一上来就打满
核心是把“发包”从“立刻发”变成“按时间片调度发”。C++ 没有现成的 traffic_shaper 类,得自己在队列和 socket 之间加一层节流逻辑。关键不是压数据,而是控节奏——用定时器驱动出队,而不是靠写 socket 的返回值判断是否阻塞。
常见错误是直接在 send() 前 sleep:这既不准(系统调度延迟大),又浪费线程(尤其高并发时)。正确做法是用异步 I/O 配合时间轮或最小堆管理待发包的计划时间点。
- 用
std::priority_queue存待发包,排序依据是「最早允许发送时间戳」 - 搭配
epoll(Linux)或kqueue(macOS)监听一个 timerfd,避免忙等 - 每次 timer 触发后,把所有「当前时间 ≥ 计划发送时间」的包批量出队、调用
send() - 如果
send()返回EAGAIN或EWOULDBLOCK,把包重新插回队列,延后重试(别丢弃)
如何计算每个包该安排在什么时间发
流量整形本质是把瞬时突发摊平成恒定速率。假设目标带宽是 10Mbps(即 1.25 MB/s),当前包大小为 1460 字节(典型 TCP MSS),那它至少要和前一个包间隔:1460 / 1250000 ≈ 1.17ms。这个间隔就是「令牌桶」中消耗一个令牌所需时间。
注意单位陷阱:带宽通常给的是 bit/s,但包长是 byte;系统时钟精度也影响下限——clock_gettime(CLOCK_MONOTONIC) 纳秒级可用,但实际调度粒度常是 1–15ms,所以建议最小间隔不低于 2ms,否则调度抖动会明显。
立即学习“C++免费学习笔记(深入)”;
- 用
std::chrono::steady_clock获取当前时间,避免系统时间跳变干扰 - 别用
usleep()或nanosleep()做逐包延迟,它们不保证唤醒准时 - 推荐用
timerfd_settime()设置单次触发的timerfd,精度远高于用户态 sleep - 若用多线程,注意
std::priority_queue非线程安全,出队/入队需加锁或换用无锁结构(如moodycamel::ConcurrentQueue)
为什么不能只靠 setsockopt(SO_SNDBUF) 控制带宽
SO_SNDBUF 只调内核发送缓冲区大小,它影响的是「能攒多少包等发」,不是「多久发一个」。增大它可能让突发更猛,减小它反而导致频繁 EAGAIN 和重试开销。真正的带宽控制必须发生在应用层决策点——也就是决定「此刻能不能发」的位置。
另一个常见误解是以为 TCP_CONGESTION(如 bbr、cubic)能替代流量整形。它们是端到端拥塞控制,响应的是丢包/延迟,对本地出口带宽无约束力。你在千兆网卡上跑 bbr,照样能打满 1Gbps;而流量整形要求无论网络状况如何,都严格 ≤ 10Mbps。
-
SO_SNDBUF设太小:频繁触发send()失败,逻辑复杂化 -
SO_SNDBUF设太大:内存占用高,且掩盖了应用层节流缺失的问题 - 真正可控的入口只有两个:
send()调用时机 + 发送前的数据组织方式
遇到 send() 返回 EAGAIN 后该怎么处理
这不是错误,是正常流控信号。说明内核发送缓冲区满了,但你的队列已经按带宽排好了计划——此时不能丢包,也不能死等,而要把包“退票”,重新算一个稍晚的时间点再排队。
容易踩的坑是直接把包放回队首(导致无限循环尝试),或者用固定延时(比如统一加 10ms),这会让实际速率严重偏离目标。正确做法是基于当前系统时间 + 基础间隔 × 重试次数做指数退避,但上限设为 100ms,避免积压雪崩。
- 记录包原始入队时间,重试时用「当前时间 + max(基础间隔, 上次失败间隔 × 1.5)」作为新计划时间
- 给每个包加重试计数,超 5 次就标记为「异常」并走降级路径(如日志告警、切直连 bypass 整形)
- 务必检查
send()返回值是否等于len:TCP 允许部分写入,未写完的部分必须缓存并等待下次发送
最复杂的点其实是时钟同步与调度延迟的耦合——你算出来该在 100.000ms 发,但 timerfd 实际在 100.012ms 触发,这 12μs 在 10Gbps 下就能多发几百字节。真要严控,得在每次触发后动态微调后续包的计划时间,而不是静态排好就不管了。









