高丢包、高延迟、断网秒切:开源一个 Android 弱网测试利器

每次都复现不了线上超时?我开源了一个 Android 弱网测试工具(含关键代码)

线上最难追的不是必现问题,而是"偶发网络异常":

同一个接口,有人超时、有人正常;测试说弱网必现,开发却怎么也跑不出来。

我做了一个 Android 弱网工具 KsNetLite,核心目标不是"模拟网络",而是把弱网故障变成可重复实验

仓库:https://github.com/kekegdsz/ksNetLite

这篇只讲技术实现:系统网络通道怎么承载弱网仿真、参数如何实时生效、限速算法怎么落地。

1. 整体架构:控制面与数据面解耦

我把系统拆成两层:

  • 控制面MainActivity + OverlayService
    • 负责场景切换(2G/3G/断网)、参数调节(丢包/延迟/抖动/带宽)
    • 参数写入 ProfileStoreStateFlow + 持久化)
  • 数据面KsNetNetworkService + RuleEngine + TrafficShaper
    • 负责每个数据包的"丢不丢、等多久、发不发"

这样做的好处是:UI 怎么改都不会影响流量处理链路,稳定性更高。

2. 入口为什么选系统网络通道?

弱网模拟最关键是"可控流量入口"。这里通过系统提供的网络通道能力建立 TUN,将 IPv4 流量统一导入仿真链路:

kotlin 复制代码
val builder = Builder()
    .setSession("KsNetLite")
    .addAddress("10.10.0.2", 24)
    .addDnsServer("8.8.8.8")
    .addRoute("0.0.0.0", 0)

tunInterface = builder.establish() ?: return

addRoute("0.0.0.0", 0) 的意义是把默认路由导进虚拟网卡,后续就能在用户态做延迟、抖动、丢包、限速等网络仿真规则。

3. 包处理主循环:先判丢包,再叠加延迟/限速

核心循环非常直接:读包 -> 规则判定 -> 延迟 -> 回写。

kotlin 复制代码
while (isActive) {
    val len = input.read(buffer)
    if (len <= 0) continue

    val currentProfile = ProfileStore.profile.value
    if (ruleEngine.shouldDropPacket(currentProfile)) {
        StatsStore.onPacketDropped()
        continue
    }

    val networkDelayMs = ruleEngine.computeDelayMs(currentProfile)
    val shapeDelayMs = trafficShaper.computeRequiredDelayMs(len, currentProfile)
    val totalDelayMs = networkDelayMs + shapeDelayMs
    if (totalDelayMs > 0) delay(totalDelayMs)

    output.write(buffer, 0, len)
    StatsStore.onPacketForwarded(len)
}

这里有两个关键点:

  • 丢包优先于延迟:被丢弃的包不参与后续等待,更符合真实弱网下"直接丢失"的语义。
  • 时延模型可叠加:基础网络时延 + 带宽整形引入的排队等待,能覆盖更多真实抖动场景。

4. 丢包与抖动:用最小模型先解决 80% 复现问题

RuleEngine 没做复杂马尔可夫链,先用概率模型保证可解释、可调试:

kotlin 复制代码
fun shouldDropPacket(profile: NetworkProfile): Boolean {
    if (profile.packetLossPercent <= 0) return false
    return random.nextInt(100) < profile.packetLossPercent
}

fun computeDelayMs(profile: NetworkProfile): Long {
    val jitter = if (profile.jitterMs == 0) 0 else random.nextInt(profile.jitterMs + 1)
    return (profile.latencyMs + jitter).toLong()
}

这个实现足够覆盖:

  • 高丢包导致的重试/超时
  • 延迟 + 抖动导致的响应漂移
  • 场景切换时的行为差异(比如 4G -> 差网)

5. 限速实现:令牌桶控制吞吐,避免"粗暴 sleep"

带宽整形我用的是令牌桶思路。核心逻辑:

  • 按时间补充 token
  • token 足够则立即放行
  • token 不足则计算需要等待的毫秒数
kotlin 复制代码
fun computeRequiredDelayMs(packetSizeBytes: Int, profile: NetworkProfile): Long {
    val bandwidthBytesPerSec = profile.bandwidthKbps * 1024.0 / 8.0
    if (bandwidthBytesPerSec <= 0.0) return 0

    refillTokens(bandwidthBytesPerSec)

    val burstLimit = max(bandwidthBytesPerSec, 8_192.0)
    tokensBytes = min(tokensBytes, burstLimit)

    if (tokensBytes >= packetSizeBytes) {
        tokensBytes -= packetSizeBytes
        return 0
    }

    val missingBytes = packetSizeBytes - tokensBytes
    tokensBytes = 0.0
    val seconds = missingBytes / bandwidthBytesPerSec
    return (seconds * 1000).toLong().coerceAtLeast(1L)
}

相比固定 sleep,这种方式在"突发流量 + 平均带宽约束"之间平衡更自然,也更接近真实链路。

6. 参数热更新:StateFlow 让控制台和处理循环同步

参数状态单独放在 ProfileStore,处理循环每次取当前值,天然支持运行中动态切换:

kotlin 复制代码
private val _profile = MutableStateFlow(NetworkProfile())
val profile: StateFlow<NetworkProfile> = _profile.asStateFlow()

fun update(profile: NetworkProfile) {
    _profile.value = profile
}

同时做了本地持久化,保证重启后仍能恢复上次测试配置。

7. 浮窗设计:把"切网动作成本"压到最低

OverlayService 本质是测试效率工具:把高频动作做成按钮,不再反复进设置页。

例如"预设场景 + 确保服务已运行"的触发链路:

kotlin 复制代码
fourG.setOnClickListener {
    resetClickTimer()
    applyProfile(PresetProfile.FOUR_G.profile)
    ensureServiceRunning()
}
threeG.setOnClickListener {
    resetClickTimer()
    applyProfile(PresetProfile.THREE_G.profile)
    ensureServiceRunning()
}

这块看似是 UI,实际上决定了工具是否"真能被天天用"。

8. 这套实现解决了什么问题?

对我自己最有价值的是三件事:

  • 把"偶发网络问题"变成可重复实验
  • 把"调环境"从分钟级降低到秒级
  • 把"测试口径"统一成可共享的配置参数

也就是说,它不是一个"网络黑魔法工具",而是一个让客户端网络问题可工程化验证的基础设施。

9. 后续规划

  • 指定 App 生效(白名单,避免全局影响)
  • 场景脚本化(按时间轴自动切换)
  • 更细粒度统计(按场景聚合耗时、失败率)

GitHub:https://github.com/kekegdsz/ksNetLite

相关推荐
StarShip2 小时前
JVM堆栈溢出监测原理
android·java
Ehtan_Zheng3 小时前
ArrayDeque 是 Kotlin 开发者工具箱中一个被低估的集合类
android
嗷o嗷o3 小时前
Android BLE 扫描连接与收发消息实战
android
古法安卓3 小时前
Android-LowmemoryKiller机制
android·后端·android studio
kerli3 小时前
Compose 组件:BoxWithConstraints作用及其原理
android·前端
努力学习的小廉3 小时前
Python 零基础入门——基础语法(二)
android·开发语言·python
北漂Zachary3 小时前
Laravel 7.x 新特性全解析
android
我命由我123453 小时前
Android Jetpack Compose - 组件分类:布局组件、交互组件、文本组件
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
BLUcoding3 小时前
Android 底部导航栏(TabHost + TabWidget)实现方案
android