拓冰建站拓冰建站
首页 / 资讯中心 / 正文

libwebsockets 故障注入(lws_fi)完全指南:构建选项、规则语法与内置故障清单

人工智能AI Agent多模态语音AI 应用【免费下载链接】ten-frameworkOpen-source framework for conversational voice AI agents项目地址https://gitcode.com/TEN-framework/ten-framework点击查看免费下载lws_fiFault Injection是 libwebsockets 提供的一套系统性故障注入机制它在构建期通过LWS_WITH_SYS_FAULT_INJECTION选项开启允许开发者在任意 lws 内部代码或用户代码中按名称精准地合成 DNS 失败、连接失败、OOM、发送失败等难以在测试期自然复现的故障从而验证错误处理路径的正确性。本文以 README.fault-injection.md 为骨架结合 lws-fault-injection.h 与 fault-injection.c 的源码实现系统讲解其设计原理、注入策略、命名空间继承、命令行接入方式并给出完整的已知内置故障名清单读者可据此在自己的 lws 应用或测试中直接复现各种故障场景。上图展示了 lws_fi 的核心思想用户准备好的故障规则lws_fi_ctx_t既可以绑定到lws_context再经由lws_vhost、Secure Stream 等子对象逐级继承分发也可以直接附加到客户端wsi或 Secure Stream 对象上故障规则会沿匹配的每个层级向下传播到子对象Faults propagate into subobjects at each matching level。为什么需要故障注入日常开发中绝大部分精力都花在让系统在正常运行时做它该做的事上。但若要保证质量仅仅测试正常路径是不够的那些低概率的错误分支例如清理了尚未初始化的资源、忘记释放内存等在测试期极难自然触发却又往往是线上事故的根源。lws 故障注入的目标就是解决这一痛点它提供简单而强大的 API允许在任何 lws 或用户代码中定向注入故障——包括早期初始化阶段甚至 lws 初始化之前的用户代码。同时lws 内部预置了大量知名故障点well-known faults可以从外部直接触发无需修改被测代码。核心概念故障上下文与故障故障注入的基本模型是用户代码中的对象可以选择在自身内部初始化故障上下文fault contexts该上下文列出一组命名清晰的故障faults代表该代码支持、且用户希望注入的故障类型。从 lws-fault-injection.h 可以看到两个核心数据结构typedef struct lws_fi { const char *name; /* 故障名称 */ const uint8_t *pattern; /* PATTERN 类型的位模式 */ uint64_t pre; /* DETERMINISTIC: 前置次数PROBABILISTIC: 百分比RANGE: 范围下限 */ uint64_t count; /* DETERMINISTIC: 注入次数PATTERN: 位宽RANGE: 范围上限 */ uint64_t times; /* 起始为 0记录被查询的次数 */ char type; /* LWSFI_* 注入策略类型 */ } lws_fi_t; typedef struct lws_fi_ctx { lws_dll2_owner_t fi_owner; /* lws_fi_t 的双链表 */ struct lws_xos xos; /* xoshiro256 PRNG 状态用于概率类注入 */ const char *name; } lws_fi_ctx_t;lws_fi_t一条故障注入规则。每次注入请求都由它描述——注入哪个故障name以及何时、以多大概率注入typepre/count/pattern/times。lws_fi_ctx_t故障上下文本质是lws_fi_t对象构成的链表lws_dll2_owner_t fi_owner内嵌一个 xoshiro256 伪随机数生成器状态用于概率类决策。这些故障上下文既可以直接嵌入对象例如嵌入lws_context创建信息结构体、客户端连接信息结构体或 Secure Stream 信息结构体也可以在对象创建后由系统对象携带。启用故障注入构建后lws_context、lws_vhost、wsi、Secure Stream 句柄 / SSPC 句柄等关键系统对象都各自持有自己的lws_fi_ctx_t链表可以挂接任意数量的lws_fi_t。不过直接向深层代码传参往往并不方便。例如想在一个由 Secure Stream 创建的 wsi 上触发故障该 wsi 由 lws 内部代码创建与 Secure Stream 对象创建隔了一层难以在创建时直接附加。因此 lws_fi 设计了基于命名空间路径namespace paths的定向继承方案通常只需在 context 创建时列出想要的故障它们就会在后续创建内部对象时被过滤分发到目标对象上。这也正是 fault-injection.c 中lws_fi_inherit_copy()的实现职责按 scope如vh与 value如myvhost匹配vhmyvhost/前缀的规则去掉匹配前缀后把副本挂到目标故障上下文。构建选项LWS_WITH_SYS_FAULT_INJECTIONlws_fi 通过一个构建期选项开启默认关闭。在仓库的 CMakeLists.txt 中option(LWS_WITH_SYS_FAULT_INJECTION Enable fault injection support OFF)在编译 lws 时可通过-DLWS_WITH_SYS_FAULT_INJECTIONON或环境变量LWS_WITH_SYS_FAULT_INJECTION1打开。需要强调的是默认行为零副作用仅开启构建选项并不会影响代码运行因为用户还必须显式地把想要的故障规则加入对象的故障上下文故障才会被注入。此外当该选项被关闭时lws_fi()调用以及所有公开的lws_fi_user_*_fi()API 都会退化为编译期常量(0)见 lws-fault-injection.h相关判断代码会被编译器整体移除零运行时开销。在 lws 私有代码中集成故障条件lws 私有代码中最简单的查询接口是lws_fi(fi_ctx, name)返回0本次不需要注入故障返回1本次应当合成该故障。如果上下文中没有匹配name的规则则总是返回0不注入。从 fault-injection.c 的实现可以看到lws_fi()先在上下文的链表中查找同名规则找不到直接返回 0找到后按规则的type字段执行对应的决策逻辑决定注入时还会打印一条告警日志Injecting fault %s-%s这正是文档示例中日志的来源。同时若构建期禁用了故障注入lws_fi()恒为常量0代码中已有的lws_fi()调用可以原样保留、由编译器消除。在用户代码中使用公开 API用户代码无需理解故障上下文内部结构只要手握一个常见对象如 wsi与规则名称即可查询。lws 提供了四组公开 API故障上下文持有者公开 APIlws_contextint lws_fi_user_context_fi(struct lws_context *ctx, const char *rule)wsiint lws_fi_user_wsi_fi(struct lws *wsi, const char *rule)ss 句柄int lws_fi_user_ss_fi(struct lws_ss_handle *h, const char *rule)sspc 句柄int lws_fi_user_sspc_fi(struct lws_sspc_handle *h, const char *rule)它们的底层实现fault-injection.c其实就是对各自对象内嵌fic调用lws_fi()。实战示例minimal-http-client仓库中的 minimal-http-client.c 在LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP回调中就有这样一段用户代码if (lws_fi_user_wsi_fi(wsi, user_reject_at_est)) return -1;即当规则wsi/user_reject_at_est命中时在连接建立ESTABLISHED阶段主动返回-1拒绝连接。运行它并注入该故障lws-minimal-http-client --fault-injection wsi/user_reject_at_est运行日志示意如下... [2021/03/11 13:41:05:2769] U: Connected to 46.105.127.147, http response: 200 [2021/03/11 13:41:05:2776] W: lws_fi: Injecting fault unk-user_reject_at_est [2021/03/11 13:41:05:2789] E: CLIENT_CONNECTION_ERROR: HS: disallowed at ESTABLISHED ...可以看到连接本身成功HTTP 200随后故障被注入连接因在 ESTABLISHED 阶段被拒绝而报出CLIENT_CONNECTION_ERROR完整地走了一遍错误路径。注入时机策略LWSFI_* 类型lws_fi_t.type字段决定何时注入。API 会记录每次被查询的次数times并根据规则类型做出决策。源码 fault-injection.c 中的 switch 分支与头文件 lws-fault-injection.h 中的枚举一一对应注入规则类型描述LWSFI_ALWAYS无条件注入故障LWSFI_DETERMINISTIC在前pre次不注入之后接下来count次每次都注入LWSFI_PROBABILISTIC以pre百分比概率注入LWSFI_PATTERN以pre指向的位模式为参考count个 bit某次查询对应的 bit 为 1 则注入指向静态数组LWSFI_PATTERN_ALLOC同PATTERN但模式数组为动态分配故障上下文销毁时释放LWSFI_RANGE不用于lws_fi()决策而是在pre与count之间取一个伪随机数实现细节DETERMINISTICtimes自增后落在[pre, precount)区间内才注入PROBABILISTIC调用lws_xos_percent(fic-xos, pre)即 PRNG 结果% 100 pre时注入100 恒真、0 恒假中间线性缩放PATTERN / PATTERN_ALLOC取times % count作为 bit 下标pattern[n 3] (1 (n 7))为真则注入RANGE返回pre (lws_xos(fic-xos) % (count - pre))从外部给定的123..456范围中取数见lws_fi_range()fault-injection.c。概率选择来源于 PRNG其种子在 context 创建信息结构的故障注入上下文中设置。默认情况下lws 辅助函数lws_cmdline_option_handle_builtin()把种子设为当前时间微秒但可以用--fault-seed decimal覆盖命令行选项首次解析时会把实际生效的 PRNG 种子记入日志便于复现。向 lws_fi_ctx_t 添加故障规则通常把lws_context作为定义故障的中央顶层位置。做法是在栈上准备好一个lws_fi_t再逐一通过lws_fi_add(lws_fi_ctx_t *fic, const lws_fi_t *fi)把它加入 context 创建信息结构体的.fic成员。lws_fi_add()会分配内存、拷贝传入的fi并挂到lws_fi_ctx_t链表上因此传入的fi可以安全地离开作用域fault-injection.c。当 context或其他使用同一机制的对象被创建时它会从信息结构体的.fic导入全部故障并接管所有权随后把.fic清空使其可以安全地随 info 结构体离开作用域。这一转移由lws_fi_import()完成——它还会顺带继承源上下文的 PRNG 种子fault-injection.c。配套的辅助 API 还有lws_fi_remove(fic, name)按名移除并销毁一条规则lws_fi_destroy(fic)清空该上下文中所有已分配的规则PATTERN_ALLOC模式会先释放模式数组lws_fi_inherit_copy(fic_dest, fic_src, scope, value)按 scope/value 匹配父上下文中的命名空间规则并拷贝给子对象命名空间机制的核心实现lws_fi_deserialize(fic, sers)把字符串形式的规则如sscaptive_portal_detect/wsi/dnsfail(10%)解析成lws_fi_t加入上下文fault-injection.c。规则的传入时机必须在对象创建之前故障注入的一个关键要求是规则必须在创建对象的代码面前可用早于对象被创建。这就是为什么用户代码要在创建信息结构体中预先准备好故障上下文、列出规则而不是等对象创建后再附加——那时再去测试创建期间的故障已经太迟了。直接应用故障上下文创建以下四类对象时可以直接传入一个预先用lws_fi_t准备好的故障上下文被创建的对象信息结构体故障上下文成员lws contextstruct lws_context_creation_infoficvhoststruct lws_context_creation_infoficSecure Streamstruct lws_ss_infofic客户端 wsistruct lws_client_connect_infofic例如在 lws-context-vhost.h 中struct lws_context_creation_info在LWS_WITH_SYS_FAULT_INJECTION条件下带有lws_fi_ctx_t fic;成员lws-client.h 中struct lws_client_connect_info同样带有fic以及一个与构建选项无关、始终可用的fi_wsi_name成员。不过实践中更常用的做法是只在 context 创建时给出故障列表让对象通过命名空间匹配并继承——这就是下一节的内容。用命名空间精准定位实例直接创建的 vhost、Secure Stream 或 wsi 可以在创建时直接挂接子规则无需命名空间。命名空间用于你只握有更上层对象如lws_context的创建时机、而目标对象由它内部创建、你没有直接句柄的场景——例如想在某个 vhost 的监听 socket 上触发故障。命名空间采用/path/形式规则名可以带上前缀使后创建的对象继承命名空间形式效果vhmyvhost/子规则名为 myvhost 的 vhost 被创建时继承该子规则vh/子规则任意 vhost 被创建时继承该子规则ssmystream/子规则streamtype 为 mystream 的 SS 继承也覆盖 SSPC / proxy 客户端ss/子规则任意 streamtype 的所有 SS 继承也覆盖 SSPC / proxy 客户端wsimyname/子规则以info-fi_wsi_name为 myname 创建的客户端 wsi 继承wsi/子规则任意 wsi 继承命名空间可以组合。例如vhmyvhost/wsi/listenskt在服务器 vhost myvhost 创建的 wsi 上设置listenskt故障即让该 vhost 的监听 socket 在创建时报错。值得注意的细节h2 连接上的网络连接 wsi 被迁移为 SID 1 时wsi migration其上已挂接的故障也会一并迁移。各对象的继承来源一览对象类型初始化的来源从何处继承匹配的故障contextstruct lws_context_creation_info.fic-vhoststruct lws_context_creation_info.ficcontext FIC客户端 wsistruct lws_client_connect_info.ficcontext FIC, vhost FICss / sspclws_ss_info_t.ficcontext FICss / sspc 的 wsi-context FIC, vhost FIC, ss / sspc .fic由于所有对象都能直接或通过逐级继承从lws_context的故障上下文触达而从外部又最方便在 context 上设置规则因此context 通常是所有注入故障的原始来源。与 minimal 示例的集成--fault-injection 与 --fault-seed所有使用lws_cmdline_option_handle_builtin()API 的 minimal 示例都能额外接收--fault-injection ...,...开关该开关会自动解析参数中的逗号分隔列表把给出的故障名加入lws_context。例如lws-minimal-http-client --fault-injection wsi/dnsfail会强制该次运行中所有 wsi 的 DNS 查询失败。从 libwebsockets.c 可以看到内建命令行选项数组包含-d、--fault-injection、--fault-seed、--ignore-sigterm四项解析到--fault-injection时调用lws_fi_deserialize(info-fic, p)把字符串解析成规则解析到--fault-seed时把十进制字符串转为 64 位种子libwebsockets.c。指定何时注入的语法默认情况下如果只给出名称部分只要命名空间缺失或匹配到对象故障就会每次注入。也可以用括号附加额外信息实现随机概率或循环模式注入语法配合使用含义wsi/thefaultlws_fi()每次都注入故障wsi/thefault(10%)lws_fi()以 10% 概率随机注入wsi/thefault(.............X.X)lws_fi()每 16 次尝试中在第 14 和第 16 次注入wsi/thefault2(123..456)lws_fi_range()在 123 与 456 之间取一个数注意包含这些符号的字符串必须用引号包裹否则可能被 shell 解释。pattern 中的.表示该次不注入X表示该次注入。最后一个示例(123..456)并不像其他示例那样通过lws_fi()决定是否注入而是配合lws_fi_range()使用作为故障处理流程中的一个次级故障名。典型场景先用myfault配合lws_fi()决定何时注入故障再用第二个相关故障名myfault_delay为故障动作引入一个外部给定范围内的随机延迟毫秒级myfault(10%),myfault_delay(123..456)代码中调用lws_fi_range()查询myfault_delay即可拿到 123456 之间的伪随机数用于控制延迟等参数见lws_fi_range()的文档注释 lws-fault-injection.h。lws 内置的知名故障名清单lws 内部预置了大量可在外部直接触发的故障点覆盖 context 创建、vhost 创建、客户端连接、UDP、Secure Streamsss / sspc / ssproxy等各层次范围命名空间名称故障效果contextctx_createfail1进入时立即让 context 创建失败contextctx_createfail_plugin_init让 context 创建失败如同插件初始化失败启用插件时contextctx_createfail_evlib_plugin让 context 创建失败如同事件库插件初始化失败启用 evlib 插件时contextctx_createfail_evlib_sel让 context 创建失败如同无法选择事件库contextctx_createfail_oom_ctx让 context 创建因 context 对象 OOM 而失败contextctx_createfail_privdrop让 context 创建因权限降级失败而失败contextctx_createfail_maxfds让 context 创建因无法确定进程 fd 上限而失败contextctx_createfail_oom_fds让 context 创建因 fds 表 OOM 而失败contextctx_createfail_plat_init让 context 创建因平台初始化失败而失败contextctx_createfail_evlib_init让 context 创建因事件库初始化失败而失败contextctx_createfail_evlib_pt让 context 创建因事件库 pt 初始化失败而失败contextctx_createfail_sys_vh让 context 创建因系统 vhost 创建失败而失败contextctx_createfail_sys_vh_init让 context 创建因系统 vhost 初始化失败而失败contextctx_createfail_def_vh让 context 创建因默认 vhost 创建失败而失败contextctx_createfail_ss_pol1让 context 创建因 ss 策略解析开始失败而失败启用策略时contextctx_createfail_ss_pol2让 context 创建因 ss 策略解析失败而失败启用策略时contextctx_createfail_ss_pol3让 context 创建因 ss 策略设置失败而失败启用策略时contextcache_createfail让lws_cache创建因 OOM 失败contextcache_lookup_oom让lws_cache查找因 OOM 失败vhostvhvh_create_oomvh 创建时对象分配 OOMvhostvhvh_create_pcols_oomvh 创建时协议表分配 OOMvhostvhvh_create_access_log_open_failvh 创建因无法打开访问日志而失败LWS_WITH_ACCESS_LOGvhostvhvh_create_ssl_srv服务端 ssl_ctx 初始化失败vhostvhvh_create_ssl_cli客户端 ssl_ctx 初始化失败vhostvhvh_create_srv_init服务器初始化失败vhostvhvh_create_protocol_init延迟协议初始化失败用于延迟创建的 vhost服务端 vhostvhxxx/wsilistenskt让 vhost 监听 socket 的socket()分配失败客户端 wsiwsidnsfail同步不调用getaddrinfo()并合成EAI_FAIL返回异步请求不启动并立即合成失败客户端 wsiwsisendfail在 wsi socket 上发送数据失败客户端 wsiwsiconnfail在 wsi socket 上连接失败客户端 wsiwsicreatefail创建客户端 wsi 本身失败udp wsiwsiudp_rx_loss丢弃实际收到的 UDP RX配合概率模式使用udp wsiwsiudp_tx_loss丢弃 UDP TX 使其不被真正发送配合概率模式使用服务端 ssssss_srv_vh_fail强制 Secure Streams 服务端 vhost 创建失败客户端 ssssss_no_streamtype_policy让该 streamtype 的策略看似缺失sspcsssspc_fail_on_linkup听到代理连接成功时拒绝该连接引发无限重试sspcsssspc_fake_rxparse_disconnect_me让客户端-代理链路解析看似请求断开引发无限重试sspcsssspc_fake_rxparse_destroy_me让客户端-代理链路解析看似请求销毁 SS将干净地销毁 SSsspcsssspc_link_write_fail强制链路写入失败引发无限重试sspcsssspc_create_oom让 sspc 句柄分配在创建时如同 OOM 失败sspcsssspc_fail_metadata_set让 metadata 分配失败sspcsssspc_rx_fake_destroy_me让客户端的用户代码rx()看似返回 DESTROY_MEsspcsssspc_rx_metadata_oom让来自代理的 metadata 分配失败ssproxyssssproxy_dsh_create_oom让代理的 DSH 创建失败ssproxyssssproxy_dsh_rx_queue_oom让 SS-P[-C] DSH rx 方向的分配如同 OOM 失败导致后续连接断开ssproxywsissproxy_client_adopt_oom让代理无法为新客户端-代理链路连接对象分配内存ssproxywsissproxy_client_write_fail让代理对客户端的写入失败ssproxywsisspc_dsh_ss2p_oom让 ss-proxy dsh 分配失败ssproxyssssproxy_onward_conn_fail让代理的后续客户端连接立即失败ssproxyssssproxy_dsh_c2p_pay_oom让代理的 C-P payload DSH 分配失败ssssss_create_smdSMDss 创建时 smd 注册失败ssssss_create_vhost服务端ss 创建时看似没有匹配 typename 的 vhost仅!vhostssssss_create_pcol服务端ss 创建时看似策略中未给出协议ssssss_srv_vh_fail服务端ss 创建时看似无法创建 vhostssssss_create_destroy_mess 创建时看似 CREATING 状态返回了 DESTROY_MEssssss_create_no_ts静态策略ss 创建时看似没有 trust storessssss_create_smd_1SMDss 创建时看似 CONNECTING 说了 DESTROY_MEssssss_create_smd_2SMDss 创建时看似 CONNECTED 说了 DESTROY_MEssssss_create_connNailed upss 创建时客户端连接以 DESTROY_ME 失败wsiwsitimedclose让 wsi 在一段时间后关闭配合下一个使用wsiwsitimedclose_mstimedclose的毫秒范围如timedclose_ms(10..250)这些故障名与文档、fault-injection.c 中的实现一一对应实际触发时lws 会在日志中打印Injecting fault告警便于确认故障确实被注入。已知的命名空间目标内部 wsi 与自定义 wsi 名命名空间可以更精准地定位目标。即便只把故障传给lws_context也能用命名空间路径只针对特定来源创建的 wsi。要定位 SS 连接产生的 wsi可使用ssstream_type_name/。以 captive portal 检测CPD为例sscaptive_portal_detect/ss_no_streamtype_policy # 让 CPD 找不到策略条目从而禁用 CPD sscaptive_portal_detect/wsi/dnsfail # 让 CPD 无法解析服务器 DNS看起来没有网络 sscaptive_portal_detect/wsi/connfail # 针对 CPD 的连接测试部分同样让 CPD 认为没有网络内部 wsi 类型名lws 为内部功能如异步 DNS 处理创建的 wsi 也可以被精准定位wsi 目标含义wsiasyncdns/lws 异步 DNS 支持用来与 DNS 服务器通信的 UDP wsiwsidhcpc/lws DHCP 客户端使用的 UDP wsiwsintpclient/lws NTP 客户端使用的 UDP wsi例如在lws_context级别传入wsiasyncdns/udp_tx_loss将抑制异步 DNS 的 UDP 发送从而强制其无法解析任何域名。对于用户自己创建的 wsi可以在客户端连接创建时把客户端创建信息结构体的.fi_wsi_name设置为字符串 xxxlws-client.h之后就能用wsixxx/命名空间只对这批 wsi 生效。minimal-http-client 中i.fi_wsi_name userminimal-http-client.c正是为此配合wsi/user_reject_at_est等规则即可精确命中该客户端 wsi。小结lws_fi 提供了一条从构建期开启 → 创建期挂规则 → 运行时按策略触发的完整故障注入链路构建期以-DLWS_WITH_SYS_FAULT_INJECTIONON编译 lws关闭时所有 API 退化为常量零开销。规则准备通过lws_fi_add()/lws_fi_deserialize()把lws_fi_t规则装入lws_fi_ctx_t通常挂在lws_context_creation_info.fic上对象创建时用lws_fi_import()接管用lws_fi_inherit_copy()按vhxxx/、ssxxx/、wsixxx/命名空间向下分发。触发查询lws 私有代码用lws_fi()用户代码用lws_fi_user_*_fi()系列公开 API按LWSFI_ALWAYS / DETERMINISTIC / PROBABILISTIC / PATTERN / PATTERN_ALLOC / RANGE策略决策概率决策由 xoshiro256 PRNG 支撑种子可通过--fault-seed固定以便复现。命令行接入minimal 示例可通过--fault-injection a,b,c直接注入(10%)、(....X.X)、(123..456)语法分别对应概率、位模式与范围取数。配合内置的数十个知名故障点context/vhost 创建失败、dnsfail、connfail、sendfail、udp_tx_loss、ss/sspc/ssproxy 系列测试人员无需构造复杂的真实故障环境即可在 CI 或本地一键复现原本低概率出现的错误路径是验证 lws 应用健壮性的实用工具。赞分享人工智能AI Agent多模态语音AI 应用【免费下载链接】ten-frameworkOpen-source framework for conversational voice AI agents项目地址https://gitcode.com/TEN-framework/ten-framework点击查看免费下载相关推荐SimianArmy配置详解自定义故障注入规则与策略SimianArmy配置详解自定义故障注入规则与策略 项目概述 SimianArmy是一套云环境运维工具集其中最著名的组件是Chaos Monkey混沌猴测试云原生LitmusChaos故障注入类型大全网络、CPU、内存、磁盘等故障的完整解析LitmusChaos故障注入类型大全网络、CPU、内存、磁盘等故障的完整解析 LitmusChaos是一个强大的云原生混沌工程平台专为Kubernetes云原生运维可观测性突破线上故障瓶颈JVM-SANDBOX从零构建故障注入工具实战指南突破线上故障瓶颈JVM SANDBOX从零构建故障注入工具实战指南 你是否还在为线上系统故障排查而头疼是否遇到过生产环境难以复现的偶发bug本文将带你使用开发工具后端上一篇jQuery 速查表reference 项目中开发者高频 API 的快速参考指南下一篇高效掌握ControlNet-v1-1_fp16_safetensors从问题诊断到实战优化的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门