前两天晚上七点多,我坐在家里用 SSH 连回公司内网服务器,敲一条 ls 命令等了差不多三秒才回显。切到远程桌面更绝望,画面像被马赛克糊过一样,拖一下窗口要半天才刷新。白天一切正常,一过六点就开始卡,每天如此,已经被折磨了一个星期。我用的就是 FRP 内网穿透,服务器端在云上,客户端在公司内网,链路本身并不复杂,问题出在晚高峰的公网链路上。
后来我把 FRP 的传输层协议从默认的 TCP 切到了 KCP,卡顿问题基本解决,SSH 和远程桌面都恢复了可用状态。加上新版本 FRP 全面转向 TOML 格式配置,网上很多教程还停留在旧版 ini 写法,照着做直接启动失败,踩了不少坑。这篇文章把整个排查过程、KCP 协议的原理、完整的 TOML 配置流程以及开启 KCP 后的新坑都梳理一遍,希望能帮到同样被内网穿透卡顿折磨的人。
1. 晚高峰卡顿到底卡在哪:TCP 在拥塞链路里有多吃亏
1.1 白天正常晚上卡,这个时间规律本身就是答案
排查网络问题,第一件事是看现象的时间规律。我的 FRP 链路搭建好之后,白天几乎感觉不到延迟,SSH 敲命令跟本机操作差不多,RDP 远程桌面也能正常办公。但一到晚上 6 点到 10 点这个时间段,延迟明显上升,丢包开始出现,远程桌面基本不可用。
这个规律直接指向公网链路拥塞。晚高峰是家庭宽带和移动网络使用量最大的时段,视频、游戏、直播都在抢占带宽,运营商骨干网和跨网互联的链路会出现明显的拥塞。FRP 服务端部署在云服务器上,家里的网络和服务器的网络往往不是同一个运营商,跨网互访在晚高峰更是重灾区。
还有一个容易忽略的因素是云服务器本身的带宽。我用的服务器是 5M 带宽,白天够用,晚上一旦出现丢包,TCP 的有效吞吐会急剧下降,5M 变成不到 1M 都有可能。
1.2 TCP 面对拥塞时的表现:老实排队,越堵越慢
理解这个问题需要回到 TCP 协议本身的特性。TCP 是面向连接的可靠传输协议,为了保证数据不丢失、不乱序,它设计了一套拥塞控制机制。当网络出现丢包时,TCP 会认为网络发生了拥塞,然后主动降低发送速率,进入拥塞避免阶段,并且每次超时重传都会让超时时间加倍,从 1 秒到 2 秒、4 秒、8 秒指数增长。
用大白话说,TCP 就像一辆在高速上遇到堵车就自动减速的大巴,它非常守规矩,为了不加剧拥堵宁愿把自己降到很慢的速度。在晚高峰这种本来就拥堵的链路上,TCP 的守规矩反而成了致命伤——丢包率稍微上升,它就把自己的发送窗口缩得很小,然后花了大量时间在等待超时重传上。
FRP 默认走的就是 TCP 协议,所以晚高峰卡顿的根源就是传输层协议在拥塞链路上的天然劣势。我做了个简单的丢包测试,使用 ping 命令连续丢 100 个包,白天的丢包率在 1% 以内,晚高峰直接到了 5% 到 8%。对于 TCP 来说,这个丢包率已经非常致命,有效吞吐量可能只剩原来的三分之一到四分之一。
1.3 远程桌面和 SSH 为什么感知最明显
不同应用对延迟和丢包的敏感度不同。SSH 是交互式协议,每一次按键都要经过完整的往返确认,网络延迟直接从按键到屏幕的反应时间体现出来。RDP 远程桌面更是吃带宽和延迟的怪兽,它需要持续传输屏幕变化的数据,一旦传输速率跟不上,画面就会降级、模糊、刷新缓慢。
这两个场景恰恰是我使用 FRP 最频繁的两个。如果你只是在内网穿透基础上跑一些文件传输或者 Web 服务,晚高峰卡顿的感知可能没有那么强,因为 HTTP 请求没有严格的实时性要求,文件传输慢一点顶多多等一会儿。但交互式应用不行,卡顿就是没法用。
1.4 排查思路:先确认是不是加密和压缩导致的计算瓶颈
在把锅甩给 TCP 之前,我还排查过另一个可能的原因——FRP 自身的加密和压缩功能。FRP 支持在传输层配置 useEncryption 和 useCompression,这两个功能会带来额外的 CPU 开销,如果两端设备性能不足,也可能造成延迟。
我检查了服务端和客户端的 CPU 使用率,发现占用率都在 5% 以下,排除了这个可能。真正的瓶颈还是链路上的丢包与延迟。判断方法也很简单:看延迟曲线是否和晚高峰的时间段完全吻合,以及丢包率是否同步上升。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. KCP 协议为什么管用:拿带宽换延迟的低层逻辑
2.1 KCP 是什么:跑在 UDP 上面的可靠传输协议
KCP 是一个基于 UDP 实现的快速可靠传输协议,它的核心思路是在 UDP 之上自己实现可靠传输机制,包括确认、重传、窗口管理,但做得比 TCP 激进得多。它的设计目标很明确——在网络状况比较差、丢包率较高的场景下,降低延迟就是一切。
FRP 从很早的版本开始就内置了 KCP 支持,这不是一个单独的插件,而是在配置里可以直接切换的选项。服务端开启 kcpBindPort 之后,客户端把 transport.protocol 设置为 kcp,FRP 就会把所有传输数据切换到基于 KCP 的 UDP 通道上。
为什么 KCP 能在高丢包环境下比 TCP 表现更好?核心在于它对重传和确认的处理策略完全不同。
2.2 KCP 与 TCP 在丢包时的行为差异
我把关键差异整理成一个表格,方便对比:
| 对比维度 | TCP | KCP |
|---|---|---|
| 重传策略 | 超时时间指数退避,1s → 2s → 4s... | RTO 相对固定,且更激进,不指数退避 |
| 丢包确认 | 依赖 ACK 超时或重复确认 | 快速重传 + 选择性确认,精确定位丢失包 |
| 拥塞控制 | 丢包即视为拥塞,主动降低发送带宽 | 默认不做主动退让,用更多带宽换取更低延迟 |
| 有效吞吐 | 丢包 5% 时有效吞吐大幅下降 | 在同样丢包率下保持更高的有效吞吐 |
| 协议基础 | TCP(传输层) | UDP(传输层)+ 自定义可靠性机制 |
这个表格背后是 KCP 对 TCP 拥塞控制哲学的颠覆:TCP 认为丢包就意味着网络拥塞,必须主动降低速率来避免加剧拥塞;KCP 的思路则是只要应用还在等数据,就要尽快把数据送过去,宁可多占用一些带宽,也不能让延迟被指数退避拖垮。
打一个比方:TCP 像一辆遇到堵车就在原地排队耐心等待的公交车,KCP 像一辆会从应急车道往前赶的救护车。在紧急场景下,救护车的行为是合理的,但它确实占用了额外资源,这就是 KCP 的代价。
2.3 KCP 的适用边界:不是所有场景都适合切
KCP 不是万能药,它有非常明确的适合场景和不适合场景。
适合的场景是交互式的、低带宽的、对延迟敏感的应用。SSH 远程登录、RDP 远程桌面、内网 Web 管理面板、数据库管理工具,这些场景的特点是每次传输的数据量不大,但是交互频率高,一次丢包重传的等待时间严重影响使用体验。在这些场景下切 KCP 收益非常明显。
不适合的场景是高带宽的持续传输。比如通过内网穿透传大文件、跑视频流、做数据同步,这些场景本来就需要大量带宽,KCP 没有拥塞控制反而可能让情况更糟。而且很多运营商会针对 UDP 流量做限速或 QoS 降级,UDP 通道持续满负荷传输时,容易被识别并限速。
另外还要注意,KCP 的底层是 UDP,UDP 在某些网络环境(比如部分企业防火墙、部分公共 Wi-Fi)下会被直接屏蔽。如果你要穿透的网络环境对 UDP 不友好,KCP 反而会导致完全无法连接。
3. 从 ini 到 TOML:新版 FRP 配置必须知道的变化
3.1 为什么突然冒出个 TOML 文件
如果你之前配置过 FRP,一定对 frps.ini 和 frpc.ini 这两个文件名不陌生。旧版 FRP 使用 ini 格式作为配置文件,用惯了也挺顺手。但新版 FRP 全面转向了 TOML 格式,文件名变成了 frps.toml 和 frpc.toml。
这不是简单的换了个扩展名,而是配置结构本身发生了变化。TOML 是 Tom's Obvious Minimal Language,它的设计目标就是比 ini 更标准、比 JSON 更易读,支持更复杂的嵌套结构,非常适合表示 FRP 这种包含服务端、客户端、多个代理配置的复杂对象。
新版 FRP 启动时如果检测到旧版 ini 文件,会直接报错,提示 ini 格式已不再支持。我升级完客户端之后,直接复制旧的 frpc.ini 改名成 frpc.toml 启动,结果程序直接拒绝运行,错误信息大意是"ini 配置已不支持,请使用 toml 格式"。这篇其实操作很简单,把配置按照 TOML 语法重新写一遍就行,但是字段结构有变化,完全照搬旧教程是行不通的。
3.2 TOML 配置的核心语法要点
TOML 配置有几条基础规则,理解了它们,看任何 FRP 配置都不会懵。
首先是键值对,格式是 键 = 值,比如 bindPort = 7000。字符串用双引号包裹,数字直接用,布尔值用 true/false,这些和常见的配置文件格式类似。
其次是表,TOML 用 [表名] 表示一个配置块。FRP 客户端配置中的 [transport] 就是传输层的配置表,[[proxies]] 是代理列表,注意是双括号,表示这是一个数组,数组里的每个元素都是独立的一个配置表。
关于注释,TOML 用 # 开头表示注释,这一点和 ini 一致。
下面是一个典型的 FRP 客户端 TOML 配置结构:
toml复制serverAddr = "your_server_ip"
serverPort = 7000
# transport 相关配置
transport.protocol = "kcp"
# 代理列表, 每个代理是一个 [[proxies]] 节点
[[proxies]]
name = "ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 6000
3.3 为什么建议你现在就迁移到 TOML
如果你还在用旧版本 FRP,我的建议是尽快升级并且迁移到 TOML。一方面新版 FRP 修复了大量安全漏洞,特别是认证相关的问题;另一方面新版的功能特性(比如更多的传输协议支持、更好的日志体系)都只在新版本里提供。
迁移的过程不难,把旧配置里的字段按层级填到新格式里,借助 frpc verify 命令校验一遍,基本就能跑通。
4. 服务端 frps.toml 与客户端 frpc.toml 完整配置流程
4.1 服务端配置:开启 KCP 接入端口
开启 KCP 的第一步在服务端。编辑 frps.toml,在基础配置之上添加 kcpBindPort 字段。这个字段定义了 FRP 服务端监听 UDP KCP 流量的端口。
toml复制# frps.toml
bindAddr = "0.0.0.0"
bindPort = 7000
# 开启 KCP 协议监听的 UDP 端口, 可以和 bindPort 相同
kcpBindPort = 7000
# 认证信息, 客户端必须使用相同的 token
auth.method = "token"
auth.token = "your_secure_token_here"
这里有个关键点:bindPort 是 TCP 端口,kcpBindPort 是 UDP 端口,两个端口都设置为 7000 完全没问题。TCP 和 UDP 是不同的协议,可以监听同一个端口号而互不干扰。这个设计非常巧妙,你只需要在防火墙和安全组里放行一个端口号的两类协议,不需要额外开第二个端口。
如果你的服务端不需要走 KCP,可以把 kcpBindPort 留空或者直接删掉这行,客户端自然不会用 KCP 连接。但既然要解决晚高峰卡顿,就一定要把这一行加上。
4.2 客户端配置:切换传输协议为 KCP
服务端配置完成后,接下来是客户端。编辑 frpc.toml,在 transport 部分设置 protocol = "kcp"。这一步是切换传输层协议的关键。
toml复制# frpc.toml
serverAddr = "your_server_ip"
serverPort = 7000
# 核心: 把传输协议切换为 KCP
transport.protocol = "kcp"
# 认证信息, 必须和服务端一致
auth.token = "your_secure_token_here"
# 远程桌面代理 (RDP)
[[proxies]]
name = "rdp"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3389
remotePort = 63389
# 建议开启加密, 内网穿透场景传输的数据可能包含敏感信息
transport.useEncryption = true
# SSH 代理
[[proxies]]
name = "ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 6222
注意,transport.useEncryption 是代理级别的字段,放在 [[proxies]] 里面,和旧版 ini 里的写法类似。传输层协议(transport.protocol)则放在顶层,这两个不要搞混。
4.3 验证配置:先 verify 再启动
新版 FRP 提供了配置校验命令,强烈建议在正式启动前先执行一遍。这个命令不会真正启动服务,只检查配置文件格式和字段是否合法。
服务端校验:
bash复制./frps verify -c frps.toml
客户端校验:
bash复制./frpc verify -c frpc.toml
如果配置正确,会输出类似 frpc: configuration file is valid 的结果。如果配置有问题,会明确指出哪个字段有误,非常方便排查。
4.4 启动服务与开机自启
校验通过后,就可以启动服务了。这里给出一个简单的 systemd 服务配置示例,适合部署在 Linux 云服务器上。
服务端 /etc/systemd/system/frps.service:
ini复制[Unit]
Description=FRP Server
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/frp/frps -c /usr/local/frp/frps.toml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
客户端 /etc/systemd/system/frpc.service 类似,把 ExecStart 换成客户端程序和配置文件路径即可。启动命令:
bash复制systemctl daemon-reload
systemctl enable frps
systemctl start frps
用 systemctl status frps 确认服务运行状态。客户端也一样操作。
4.5 第一次连接:如何确认 KCP 真的生效
配置完成后第一次启动,可以通过日志确认 KCP 是否真正生效。FRP 客户端启动时会打印连接日志,看到类似 start a new connection 的消息,同时服务端日志里会有来自客户端的注册信息。
更直接的方法是在服务端用 ss -u -l 查看 UDP 监听端口,确认 frps 监听在 UDP 7000 端口。然后客户端启动后,用 ss -u -a 查看本机到服务端 UDP 7000 端口的连接状态。如果能看到 ESTABLISHED 状态的 UDP 连接,说明 KCP 通道已经建立成功。
我当时的验证结果:客户端日志显示连接正常,UDP 连接状态是 ESTABLISHED,SSH 的延迟明显下降,远程桌面也流畅了不少。
5. 开启 KCP 后更容易翻车的几个坑
5.1 云服务器安全组和防火墙必须放行 UDP
这是最容易踩的坑,没有之一。我刚开始切换 KCP 时,客户端一直报连接超时,服务端日志里连一个 UDP 包都收不到。排查了半天,发现云服务器安全组只放行了 TCP 7000 端口,UDP 7000 压根没放开。
云服务器的安全组规则和系统防火墙(iptables/firewalld)是两个层面,都要放行。安全组在云平台控制台配置,防火墙在服务器系统内配置。具体操作是:
- 在云平台控制台的"安全组"中添加入方向规则:UDP 端口 7000,来源 0.0.0.0/0(或你的客户端 IP 范围)
- 在服务器系统内执行
firewall-cmd --add-port=7000/udp --permanent(firewalld)或iptables -A INPUT -p udp --dport 7000 -j ACCEPT(iptables)
很多人只配了 TCP 规则,KCP 连接自然建立不起来。这个坑出现的频率极高,一定要检查。
5.2 TCP 和 UDP 端口复用:看似巧妙但也有坑
前面说到 bindPort 和 kcpBindPort 可以设置成同一个值,这是 FRP 的一种巧妙设计。但它带来一个隐患:排查问题的时候容易搞混。
比如你测试端口连通性,习惯性地用 telnet 服务器IP 7000,这个命令走的是 TCP,只能验证 TCP 端口。KCP 走的是 UDP,要用 nc -u -vz 服务器IP 7000 或者在自己电脑上跑一个 UDP 发包工具才能验证。
我建议在维护文档里明确记录:7000/TCP 和 7000/UDP 都需要放行,两者都在服务。很多人只记住了一个端口号 7000,漏掉了 UDP 协议,出问题的时候抓不到头绪。
5.3 运营商对 UDP 的策略差异:有些网络环境反而更糟
切换 KCP 之前,最好先确认你的网络环境对 UDP 是否友好。不同运营商、不同网络场景对 UDP 的策略差异很大。
家庭宽带一般对 UDP 比较宽松,这也是 KCP 能生效的前提。但有些办公网络、企业 Wi-Fi、校园网会限制 UDP 流量,或者做 QoS 降级。在 UDP 被限制的网络环境里,KCP 不仅不会改善卡顿,反而可能导致完全无法连接。
我的建议是先在当前网络环境下做一个基准测试:白天用 TCP 连接跑一次,晚高峰用 KCP 连接跑一次,对比延迟和丢包情况。如果 KCP 的延迟改善明显,那说明你的网络对 UDP 友好;如果没有改善甚至更差,可能需要换回 TCP 或者考虑其他方案。
5.4 心跳超时:NAT 会话老化导致连接断开
FRP 默认有心跳机制,客户端会定期向服务端发送心跳包,保活连接。在切换到 KCP 后,心跳包也走 UDP,而 NAT 设备对 UDP 会话的老化时间通常比 TCP 短。如果心跳间隔设置得太长,NAT 会话可能已经老化,UDP 数据包无法正确路由。
默认配置下这个风险不大,但如果你在较复杂的内网环境(比如多级 NAT、企业路由器)中运行 FRP,建议主动配置更短的心跳间隔:
toml复制transport.heartbeatInterval = 10
transport.heartbeatTimeout = 30
这些配置会让客户端每 10 秒发送一次心跳,30 秒未收到服务端响应则判定连接断开并重连。代价是心跳包会占一点带宽,但换来的是连接稳定性。
5.5 运营商 QoS 后的降级:为什么 UDP 会被限速
还有一类情况比较隐蔽,就是运营商对 UDP 流量的限速。有些运营商会针对 P2P、视频会议等 UDP 流量实施 QoS 策略,当检测到大量 UDP 流量时,会限制该连接的带宽。
我遇到过一次:KCP 开启初期延迟改善明显,但跑了一段大流量传输后,UDP 连接带宽被限制到非常低,SSH 又卡了。后来发现是运营商 QoS 机制在起作用。解决方案有两个:一是避免在高带宽场景下使用 KCP,只让交互式应用走 KCP;二是定期重启连接,让 QoS 策略重新评估。
6. 实测效果与最终调优建议
6.1 不同场景下的实测对比数据
切换 KCP 后,我记录了一组晚高峰的对比数据,供参考:
| 场景 | TCP 协议(晚高峰) | KCP 协议(晚高峰) |
|---|---|---|
| SSH 敲命令延迟 | 2-4 秒,偶尔超时 | 0.3-0.6 秒,无超时 |
| RDP 远程桌面 | 画面模糊,刷新困难 | 基本流畅,个别时刻轻微卡顿 |
| 内网 Web 管理面板 | 加载 8-10 秒 | 加载 2-3 秒 |
| 传输 100MB 文件 | 5-8 分钟 | 4-6 分钟,改善不明显 |
从这个表能看出问题:对于交互式应用,KCP 的改善是质的飞跃;对于大文件传输,KCP 的改善微乎其微,这印证了前面说的适用边界。
6.2 调优参数:带宽限制与传输模式
如果你切换 KCP 后效果不错,还想进一步优化,可以关注几个参数。
toml复制# frps.toml 服务端
# 设置服务端带宽限制,单位 KB/s,0 表示不限制
transport.bandwidthLimit = 0
# frpc.toml 客户端
# 传输模式: standard(标准), multiplex(多路复用)
transport.protocol = "kcp"
transport.bandwidthLimit = 0
对于个人使用场景,建议不要把 bandwidthLimit 设得太小,因为 KCP 本来就是拿带宽换延迟,限得太狠等于自废武功。但如果你和多人共享同一台云服务器,设置合理的带宽上限可以防止某个连接饿死其他服务。
另外一个值得尝试的是开启多路复用(multiplex),让多个代理共享一条物理连接,减少连接建立的开销。考虑到 KCP 本来就建立了一条 UDP 连接,多路复用对减少握手次数有一些帮助。
6.3 什么样的场景应该换回 TCP
这里说句实在话:KCP 不是所有 FRP 场景的银弹。如果你的网络环境本身质量很好,白天晚上都没有明显丢包,那 KCP 带来的改善可以忽略不计,反而因为 UDP 的特性可能引入不确定性。这种情况下,用默认的 TCP 就够了,省心省力。
反之,如果你已经确认晚高峰丢包严重、TCP 延迟明显,而且你的主要应用是 SSH、RDP 这类交互式工具,那么切换 KCP 会是一个性价比极高的优化方案。
6.4 最终配置版本:我目前正在用的方案
最后把我目前正在使用的完整配置贴出来,供直接参考。
服务端 frps.toml:
toml复制bindAddr = "0.0.0.0"
bindPort = 7000
kcpBindPort = 7000
auth.method = "token"
auth.token = "替换成你自己的随机字符串"
transport.heartbeatTimeout = 30
log.to = "/var/log/frps.log"
log.level = "info"
log.maxDays = 7
客户端 frpc.toml:
toml复制serverAddr = "你的服务器IP"
serverPort = 7000
loginFailExit = false
auth.token = "替换成你自己的随机字符串"
transport.protocol = "kcp"
transport.heartbeatInterval = 10
transport.heartbeatTimeout = 30
[[proxies]]
name = "rdp"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3389
remotePort = 63389
transport.useEncryption = true
[[proxies]]
name = "ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 6222
transport.useEncryption = true
经过这一轮排障和配置调整,晚高峰的内网穿透体验已经从"不可用"恢复到了"基本可用"。后面我还会继续观察不同网络环境下的表现,特别是在出差场景下用移动网络访问内网机器的效果。如果你也在为 FRP 晚高峰卡顿发愁,可以先从验证丢包率开始,确认瓶颈在链路质量后,再切 KCP,大概率能少走不少弯路。
