
专栏FRP 内网穿透从入门到实战前置阅读已完成正篇部署与 webServer 仪表盘配置第二篇、第四篇。本篇目标解决边缘网关临时远程运维中的两个工程难题——动态端口获取与空闲自动断开。前言在视频物联网平台中大量设备通过边缘网关以长连接方式注册上线。运维人员远程调试网关 Web 管理页时平台会向边缘网关下发指令由网关侧 frpc临时开启TCP 穿透把内网 80 端口暴露到公网 frps。这和正篇里「一台设备固定映射一个 remotePort」的场景不同有两个现实约束端口多且临时每台网关、每次远程会话都可能新建代理不能给每个 client 预分配固定端口用完即关远程运维结束或操作者离开穿透隧道不能一直挂着否则公网端口长期暴露。本篇方案源自畅联云平台边缘网关远程运维的生产实践用 Python 示例说明如何通过frps webServer API解决上述两个问题。一、业务架构平台下发网关执行整体链路如下云平台 ──长连接──▶ 边缘网关frpc │ │ │ ① 下发 openRemoteConfig │ ② remotePort0 动态建代理 │ │ └──── ③ webServer API ───┼──▶ frps 查询实际端口 │ ④ 定时任务轮询流量 ──────┘ ⑤ 空闲 15 分钟 → 下发 closeRemoteConfig关键约定项目说明代理命名{网关序列号}-80如GW20240001-80动态端口下发remotePort 0由 frps 从端口池自动分配端口查询GET /api/proxies/{proxyName}读取conf.remotePort活跃检测每 2 分钟拉取GET /api/proxy/tcp对比todayTrafficIn空闲阈值入站流量连续 15 分钟无变化判定为空闲并关闭远程配置二、frps 侧前置配置动态端口分配需要在 frps 开启 webServer并预留端口范围# frps.toml bindPort 7000 auth.method token auth.token YourStrongToken # 仪表盘 / API平台内网调用勿对公网裸奔 webServer.addr 0.0.0.0 webServer.port 7500 webServer.user admin webServer.password YourDashboardPass # 动态端口池remotePort0 时从此范围分配 allowPorts [ { start 20000, end 30000 } ] log.to /var/log/frps.log log.level info边缘网关侧 frpc 通过平台指令运行时创建代理无需写死在 toml 里核心参数示意{clientID:GW20240001,serverAddr:frps公网IP,serverPort:7000,authToken:YourStrongToken,proxyName:GW20240001-80,remotePort:0,localIP:127.0.0.1,localPort:80}remotePort 0是本篇第一个问题的关键不指定固定端口交给 frps 动态分配。三、问题一获取动态分配的 remotePort3.1 为什么需要反查端口代理创建是异步的平台下发指令后frpc 连接 frps 并完成注册需要 13 秒。此时平台还不知道最终映射到哪个公网端口运维人员也就无法打开远程 Web 页。推荐做法下发开启指令后轮询 frps webServer API直到代理状态为online且拿到conf.remotePort。3.2 API 说明接口方法用途/api/proxies/{name}GET查询单个代理详情含动态端口/api/proxy/tcpGET列出所有 TCP 代理含流量统计认证方式HTTP Basic Auth用户名密码对应webServer.user/webServer.password。单个代理响应示例{name:GW20240001-80,clientID:GW20240001,status:online,todayTrafficIn:1024,todayTrafficOut:512,lastStartTime:08-13 14:30:00,conf:{remotePort:23841}}3.3 Python 实现等待动态端口就绪importbase64importtimefromdataclassesimportdataclassfromtypingimportOptionalimportrequestsdataclassclassFrpServerConfig:inner_ip:str# frps 内网 IP平台与 frps 同 VPC 时使用web_port:int# webServer 端口默认 7500web_user:strweb_password:strout_ip:str# frps 公网 IP拼接远程访问 URL 用classFrpWebApiClient:frps webServer API 封装def__init__(self,config:FrpServerConfig):self.configconfig tokenbase64.b64encode(f{config.web_user}:{config.web_password}.encode()).decode()self.headers{Authorization:fBasic{token}}propertydefbase_url(self)-str:returnfhttp://{self.config.inner_ip}:{self.config.web_port}defget_proxy(self,proxy_name:str)-Optional[dict]:urlf{self.base_url}/api/proxies/{proxy_name}resprequests.get(url,headersself.headers,timeout5)resp.raise_for_status()returnresp.json()deflist_tcp_proxies(self)-list[dict]:urlf{self.base_url}/api/proxy/tcpresprequests.get(url,headersself.headers,timeout5)resp.raise_for_status()dataresp.json()returndata.get(proxies)or[]defget_remote_config_url(frp_client:FrpWebApiClient,proxy_name:str,retry:int3,interval:float3.0,)-Optional[str]: 等待动态代理上线返回远程 Web 访问 URL。 逻辑下发 openRemoteConfig 后轮询直到 statusonline 且 remotePort 可用。 forattemptinrange(retry1):proxyfrp_client.get_proxy(proxy_name)ifproxyandproxy.get(status)online:remote_port(proxy.get(conf)or{}).get(remotePort)ifremote_port:returnfhttp://{frp_client.config.out_ip}:{remote_port}ifattemptretry:time.sleep(interval)returnNone# ---- 使用示例 ----if__name____main__:frpFrpWebApiClient(FrpServerConfig(inner_ip10.0.0.5,web_port7500,web_useradmin,web_passwordYourDashboardPass,out_ip123.45.67.89,))gateway_serialGW20240001proxy_namef{gateway_serial}-80# ① 此处由平台向边缘网关下发 openRemoteConfigremotePort0# send_open_remote_config(gateway_serial, proxy_name)# ② 轮询 API 获取动态端口urlget_remote_config_url(frp,proxy_name)ifurl:print(f远程配置地址{url})else:print(代理未能在预期时间内上线)生产要点平台调用 API 应走 frps内网 IPinner_ip避免 webServer 暴露公网重试 3 次、间隔 3 秒是生产环境验证过的参数返回 URL 直接给前端运维人员点击即可访问网关 Web 页。四、问题二空闲 15 分钟自动断开4.1 为什么用流量而不是连接数TCP 连接在「页面打开但无操作」时可能仍然 ESTABLISHED单纯看连接数无法判断是否真的在用。更可靠的做法是对比todayTrafficIn是否在持续变化——有交互就有流量无交互则流量停滞。4.2 检测策略与生产 Job 一致规则说明执行频率每 2 分钟一次活跃判定todayTrafficIn与上次不同 → 该代理活跃空闲判定流量连续 15 分钟无变化 → 空闲候选启动保护代理lastStartTime在 5 分钟内 → 跳过避免刚创建就被误关多代理合并同一clientID下多个 proxy任一活跃则整台网关视为活跃关闭动作向边缘网关下发closeRemoteConfigfrpc 删除临时代理4.3 Python 实现空闲检测定时任务importloggingfromdatetimeimportdatetime,timedeltafromtypingimportDict,Set,Tuple loggerlogging.getLogger(__name__)# 流量缓存key {frp_server_id}_{proxy_name} → (last_traffic_in, last_change_time)_traffic_cache:Dict[str,Tuple[int,datetime]]{}INACTIVE_THRESHOLD_MINUTES15STARTUP_GRACE_MINUTES5CACHE_CLEANUP_HOURS24def_parse_start_time(last_start_time:str)-datetime|None:解析 frps 返回的启动时间格式如 08-13 14:30:00ifnotlast_start_time:returnNonetry:yeardatetime.now().yearreturndatetime.strptime(f{year}-{last_start_time},%Y-%m-%d %H:%M:%S)exceptValueError:returnNonedef_is_started_within_grace_period(last_start_time:str)-bool:start_parse_start_time(last_start_time)ifnotstart:returnFalsereturnabs((datetime.now()-start).total_seconds())STARTUP_GRACE_MINUTES*60defcheck_proxy_activity(frp_server_id:str,proxy:dict,active_client_ids:Set[str],inactive_candidates:Set[str],)-None:检测单个代理的活跃状态更新缓存与候选集合nameproxy.get(name)client_idproxy.get(clientID)ifnotnameornotclient_id:returncache_keyf{frp_server_id}_{name}current_trafficproxy.get(todayTrafficIn)or0nowdatetime.now()cached_traffic_cache.get(cache_key)ifcachedisNone:_traffic_cache[cache_key](current_traffic,now)returnlast_traffic,last_change_timecachedifcurrent_traffic!last_traffic:_traffic_cache[cache_key](current_traffic,now)active_client_ids.add(client_id)else:idle_minutes(now-last_change_time).total_seconds()/60ifidle_minutesINACTIVE_THRESHOLD_MINUTES:inactive_candidates.add(client_id)defrun_activity_check(frp_client:FrpWebApiClient,frp_server_id:str,close_remote_config_fn,)-str: 主检测流程等价于生产环境 FrpProxyActivityJob。 close_remote_config_fn: callable(client_id) → 向边缘网关下发关闭指令 active_client_ids:Set[str]set()inactive_candidates:Set[str]set()current_keys:Set[str]set()proxiesfrp_client.list_tcp_proxies()forproxyinproxies:ifproxy.get(status)offline:continueif_is_started_within_grace_period(proxy.get(lastStartTime,)):continuecache_keyf{frp_server_id}_{proxy[name]}current_keys.add(cache_key)check_proxy_activity(frp_server_id,proxy,active_client_ids,inactive_candidates)# 多 proxy 合并任一活跃 → 整台网关活跃final_inactiveinactive_candidates-active_client_ids# 清理 24 小时未出现的缓存nowdatetime.now()expired[kfork,(_,t)in_traffic_cache.items()ifknotincurrent_keysand(now-t).total_seconds()CACHE_CLEANUP_HOURS*3600]forkinexpired:del_traffic_cache[k]closed0forclient_idinfinal_inactive:try:close_remote_config_fn(client_id)closed1logger.info(网关 %s 空闲超时已关闭远程配置,client_id)exceptException:logger.exception(关闭网关 %s 远程配置失败,client_id)return(f检测{len(proxies)}个代理f发现{len(final_inactive)}个不活跃网关f已关闭{closed}个)4.4 接入定时调度importscheduleimporttime frpFrpWebApiClient(FrpServerConfig(inner_ip10.0.0.5,web_port7500,web_useradmin,web_passwordYourDashboardPass,out_ip123.45.67.89,))defclose_remote_config(client_id:str)-None:向边缘网关下发 closeRemoteConfig平台长连接通道实现# platform.send_command(client_id, closeRemoteConfig, {})passdefjob():resultrun_activity_check(frp,frp_server_id1,close_remote_config_fnclose_remote_config)print(f[{datetime.now():%H:%M:%S}]{result})schedule.every(2).minutes.do(job)if__name____main__:whileTrue:schedule.run_pending()time.sleep(1)五、两个问题的协作时序运维人员点击「远程配置」 │ ▼ 平台 ──openRemoteConfig(remotePort0)──▶ 边缘网关 frpc 建代理 │ ▼ 平台轮询 GET /api/proxies/{serial}-80最多 3 次 │ ▼ 返回 http://公网IP:{动态端口} 给前端 │ ▼ 运维人员操作网关 Web 页产生 todayTrafficIn 变化 │ ▼ 定时任务每 2 分钟检测流量 │ ├── 有变化 → 重置计时保持穿透 │ └── 15 分钟无变化 → closeRemoteConfig → 代理删除端口释放六、常见问题Q1remotePort0后 API 一直查不到端口检查 frps 是否配置了allowPorts端口池确认边缘网关 frpc 已成功连接 frps仪表盘可见 client适当加大轮询次数或间隔。Q2刚打开远程页就被自动关闭确认定时任务对lastStartTime5 分钟内的新代理做了启动保护首次检测只写缓存、不做空闲判定。Q3页面开着但流量不变会被误关吗会。纯静态页面无请求时流量确实可能不变。生产环境可结合前端心跳或适当延长阈值如 30 分钟。Q4webServer API 要不要暴露公网不要。平台与 frps 同 VPC走内网 IP 调用即可仪表盘端口安全组仅放行运维 IP。本篇小结边缘网关临时远程运维场景下remotePort0 webServer API 反查是动态端口的标准解法GET /api/proxies/{name}获取实际端口GET /api/proxy/tcp批量拉取流量统计以todayTrafficIn变化判断活跃15 分钟无变化自动关闭比看连接数更可靠多代理按clientID合并判定新代理 5 分钟保护期避免误关方案可直接移植到同类视频物联网平台的边缘网关远程运维场景。系列导航正篇回顾建议从第一篇至第七篇顺序阅读本篇为番外实战聚焦边缘网关 FRP 工程化与正篇互补 提示若你的业务涉及批量边缘网关与视频设备接入可了解畅联云平台——面向视频物联网的底层设备接入平台。若只需个人级 FRP 穿透正篇教程已足够。